Skip to content

Add language settings, normalized language in app - #1004

Closed
lukezielke wants to merge 2 commits into
ryanbr:mainfrom
lukezielke:fix/language-mixing
Closed

lukezielke wants to merge 2 commits into
ryanbr:mainfrom
lukezielke:fix/language-mixing

Conversation

@lukezielke

@lukezielke lukezielke commented Jul 31, 2026 •

Copy link
Copy Markdown

What this PR does

Fixes mixed-language UI by applying the selected language consistently to localized copy and locale-sensitive formatting.

Adds an app-language setting on Apple and Android with:

  • System default
  • English
  • Deutsch
  • Español
  • Français
  • Português
  • 中文

Apple preserves the device region while applying the selected UI language and requires reopening NOOP. Android applies the language immediately and updates its process locale, so locale-sensitive date order and number formatting may also change. Units and time zone remain independently configured.

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Documentation
  • CI / tooling

How it was tested

  • Added Apple and Android language-selection unit tests, including explicit coverage for the Chinese zh tag.
  • Tools/i18n_audit.py --ci origin/main passes.
  • All 36 localization-audit unit tests pass.
  • Tools/doc_comment_lint.py passes.
  • Verified localization catalogs parse and Android resource references resolve.
  • git diff --check passes.
  • Android unit tests could not run locally because the Android SDK is unavailable.
  • Apple app tests could not run because this environment does not have Xcode.
  • No BLE, protocol, or analytics behavior changed; hardware testing is not applicable.

Checklist

  • Swift package tests pass for any package I touched (swift test in Packages/<name>) — not applicable; no package was touched
  • Android unit tests pass if I touched android/ — blocked locally by the missing Android SDK
  • No new build warnings introduced — platform builds were unavailable
  • UI changes use only StrandDesign tokens — the restart note uses NoopMetrics.space1
  • No hardcoded hex frame bytes; protocol facts live in the schema / decoders
  • Follows the conventions in docs/CONTRIBUTING.md
  • I did not commit generated output (Strand.xcodeproj/) or any secrets/keystores

Related issues

None

@ryanbr

ryanbr commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Good feature, and the implementation is in better shape than your checklist suggests. Two things to fix before it lands, one of which is user-visible.

Chinese is missing, and the PR translates the picker into Chinese

The app ships 1,711 Chinese strings in values-zh, and this PR adds the new picker's own strings to that file — settings_language, settings_choose_language, settings_language_system. But the enum offers only en/de/es/fr/pt-PT, on both platforms.

So a Chinese user opens a language picker rendered in Chinese and cannot find Chinese in the list. Adding CHINESE("zh", "中文") to AppLanguage and case chinese = "zh" to the Swift twin should be all it needs, since the catalogue is already there.

.padding(.top, 6) — and 6 is not on the scale

You flagged this yourself. Worth noting the scale is 4 / 8 / 12 / 16 / 20 (NoopMetrics.space1…space5), so there is no 6-point token to reach for — it needs space1 or space2, not a new token. "Design system is law" is one of the hard rules in CLAUDE.md, so this is the one blocking item.

The PR description contradicts the implementation

Regional preferences such as time zone, units, date order, and 24-hour clock remain unchanged.

Locale.setDefault(locales[0]) changes date order and number formatting process-wide — your own doc comment on AppLanguagePrefs says as much ("keeps date/month words and locale-aware casing aligned"). Date order does not remain unchanged; it follows the selected language. That is arguably the right behaviour, but the description should say so.

I checked whether it can corrupt anything stored, and it cannot, for the languages you offer: WhoopRepository.freshnessDayKey and the analytics formatters already pin Locale.US, and en/de/es/fr/pt all use ASCII digits and the Gregorian calendar, so DateTimeFormatter.ofPattern("yyyy-MM-dd") is stable across them.

That safety is a property of the language list, not of the code. The importers (XiaomiBandImporter, WearableExportImporter, CaptureImporter) build day keys with ofPattern("yyyy-MM-dd") and no explicit locale, so adding Arabic or Thai to that enum later would silently change stored day keys — Eastern Arabic numerals, or a Buddhist-calendar year. Worth a comment on the enum saying that, because the next person adding a language will not know it.

Testing, verified rather than taken on faith

