From 291a4ac826a7eb51bc4db275c937fe47468892e8 Mon Sep 17 00:00:00 2001 From: ryanbr Date: Fri, 10 Jul 2026 20:10:05 +1200 Subject: [PATCH 1/2] Settings: fix profile-photo blurb clipping + HRV-window label truncation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two small Settings layout fixes: - Profile-photo blurb clipped to the card edge. Interpolating `Platform.deviceNounPhrase` (an already-resolved localized String) into the `blurb:` `LocalizedStringKey` literal confused SwiftUI's text-measurement pass, so the blurb rendered with zero trailing margin instead of wrapping inside the card padding. Resolve the whole sentence through `String(localized:)` first, then hand SwiftUI the plain String via `LocalizedStringKey(_:)`. The localization key is unchanged (`…Stored only on %@…`), so existing translations still apply. Also drop the stray `.frame(maxWidth: .infinity)` on the button column. - HRV-window picker: "Whole night" → "Night" so the two-segment control doesn't truncate once it sizes to the row (some locales' longer translations overflowed), matching the Temperature/Theme pickers. Adds the "Night" String Catalog entry (de/es/fr/it/pt/zh). Extracted from #153 (which bundled unrelated HCI tooling + already-merged changes); this is the settings-layout part only. --- Strand/Resources/Localizable.xcstrings | 46 ++++++++++++++++++++++++++ Strand/Screens/SettingsView.swift | 17 +++++++--- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/Strand/Resources/Localizable.xcstrings b/Strand/Resources/Localizable.xcstrings index eabb5820..b7cbfd78 100644 --- a/Strand/Resources/Localizable.xcstrings +++ b/Strand/Resources/Localizable.xcstrings @@ -1,6 +1,52 @@ { "sourceLanguage": "en", "strings": { + "Night": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nacht" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Noche" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nuit" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Notte" + } + }, + "pt-PT": { + "stringUnit": { + "state": "translated", + "value": "Noite" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "夜间" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "夜間" + } + } + } + }, "": { "localizations": { "de": { diff --git a/Strand/Screens/SettingsView.swift b/Strand/Screens/SettingsView.swift index bfdf66b7..bc0a7c8d 100644 --- a/Strand/Screens/SettingsView.swift +++ b/Strand/Screens/SettingsView.swift @@ -249,10 +249,17 @@ struct SettingsView: View { /// iOS 16+ and macOS 13+ (NOOP's floor), so the same control serves both platforms — no /// availability gating needed. The photo is stored only on this device (NOOP is fully offline). private var profilePhotoCard: some View { - SettingsSection( + // #153: resolve the whole blurb through `String(localized:)` first, then hand SwiftUI the plain + // String via `LocalizedStringKey(_:)`. Interpolating `Platform.deviceNounPhrase` (itself an + // already-resolved localized String) straight into the `blurb:` `LocalizedStringKey` literal + // confused SwiftUI's text-measurement pass — the blurb rendered with zero trailing margin and + // clipped to the card edge instead of wrapping inside the card padding. The localization key is + // unchanged (`…Stored only on %@…`), so the existing translations still apply. + let blurbText = String(localized: "Optional. Add a photo for the avatar in the top-left. Stored only on \(Platform.deviceNounPhrase). NOOP is offline, so it's never uploaded.") + return SettingsSection( icon: "person.crop.circle", title: "Profile photo", - blurb: "Optional. Add a photo for the avatar in the top-left. Stored only on \(Platform.deviceNounPhrase). NOOP is offline, so it's never uploaded." + blurb: LocalizedStringKey(blurbText) ) { HStack(spacing: 16) { ProfileAvatarView(imageData: profile.avatarImageData, size: 64) @@ -270,7 +277,6 @@ struct SettingsView: View { .accessibilityHint("Reverts to the default profile icon") } } - .frame(maxWidth: .infinity) } } // Load the picked photo's bytes, then hand them to the store (which downscales + persists). @@ -889,7 +895,10 @@ struct SettingsView: View { // re-baselines (like a sleep edit). FormRow(label: "HRV window") { Picker("HRV window", selection: $hrvWindowRaw) { - Text("Whole night").tag(HrvWindow.whole.rawValue) + // #153: "Night" (not "Whole night") — a single short word so the two-segment + // control doesn't truncate once it sizes to the row (some locales' longer + // translations overflowed), matching the Temperature/Theme pickers above. + Text("Night").tag(HrvWindow.whole.rawValue) Text("Deep sleep").tag(HrvWindow.deep.rawValue) } .labelsHidden() From e8d20b941884a68f7b20397858cbcace38e3b045 Mon Sep 17 00:00:00 2001 From: ryanbr Date: Fri, 10 Jul 2026 20:20:14 +1200 Subject: [PATCH 2/2] =?UTF-8?q?Settings=20(Android):=20HRV-window=20label?= =?UTF-8?q?=20"Whole=20night"=20=E2=86=92=20"Night"=20for=20parity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the iOS label change so the HRV-window two-segment control reads the same word on both platforms. (The blurb-clipping half of #203 is a SwiftUI text-measurement quirk with no Compose equivalent, so it stays iOS-only.) --- android/app/src/main/java/com/noop/ui/SettingsScreen.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/noop/ui/SettingsScreen.kt b/android/app/src/main/java/com/noop/ui/SettingsScreen.kt index 17f6c835..05f20645 100644 --- a/android/app/src/main/java/com/noop/ui/SettingsScreen.kt +++ b/android/app/src/main/java/com/noop/ui/SettingsScreen.kt @@ -1355,7 +1355,9 @@ fun SettingsScreen( SegmentedPillControl( items = listOf(HrvWindow.WHOLE_NIGHT, HrvWindow.DEEP_SLEEP), selection = hrvWindow, - label = { if (it == HrvWindow.DEEP_SLEEP) "Deep sleep" else "Whole night" }, + // #153: "Night" (not "Whole night") so the two-segment pill reads the same as the iOS + // picker and stays short — keeps the label consistent across platforms. + label = { if (it == HrvWindow.DEEP_SLEEP) "Deep sleep" else "Night" }, onSelect = { hrvWindow = it UnitPrefs.setHrvWindow(context, it)