No CI ran on this at all — it is a fork PR and zero checks triggered, which is why it shows UNSTABLE. Since you could not build locally either, I ran what I could:

  • Kotlin compiles clean. Full src/main/java against main: the only new errors are twelve unresolved reference: R in SettingsScreen.kt, which is my ad-hoc classpath having no generated R. I checked all ten new R.string.* references by name against values/strings.xml — all present, so a real build resolves them.
  • Tools/i18n_audit.py --ci passes, and Tools/doc_comment_lint.py passes.
  • StrandTests/AppLanguageTests.swift never runs. StrandTests appears only in app-build.yml, which is disabled — so the Apple half of "added unit tests" is not executed by anything today. Not your fault, and worth knowing when you say the tests pass. The Android AppLanguageTest.kt does run, under testFullDebugUnitTest.

Fix the Chinese omission and the padding token, and I am happy to take this.

@lukezielke

Copy link
Copy Markdown
Author

Thanks for the review. I addressed each item:

  • Added Chinese (zh / 中文) to both the Swift and Kotlin language enums. The picker now exposes the Chinese catalogs already shipped by both platforms.
  • Added explicit Apple and Android tests covering the zh tag and autonym.
  • Replaced .padding(.top, 6) with NoopMetrics.space1, so the UI change now stays on the design-system spacing scale.
  • Updated the PR description to accurately distinguish the platform behavior: Apple preserves the device region, while Android changes the process locale and may therefore change date order and number formatting.
  • Added a warning to the Android AppLanguage enum that future languages require an audit of default-locale parsers and formatters, particularly persistent day keys and languages using different numeral or calendar systems.

@ryanbr

ryanbr commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Heads-up: this went CONFLICTING when #1029 (German onboarding localization) landed — both touch the same six Android strings.xml files.

The resolution is provably a union, so it should be quick: I diffed the three sides and zero keys were edited on both. Base 1747, your branch adds 10, main added 58, no overlap. Same in values-de. So taking both sets of additions is exact, not a judgement call.

One thing worth knowing when you rebase: main's catalogues are not key-sorted. Sorting them on the way through reorders every entry into an unreadable diff — append instead.

The review items from before still stand (Chinese missing from both language enums, and the hardcoded .padding(.top, 6)), so this needs a pass anyway.

@lukezielke
lukezielke force-pushed the fix/language-mixing branch from b95bad2 to 9b847d3 Compare August 6, 2026 13:28
@lukezielke

Copy link
Copy Markdown
Author

Done @ryanbr

ryanbr pushed a commit that referenced this pull request Aug 9, 2026
In-app UI-language selector on Apple + Android (System / English / Deutsch /
Español / Français / Português / 中文), applied consistently to localized copy
and locale-sensitive date/number formatting. Units and time zone stay
independently configured.

- Apple: writes the standard `AppleLanguages` bundle override and takes effect
  after reopening NOOP; `AppLanguage.activeLocale` follows the running bundle
  (not the pending picker) and combines the app language with the device region,
  fed to SwiftUI format styles via `.environment(\.locale,)` at both app roots.
  Formatters across the day/sleep/workout screens switch from `Locale.current`
  to `AppLanguage.activeLocale`.
- Android: `AppLanguagePrefs.wrap` sets the process locale in both
  `Application` and `Activity` `attachBaseContext`; the picker recreates the
  Activity so composable + non-composable resource lookups switch together.
- Full de/es/fr/pt-PT/zh translations for the new settings strings on both
  platforms (iOS xcstrings carries all 8 locales); extracts several previously
  hardcoded Android appearance labels into resources.

Rebased onto current main (theming/accent/HSV-picker merges): the Settings
appearance card now shows Language above Preset/Theme on both platforms.

Day-key hardening (parity + #1004 safety): pin `Locale.ROOT` on the Kotlin
`AnalyticsEngine.isoDay` formatter and `AppleHealthImporter.civilFromDays`
`String.format`, so the primary stored `DailyMetric.day` writer stays ASCII
yyyy-MM-dd under any app-language selection. The Swift `AnalyticsEngine.isoDay`
already pins `en_US_POSIX`; this closes that Swift↔Kotlin gap rather than
introducing new behavior (all six shipped languages already emit Latin digits).
ryanbr added a commit that referenced this pull request Aug 9, 2026
In-app UI-language selector on Apple + Android (System / English / Deutsch /
Español / Français / Português / 中文), applied consistently to localized copy
and locale-sensitive date/number formatting. Units and time zone stay
independently configured.

- Apple: writes the standard `AppleLanguages` bundle override and takes effect
  after reopening NOOP; `AppLanguage.activeLocale` follows the running bundle
  (not the pending picker) and combines the app language with the device region,
  fed to SwiftUI format styles via `.environment(\.locale,)` at both app roots.
  Formatters across the day/sleep/workout screens switch from `Locale.current`
  to `AppLanguage.activeLocale`.
- Android: `AppLanguagePrefs.wrap` sets the process locale in both
  `Application` and `Activity` `attachBaseContext`; the picker recreates the
  Activity so composable + non-composable resource lookups switch together.
- Full de/es/fr/pt-PT/zh translations for the new settings strings on both
  platforms (iOS xcstrings carries all 8 locales); extracts several previously
  hardcoded Android appearance labels into resources.

Rebased onto current main (theming/accent/HSV-picker merges): the Settings
appearance card now shows Language above Preset/Theme on both platforms.

Day-key hardening (parity + #1004 safety): pin `Locale.ROOT` on the Kotlin
`AnalyticsEngine.isoDay` formatter and `AppleHealthImporter.civilFromDays`
`String.format`, so the primary stored `DailyMetric.day` writer stays ASCII
yyyy-MM-dd under any app-language selection. The Swift `AnalyticsEngine.isoDay`
already pins `en_US_POSIX`; this closes that Swift↔Kotlin gap rather than
introducing new behavior (all six shipped languages already emit Latin digits).

Co-authored-by: Luke Zielke <luke.bjarne@icloud.com>
@ryanbr

ryanbr commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Landed via #1181 — rebased onto current main (the appearance-card region had diverged after the theming/accent/HSV-picker merges), your feature content unchanged and authorship preserved on the commit. While landing it I added two small follow-ons: a Locale.ROOT pin on the Android day-key writers (matching what Swift's AnalyticsEngine.isoDay already does with en_US_POSIX — a parity fix the language selector made worth closing), and a fix to testExplicitLanguageWritesAndSystemRemovesAppleOverride (it read object(forKey:), which falls through to NSGlobalDomain where CI has the system language set — now reads the suite's own domain). Validated green on iOS + macOS app-build (incl. StrandTests) and the Android build+unit-tests. Thanks @lukezielke!

@ryanbr ryanbr closed this in #1181 Aug 9, 2026
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
…br#1181)

In-app UI-language selector on Apple + Android (System / English / Deutsch /
Español / Français / Português / 中文), applied consistently to localized copy
and locale-sensitive date/number formatting. Units and time zone stay
independently configured.

- Apple: writes the standard `AppleLanguages` bundle override and takes effect
  after reopening NOOP; `AppLanguage.activeLocale` follows the running bundle
  (not the pending picker) and combines the app language with the device region,
  fed to SwiftUI format styles via `.environment(\.locale,)` at both app roots.
  Formatters across the day/sleep/workout screens switch from `Locale.current`
  to `AppLanguage.activeLocale`.
- Android: `AppLanguagePrefs.wrap` sets the process locale in both
  `Application` and `Activity` `attachBaseContext`; the picker recreates the
  Activity so composable + non-composable resource lookups switch together.
- Full de/es/fr/pt-PT/zh translations for the new settings strings on both
  platforms (iOS xcstrings carries all 8 locales); extracts several previously
  hardcoded Android appearance labels into resources.

Rebased onto current main (theming/accent/HSV-picker merges): the Settings
appearance card now shows Language above Preset/Theme on both platforms.

Day-key hardening (parity + ryanbr#1004 safety): pin `Locale.ROOT` on the Kotlin
`AnalyticsEngine.isoDay` formatter and `AppleHealthImporter.civilFromDays`
`String.format`, so the primary stored `DailyMetric.day` writer stays ASCII
yyyy-MM-dd under any app-language selection. The Swift `AnalyticsEngine.isoDay`
already pins `en_US_POSIX`; this closes that Swift↔Kotlin gap rather than
introducing new behavior (all six shipped languages already emit Latin digits).

Co-authored-by: Luke Zielke <luke.bjarne@icloud.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants