Skip to content

feat(mobile): add i18n infrastructure and language settings - #2069

Merged
CodeWithCJ merged 14 commits into
CodeWithCJ:mainfrom
Dragonk:feat/mobile-i18n-infrastructure
Aug 16, 2026
Merged

feat(mobile): add i18n infrastructure and language settings#2069
CodeWithCJ merged 14 commits into
CodeWithCJ:mainfrom
Dragonk:feat/mobile-i18n-infrastructure

Conversation

@Dragonk

@Dragonk Dragonk commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Description

What problem does this PR solve?
The mobile app has no shared i18n infrastructure and no way to follow the system language or pick a manual one. This PR introduces the foundation: i18next/react-i18next, a splash-safe language bootstrap, native per-app language support (Android 13+ reads/writes the platform per-app language with two-way sync; iOS reads the OS-owned per-app Language and opens Settings for changes; Android ≤12 keeps the local fallback), and an audit that enforces an explicit English-fallback contract.

How did you implement the solution?

  • i18next 25 + react-i18next 16 with bundled en/pl resources (a deliberately minimal, representative key set — not a full translation).
  • src/localization/ exposes the app-language model (system/en/pl), device-locale resolution (pl-*pl, en-*en, unsupported → English), persistence through appPreferencesStore, and a one-time Android 13+ legacy-preference handoff marker (@SparkyFitness/app-language-migration).
  • Splash-safe bootstrap (useAppBootstrap): the effective locale is resolved before the first screen renders — no English flash followed by a jump to Polish.
  • Android App Languages: on Android 13+ (API 33+) a thin native bridge over the platform android.app.LocaleManager / applicationLocales reads/writes the system per-app language (system clears the override, en/pl apply the locale). On Android 12 and below no native locale API is called: the persisted in-app preference is authoritative, manual en/pl work through i18next and system follows the device locale via expo-localization. No AppCompat dependency, AppLocalesMetadataHolderService, or autoStoreLocales is used on any API level.
  • iOS: the operating system's per-app Language setting is authoritative. SparkyFitness reads the effective locale through expo-localization before the first screen renders and re-reads it during bootstrap/foreground reconciliation; it never maintains a competing explicit override. supportedLocales.ios: ['en','pl'] and the locales map generate the localized native metadata and permission strings (camera / HealthKit read / HealthKit write / local network); UIPrefersShowingLanguageSettings: true keeps the app-specific Language entry visible in iOS Settings even without multiple preferred system languages, and CFBundleAllowMixedLocalizations is enabled for the localized InfoPlist strings. The in-app row shows the effective language (endonym) and opens iOS Settings via Linking.openSettings(); a stale persisted explicit preference is normalized to system so it can never override the native value, and a failed openSettings leaves all language state unchanged. No private AppleLanguages/UserDefaults/App-Prefs: hacks are used — there is no public API to write the iOS per-app language, so the app reads it and opens Settings for changes.
  • Explicit English fallback contract: every user-facing t() passes a fallback string or defaultValue. pnpm run i18n:audit enforces this plus missing keys, placeholder/plural mismatches, duplicate keys, dynamic t(), and unsafe template-literal keys.

Linked Issue: Related to #1774 · Closes #1490

How to Test

  1. cd SparkyFitnessMobile && pnpm install && pnpm run i18n:audit → expect user-facing t() without English fallback: 0, dynamic t() keys: 0, all structural counts 0, exit 0.
  2. npx jest --runInBand __tests__/localization __tests__/hooks/useAppLanguageForegroundSync.test.tsx __tests__/screens/AppSettingsScreen.test.tsx __tests__/config8 suites / 81 tests pass (focused native-language regression set); bootstrap/startup/store set (useAppBootstrap, useAppStartup, appPreferencesStore) → 3 suites / 23 tests pass. Full mobile Jest suite on the final HEAD fb430c63: 291 suites / 4861 tests, 0 failed; pnpm run typecheck, pnpm run lint, pnpm run i18n:audit and git diff --check clean, expo config --type public generation verified, all i18n:audit blockers 0
  3. Build and run on Android; open Settings → App Settings → Language and switch System / English / Polski. Restart the app and confirm the choice persists and the representative strings (Language row, App Settings shell, header Save) switch immediately. In a Polish session, the primary header action's accessibility label reads Zapisz — the same localized text as the visible button.

PR Type

  • Issue (bug fix)
  • New Feature
  • Refactor
  • Documentation

Checklist

All PRs:

  • [MANDATORY - ALL] Integrity & License: I certify this is my own work, free of malicious code, and I agree to the License terms.

New features only:

Mobile changes (SparkyFitnessMobile/):

  • [MANDATORY for Mobile changes] Tested on device or emulator: (Not checked: no device/emulator pass on the final commit yet. a release APK + AAB build is generated by the fork's android.yml workflow on this branch (fb430c63); device validation remains pending on the final artifact.)

Screenshots

Click to expand

The UI diff is limited to the language row in App Settings (BottomSheetPicker with System / English / Polski), the App Settings shell header, and the header Save fallback labels — all of which resolve through the audited fallback contract and are covered by the targeted Jest suites.
Screenshot_2026-08-10-17-38-20-249_org SparkyApps SparkyFitnessMobile1 dev
Screenshot_2026-08-10-17-38-03-623_org SparkyApps SparkyFitnessMobile1 dev
Screenshot_2026-08-10-17-38-11-671_org SparkyApps SparkyFitnessMobile1 dev
Screenshot_2026-08-10-17-37-54-079_org SparkyApps SparkyFitnessMobile1 dev

Notes for Reviewers

  • Scope: this is an infrastructure PR. Only ~10 representative strings are localized (Language / System / English / Polski, App Settings + Settings shell, Save / Saving…, and the language subtitle). The full string migration, calendar/date locale propagation, medications.types.*, widgets, Live Activity localization and Weblate/translation-repo sync are explicitly not included here (PR5 / later).
  • Hardcoded UI strings: i18n:audit reports them as informational only (currently 2358 against this branch) and never blocks — the full hardcoded-UI inventory and its mass migration belong to PR5, where a fresh baseline can be captured if still needed.
  • Accessibility: the default primary header action mirrors its localized visible label (Zapisz in Polish); an explicit caller accessibilityLabel still wins.
  • Android App Languages: the system per-app-language entry depends on the platform/ROM. If a device does not expose it, the in-app selector remains the fully supported mechanism.
  • Android build: expo prebuild --clean --platform android was run twice and is idempotent (the Kotlin bridge sources and AppLanguagePackage registration are generated exactly once; no AppCompat dependency, AppLocalesMetadataHolderService, or autoStoreLocales is generated). A release APK + AAB build is generated by the fork's android.yml (Build Android APK) workflow on this branch (fb430c63): run 31937551546 / 31937533353. Upstream CI on the final HEAD (fb430c63): Mobile Tests, Server Tests, Detect Changes, Validate & Label, GitGuardian and CodeRabbit all pass; the only remaining gate is the pr-validation bot's mandatory device/emulator checkbox, which stays [ ] until a real device pass on this APK.
  • iOS: native-authoritative per the OS contract — the app reads the effective locale and opens iOS Settings for changes; there is no public programmatic setter and no private language API. supportedLocales.ios is intentional and remains. Runtime/Xcode validation is still pending (no Mac available here); config generation, locale declarations and the TS lifecycle are covered by tests.
  • Follow-ups: PR4 = native surfaces (WidgetKit / Live Activity localization); PR5 = remaining strings + Weblate component (mobile/en/translation.json, mobile/pl/translation.json in the translations repo).

Summary by CodeRabbit

  • New Features

    • Added English and Polish language support.
    • Added a language picker with System, English, and Polish options.
    • Preferences sync with device settings, including Android per-app language support.
    • Added localized settings, notifications, navigation labels, Save/Saving states, accessibility hints, and permission descriptions.
  • Bug Fixes

    • Improved startup language initialization and fallback behavior.
    • Language changes update visible labels without resetting the current screen.
    • Added clearer error feedback when language updates fail.
    • Improved startup routing when server configuration cannot be loaded.

Dragonk added 2 commits August 7, 2026 19:44
Introduce the shared localization foundation for the mobile app:

- i18next/react-i18next instance with bundled en/pl resources and an
  explicit English-fallback contract (every user-facing t() call must
  pass a fallback string or defaultValue).
- App language model (system/en/pl) with persisted preference in
  appPreferencesStore, device-locale resolution (pl-* -> pl, en-* -> en,
  unsupported -> en) and splash-safe bootstrap that resolves the
  effective locale before the first screen renders.
- One-time AppCompat migration marker for the later native App Language
  sync (@SparkyFitness/app-language-migration).
- useScreenHeader primary Save labels fall back to localized
  common.save/common.saving with English fallbacks.
- Representative language settings UI in AppSettingsScreen plus shell
  strings (Settings / App Settings) in App.tsx.
- i18n:audit tooling (missing keys, duplicate keys, placeholder and
  plural mismatches, dynamic t(), hardcoded UI text baseline, and the
  new missing-English-fallback rule) with a fresh baseline captured from
  current main + this PR (zero new findings).
- Jest mocks for expo-localization / expo-winter and localization,
  bootstrap, foreground-sync, audit and native-config test suites.

Assisted-by: Open WebUI
Wire the native app-language surface for Android App Languages and iOS
InfoPlist localization:

- Expo config plugin (withAppLanguage) that adds the AppCompat
  dependency, registers AppLocalesMetadataHolderService with
  autoStoreLocales, installs the AppLanguage native module, and copies
  the Kotlin sources into the generated project (idempotent).
- Native AppLanguageModule over AppCompatDelegate.setApplicationLocales:
  system clears the override, en/pl apply the locale, effective language
  is read from the configuration.
- expo-localization config plugin declares supportedLocales (en/pl) for
  Android/iOS; app.config.ts wires the plugin and the iOS locales map.
- Minimal en/pl native permission strings for iOS InfoPlist (camera,
  HealthKit read/write, local network).
- Foreground reconciliation contract is implemented in the JS layer
  (useAppLanguageForegroundSync + syncAppLanguageFromSystem); this
  commit adds its config/native test coverage.

Assisted-by: Open WebUI
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The mobile app adds English and Polish localization, manual language selection, Android native language integration, localized startup and navigation behavior, and an automated i18n audit with validation coverage.

Changes

Mobile localization

Layer / File(s) Summary
Android language integration
SparkyFitnessMobile/plugins/withAppLanguage.ts, SparkyFitnessMobile/targets/android-language/..., SparkyFitnessMobile/app.config.ts, SparkyFitnessMobile/locales/*, SparkyFitnessMobile/__tests__/config/*
The Expo plugin installs the Android language package. Native locale handling exposes configured and effective languages. App configuration includes English and Polish locale resources.
Language state and translation runtime
SparkyFitnessMobile/src/localization/*, SparkyFitnessMobile/src/services/appLanguageNative.ts, SparkyFitnessMobile/src/stores/appPreferencesStore.ts, SparkyFitnessMobile/__tests__/localization/*, SparkyFitnessMobile/__tests__/stores/*
The app persists language preferences, synchronizes native locales, initializes i18next, and provides English and Polish translations.
Startup, foreground sync, and localized UI
SparkyFitnessMobile/App.tsx, SparkyFitnessMobile/src/hooks/*, SparkyFitnessMobile/src/screens/AppSettingsScreen.tsx, SparkyFitnessMobile/src/components/BottomSheetPicker.tsx, related tests
Startup initializes language before route selection. Foreground changes resynchronize language. Settings expose a language picker. Headers use localized Save labels and accessibility text.
Localization audit and validation
SparkyFitnessMobile/scripts/*, SparkyFitnessMobile/__tests__/scripts/i18nAudit.test.ts, SparkyFitnessMobile/package.json
The audit checks locale structure, translation keys, fallbacks, plural forms, hardcoded UI text, suppressions, and source-scan failures. The validation workflow runs the audit.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: codewithcj

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.96% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements manual language selection and Android App Languages support requested by issue #1490.
Out of Scope Changes check ✅ Passed The changes support the language-selection feature and its required infrastructure; deferred work is explicitly excluded.
Title check ✅ Passed The title clearly and concisely describes the main change: adding mobile i18n infrastructure and language settings.
Description check ✅ Passed The description includes the required sections, implementation details, testing steps, issue links, screenshots, checklist, and clearly documents pending device validation.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added enhancement New feature or request mobile labels Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

PR Validation Results

Change Detection

  • 📱 Mobile changes detected

⚠️ Recommendations (1)

  • Please link a related GitHub issue (Linked Issue: Closes #123).

✅ All required checks passed.

Dragonk added 4 commits August 7, 2026 19:52
AppSettingsScreen now renders the Language row with the explicit English
fallback copy, exercising the in-app selector surface that Closes CodeWithCJ#1490.

Assisted-by: Open WebUI
Remove unused variables in the audit test and use the project's T[] array
syntax in the withAppLanguage manifest types so the mobile lint gate
passes with --max-warnings 0.

Assisted-by: Open WebUI
Correction round requested in review of the i18n infrastructure PR:

- Remove the 24.6k-line hardcoded-UI baseline snapshot from the audit.
  Hardcoded English strings are now reported as informational only and
  never block i18n:audit (full inventory/migration moves to PR5). The
  PR drops from ~29.5k insertions to ~4.5k and becomes reviewable.
- Keep every blocking rule intact: missing English fallback, dynamic
  t(), unsafe template-literal keys, missing locale keys, EN/PL
  structure mismatch, placeholder mismatch, plural mismatch, duplicate
  keys, forbidden legacy files, and invalid suppressions.
- useScreenHeader: the default accessibility label for kind:'primary'
  now mirrors the resolved, localized visible label instead of the
  hard-coded English SAVE_LABEL (explicit caller accessibilityLabel
  still wins). Native busy path mirrors the busy label too.
- Add useScreenHeader regression tests: EN Save/Save, PL
  Zapisz/Zapisz, busy disabled states, explicit accessibilityLabel
  override, and the native-path busy contract.

Assisted-by: Open WebUI
CI lint (no-unused-vars) flags the unused useAppPreferencesStore import
in the new useScreenHeader regression suite; the store reset helper is
the only thing used there.

Assisted-by: Open WebUI
@Dragonk
Dragonk marked this pull request as ready for review August 7, 2026 19:24

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (13)
SparkyFitnessMobile/scripts/i18n-audit/core.cjs (1)

10-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive both forbidden lists from one source.

FORBIDDEN_FILES and the inline default inside checkForbiddenFiles list the same three paths. The two lists can diverge when a new forbidden file is added. Define the relative names once and build both lists from them.

♻️ Proposed refactor
+const FORBIDDEN_RELATIVE_FILES = [
+  ['src', 'localization', 'mobile.pl.json'],
+  ['src', 'localization', 'mobile.pl.overrides.json'],
+  ['scripts', 'populate-mobile-polish.mjs'],
+];
+
+function defaultForbiddenFiles(rootDir) {
+  return FORBIDDEN_RELATIVE_FILES.map((parts) => path.join(rootDir, ...parts));
+}
+
-const FORBIDDEN_FILES = [
-  path.join(MOBILE_ROOT, 'src', 'localization', 'mobile.pl.json'),
-  path.join(MOBILE_ROOT, 'src', 'localization', 'mobile.pl.overrides.json'),
-  path.join(MOBILE_ROOT, 'scripts', 'populate-mobile-polish.mjs'),
-];
+const FORBIDDEN_FILES = defaultForbiddenFiles(MOBILE_ROOT);
 
 const SOURCE_ROOTS = [path.join(MOBILE_ROOT, 'src')];
 
 function checkForbiddenFiles(rootDir, forbiddenFiles) {
   const errors = [];
   const files = forbiddenFiles && forbiddenFiles.length > 0
     ? forbiddenFiles
-    : [
-    path.join(rootDir, 'src', 'localization', 'mobile.pl.json'),
-    path.join(rootDir, 'src', 'localization', 'mobile.pl.overrides.json'),
-    path.join(rootDir, 'scripts', 'populate-mobile-polish.mjs'),
-  ];
+    : defaultForbiddenFiles(rootDir);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SparkyFitnessMobile/scripts/i18n-audit/core.cjs` around lines 10 - 26, Define
the three forbidden file paths once as relative names, then derive both the
module-level FORBIDDEN_FILES and checkForbiddenFiles’s fallback list from that
shared source using the applicable root directory. Remove the duplicated inline
path list while preserving the existing custom forbiddenFiles override behavior.
SparkyFitnessMobile/scripts/audit-mobile-i18n.mjs (1)

7-9: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Report the output path when --json is absent.

If a user passes an output path without --json, the script prints the human report and ignores the path. No message explains this. Consider writing the human report to the file, or printing a warning.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SparkyFitnessMobile/scripts/audit-mobile-i18n.mjs` around lines 7 - 9, Update
the argument handling and reporting flow in the audit script so a provided
outputFile is not silently ignored when showJson is false. Either write the
human-readable report to outputFile or emit a clear warning explaining that the
path requires JSON output, while preserving the existing JSON flag behavior.
SparkyFitnessMobile/scripts/i18n-audit/sourceScanner.cjs (1)

158-161: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove or use the unused scanner exports.

KNOWN_ICONS is exported but no code reads it; isLikelyFalsePositive does not consult it, so icon names pass only through the other heuristics. getSuppressionWithoutJustificationFindings is defined but neither exported nor called. Remove both, or wire them into the audit.

Also applies to: 514-518

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SparkyFitnessMobile/scripts/i18n-audit/sourceScanner.cjs` around lines 158 -
161, Remove the unused KNOWN_ICONS export and the unreferenced
getSuppressionWithoutJustificationFindings function from the scanner, unless
they are intentionally integrated into the audit flow. If retained, update
isLikelyFalsePositive to consult KNOWN_ICONS and invoke/export
getSuppressionWithoutJustificationFindings where appropriate.
SparkyFitnessMobile/scripts/i18n-audit/localeValidator.cjs (2)

100-131: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused helper and the unused parameter.

pluralFormsFor is never called and is not exported. detectSingularPluralCollision never reads its data parameter; the caller at line 196 still passes enData/plData. Remove both to keep the audit module minimal.

♻️ Proposed refactor
-/**
- * Returns an array of plural-form suffixes (e.g. '_one', '_other') used in a
- * locale for the given base key, when the key is a plural group.
- */
-function pluralFormsFor(keys, base) {
-  const forms = [];
-  for (const key of keys) {
-    const kBase = getPluralBase(key);
-    if (kBase === base) {
-      forms.push(key.slice(base.length));
-    }
-  }
-  return forms;
-}
-
 /**
  * Detects a plain (singular) key sharing its base with a plural group in the
  * same locale, e.g. both `item` and `item_one`/`item_other`. This is ambiguous
  * for i18next lookups and is a structural error that cannot be suppressed.
  */
-function detectSingularPluralCollision(data, groups, localeName) {
+function detectSingularPluralCollision(groups, localeName) {

Update the call site at lines 192-200 accordingly:

     for (const localeName of ['en', 'pl']) {
       const isEn = localeName === 'en';
       const groups = isEn ? enGroups : plGroups;
-      const data = isEn ? enData : plData;
-      const collisionErrors = detectSingularPluralCollision(data, groups, localeName);
+      const collisionErrors = detectSingularPluralCollision(groups, localeName);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SparkyFitnessMobile/scripts/i18n-audit/localeValidator.cjs` around lines 100
- 131, Remove the unused pluralFormsFor helper, remove the unused data parameter
from detectSingularPluralCollision, and update every call site to pass only the
arguments the function reads, including the caller currently passing
enData/plData.

366-378: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse samePlaceholderMultiset in the singular branches.

Both blocks re-implement the length-plus-index comparison that samePlaceholderMultiset already provides. Call the helper to keep one comparison rule.

♻️ Proposed refactor
-                  if (enPlaceholders.length !== plPlaceholders.length ||
-                      !enPlaceholders.every((p, i2) => p === plPlaceholders[i2])) {
+                  if (!samePlaceholderMultiset(enPlaceholders, plPlaceholders)) {
-          if (enPlaceholders.length !== plPlaceholders.length ||
-              !enPlaceholders.every((p, i) => p === plPlaceholders[i])) {
+          if (!samePlaceholderMultiset(enPlaceholders, plPlaceholders)) {

Also applies to: 391-404

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SparkyFitnessMobile/scripts/i18n-audit/localeValidator.cjs` around lines 366
- 378, Update the singular placeholder-mismatch checks in both branches around
the visible comparison and the corresponding block near the alternate branch to
call samePlaceholderMultiset instead of manually comparing lengths and indexed
placeholder values. Preserve the existing error objects, keys, and messages.
SparkyFitnessMobile/__tests__/scripts/i18nAudit.test.ts (2)

22-65: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

createFixtureStructure and cleanupFixture do not need async.

Both functions use only synchronous fs calls and contain no await. Making them synchronous removes the await at every call site and shortens the tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SparkyFitnessMobile/__tests__/scripts/i18nAudit.test.ts` around lines 22 -
65, Remove the async modifier from createFixtureStructure and cleanupFixture,
since both use only synchronous filesystem operations. Update every call site to
remove the corresponding await while preserving fixture setup and cleanup
behavior.

723-750: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The forbidden-file tests depend on an ambiguous empty-array fallback.

auditRun passes forbiddenFiles: []. checkForbiddenFiles in SparkyFitnessMobile/scripts/i18n-audit/core.cjs at line 20 treats an empty array as "not provided" and falls back to the default list built from rootDir. These two tests therefore pass only because of that fallback, and no test can express "check no forbidden files". Pass the explicit fixture paths in these tests so the intent is clear.

♻️ Proposed change for the first test
-    const result = auditRun(tmpDir);
+    const result = auditRun(tmpDir, {
+      forbiddenFiles: [path.join(tmpDir, 'src', 'localization', 'mobile.pl.json')],
+    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SparkyFitnessMobile/__tests__/scripts/i18nAudit.test.ts` around lines 723 -
750, Update both tests in the “Forbidden files” suite to pass the relevant
fixture path explicitly to auditRun/checkForbiddenFiles instead of relying on
the empty-array fallback: use the created mobile.pl.json path in the first test
and populate-mobile-polish.mjs path in the second. Preserve the existing
assertions that each file is reported as forbidden.
SparkyFitnessMobile/src/localization/appLanguage.ts (1)

23-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse a single preference normalizer.

i18n.ts lines 46-51 define the same logic. Export it from i18n.ts and import it here to keep one definition.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SparkyFitnessMobile/src/localization/appLanguage.ts` around lines 23 - 25,
Remove the duplicate normalizePreference implementation from appLanguage.ts,
export the existing normalizer from i18n.ts, and import and reuse it in the app
language flow while preserving the current fallback to 'system'.
SparkyFitnessMobile/__tests__/localization/i18n.test.ts (1)

184-194: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Tighten the English-branch assertion.

Line 189 uses .not.toContain('missing.key'). That passes for many wrong return values. The Polish branch at Line 192 already uses an exact match. Use the same exact assertion for the English branch.

💚 Proposed change
-      expect(i18n.t('missing.key.with.fallback', 'Readable text')).not.toContain('missing.key');
+      expect(i18n.t('missing.key.with.fallback', 'Readable text')).toBe('Readable text');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SparkyFitnessMobile/__tests__/localization/i18n.test.ts` around lines 184 -
194, Update the English assertion in the `never leaks a raw translation key into
the UI` test to require an exact `Readable text` result from `i18n.t`, matching
the existing Polish assertion, rather than only checking that the value does not
contain `missing.key`.
SparkyFitnessMobile/__tests__/localization/appLanguage.test.ts (1)

111-177: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for a rejected native read after migration.

The suite covers a rejected setApplicationLanguage during migration at Lines 99-108. It does not cover a rejected getApplicationLanguage on the post-migration adopt path. That path currently has no error handling in adoptNativeState; see the comment on src/localization/appLanguage.ts Lines 122-133.

💚 Proposed test
+    it('keeps startup usable when the native read fails', async () => {
+      useAppPreferencesStore.setState({ languagePreference: 'pl' });
+      mockNative.getApplicationLanguage.mockRejectedValue(new Error('bridge failure'));
+
+      await expect(initializeAppLanguage()).resolves.toBe('pl');
+      expect(i18n.resolvedLanguage).toBe('pl');
+    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SparkyFitnessMobile/__tests__/localization/appLanguage.test.ts` around lines
111 - 177, Add a post-migration test in the “bootstrap after migration” suite
that makes mockNative.getApplicationLanguage reject, calls
initializeAppLanguage, and verifies the rejection is handled without failing
initialization while preserving the store and i18n language state. Anchor the
test to the existing initializeAppLanguage flow and use the established
rejected-native-operation expectations from the migration tests.
SparkyFitnessMobile/src/localization/i18n.ts (2)

117-126: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Narrow the exported language-change surface.

applyLanguagePreference changes i18next without updating the preferences store or AppCompat. setAppLanguagePreference in appLanguage.ts is the intended entry point and keeps all three in sync. A future caller that picks this function desynchronizes them.

Mark this function as internal, or remove the export and keep applyEffectiveLanguage in appLanguage.ts as the only applier.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SparkyFitnessMobile/src/localization/i18n.ts` around lines 117 - 126,
Restrict the public language-change API around applyLanguagePreference so
callers use setAppLanguagePreference, which synchronizes i18n, preferences, and
AppCompat. Either remove its export while retaining internal use, or mark it
internal, and preserve applyEffectiveLanguage as the only externally accessible
direct language applier.

13-13: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Import the persist key instead of duplicating it.

STORE_KEY is duplicated in appPreferencesStore.ts and i18n.ts. Export it from appPreferencesStore.ts and import it here so both modules use the same AsyncStorage key.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SparkyFitnessMobile/src/localization/i18n.ts` at line 13, Update STORE_KEY
ownership in appPreferencesStore.ts by exporting the existing constant, then
remove the duplicate declaration in i18n.ts and import the shared symbol there
so both modules use the same AsyncStorage key.
SparkyFitnessMobile/__tests__/config/withAppLanguage.test.ts (1)

75-88: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert meta-data after the second call.

The idempotency test checks only service length on the second call. The plugin's existing-service branch replaces $ but does not rewrite meta-data. A future regression that drops autoStoreLocales on re-application would still pass.

♻️ Proposed additional assertion
     expect(twice?.service).toHaveLength(1);
+    expect(twice?.service?.[0]?.['meta-data']?.[0]?.$?.['android:name']).toBe('autoStoreLocales');
+    expect(twice?.service?.[0]?.['meta-data']?.[0]?.$?.['android:value']).toBe('true');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SparkyFitnessMobile/__tests__/config/withAppLanguage.test.ts` around lines 75
- 88, Extend the idempotency test around addAppLocalesService to assert that the
second result, twice, still contains the APP_LOCALES_SERVICE metadata with
android:name "autoStoreLocales" and android:value "true". Keep the existing
first-call assertions and service-length check unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@SparkyFitnessMobile/__tests__/scripts/i18nAudit.test.ts`:
- Around line 8-16: Replace all any annotations in i18nAudit.test.ts with
explicit local audit types: define interfaces for AuditFinding, AuditError,
AuditResult, and AuditOptions, type the required LocaleValidator,
groupPluralKeys, collectFindings, and runAudit symbols accordingly, and change
assertion callback parameters to AuditError or AuditFinding plus extra to
Partial<AuditOptions>; do not suppress the no-explicit-any rule.

In `@SparkyFitnessMobile/AGENTS.md`:
- Line 67: Update the startup ownership bullet in AGENTS.md to state that
App.tsx’s useAppBootstrap owns language initialization, initial-route selection,
linking state, and splash hiding, replacing the outdated
useInitialRoute/useAppStartup guidance.

In `@SparkyFitnessMobile/scripts/i18n-audit/sourceScanner.cjs`:
- Around line 345-367: Remove the redundant ts.isJsxElement check from the outer
guard in the JSX scanning block, relying on isTextLikeElement(node) for element
validation. In the child-expression branch, treat child itself as the
JsxExpression node, validate it with ts.isJsxExpression(child), and pass
child.expression to literalText so expressions such as <Text>{'Hardcoded
English'}</Text> are recorded. Add or update a fixture covering this
expression-child form.

In `@SparkyFitnessMobile/src/hooks/useAppBootstrap.ts`:
- Around line 39-42: Remove the early return from the finally block in the
bootstrap flow around SplashScreen.hideAsync. Preserve the cancelled condition
while allowing finally to complete normally, and keep the resulting TypeScript
strict and Biome-compliant.

In `@SparkyFitnessMobile/src/hooks/useScreenHeader.tsx`:
- Around line 615-616: Update the native left-item signature near
resolveItemLabel and resolveItemBusyLabel to include the localized label value
for left text/primary items, matching the right-item signature behavior so
language changes rebuild the native item. Add a native-path test covering a
localized left text or primary item and verifying it updates after the locale
changes.

In `@SparkyFitnessMobile/src/localization/appLanguage.ts`:
- Around line 122-133: The native language read in adoptNativeState must handle
rejection by catching getApplicationLanguage failures and falling back to the
stored preference, matching runMigration’s resilience; update
SparkyFitnessMobile/src/localization/appLanguage.ts lines 122-133. Make no
direct change to SparkyFitnessMobile/src/localization/appLanguage.ts lines
201-214; confirm it is covered once adoptNativeState catches errors. Add a test
in SparkyFitnessMobile/__tests__/localization/appLanguage.test.ts lines 111-177
that rejects getApplicationLanguage after the migration marker exists and
verifies initializeAppLanguage resolves with the stored preference.

In `@SparkyFitnessMobile/src/localization/i18n.ts`:
- Around line 97-115: Update the failure handling in initializeI18n so that
after the primary and English fallback initialization both fail, it clears
initPromise when i18n.isInitialized remains false. Preserve the existing error
logging and fallback behavior, while allowing subsequent initializeI18n calls to
start a fresh initialization attempt.

In `@SparkyFitnessMobile/src/screens/AppSettingsScreen.tsx`:
- Around line 94-100: Update BottomSheetPicker to accept an accessibilityHint
prop and use it instead of the hardcoded default when provided. In
AppSettingsScreen’s language picker, pass the localized settings translation for
the hint, and add or update the relevant test to verify the Polish locale
announces the Polish hint.

---

Nitpick comments:
In `@SparkyFitnessMobile/__tests__/config/withAppLanguage.test.ts`:
- Around line 75-88: Extend the idempotency test around addAppLocalesService to
assert that the second result, twice, still contains the APP_LOCALES_SERVICE
metadata with android:name "autoStoreLocales" and android:value "true". Keep the
existing first-call assertions and service-length check unchanged.

In `@SparkyFitnessMobile/__tests__/localization/appLanguage.test.ts`:
- Around line 111-177: Add a post-migration test in the “bootstrap after
migration” suite that makes mockNative.getApplicationLanguage reject, calls
initializeAppLanguage, and verifies the rejection is handled without failing
initialization while preserving the store and i18n language state. Anchor the
test to the existing initializeAppLanguage flow and use the established
rejected-native-operation expectations from the migration tests.

In `@SparkyFitnessMobile/__tests__/localization/i18n.test.ts`:
- Around line 184-194: Update the English assertion in the `never leaks a raw
translation key into the UI` test to require an exact `Readable text` result
from `i18n.t`, matching the existing Polish assertion, rather than only checking
that the value does not contain `missing.key`.

In `@SparkyFitnessMobile/__tests__/scripts/i18nAudit.test.ts`:
- Around line 22-65: Remove the async modifier from createFixtureStructure and
cleanupFixture, since both use only synchronous filesystem operations. Update
every call site to remove the corresponding await while preserving fixture setup
and cleanup behavior.
- Around line 723-750: Update both tests in the “Forbidden files” suite to pass
the relevant fixture path explicitly to auditRun/checkForbiddenFiles instead of
relying on the empty-array fallback: use the created mobile.pl.json path in the
first test and populate-mobile-polish.mjs path in the second. Preserve the
existing assertions that each file is reported as forbidden.

In `@SparkyFitnessMobile/scripts/audit-mobile-i18n.mjs`:
- Around line 7-9: Update the argument handling and reporting flow in the audit
script so a provided outputFile is not silently ignored when showJson is false.
Either write the human-readable report to outputFile or emit a clear warning
explaining that the path requires JSON output, while preserving the existing
JSON flag behavior.

In `@SparkyFitnessMobile/scripts/i18n-audit/core.cjs`:
- Around line 10-26: Define the three forbidden file paths once as relative
names, then derive both the module-level FORBIDDEN_FILES and
checkForbiddenFiles’s fallback list from that shared source using the applicable
root directory. Remove the duplicated inline path list while preserving the
existing custom forbiddenFiles override behavior.

In `@SparkyFitnessMobile/scripts/i18n-audit/localeValidator.cjs`:
- Around line 100-131: Remove the unused pluralFormsFor helper, remove the
unused data parameter from detectSingularPluralCollision, and update every call
site to pass only the arguments the function reads, including the caller
currently passing enData/plData.
- Around line 366-378: Update the singular placeholder-mismatch checks in both
branches around the visible comparison and the corresponding block near the
alternate branch to call samePlaceholderMultiset instead of manually comparing
lengths and indexed placeholder values. Preserve the existing error objects,
keys, and messages.

In `@SparkyFitnessMobile/scripts/i18n-audit/sourceScanner.cjs`:
- Around line 158-161: Remove the unused KNOWN_ICONS export and the unreferenced
getSuppressionWithoutJustificationFindings function from the scanner, unless
they are intentionally integrated into the audit flow. If retained, update
isLikelyFalsePositive to consult KNOWN_ICONS and invoke/export
getSuppressionWithoutJustificationFindings where appropriate.

In `@SparkyFitnessMobile/src/localization/appLanguage.ts`:
- Around line 23-25: Remove the duplicate normalizePreference implementation
from appLanguage.ts, export the existing normalizer from i18n.ts, and import and
reuse it in the app language flow while preserving the current fallback to
'system'.

In `@SparkyFitnessMobile/src/localization/i18n.ts`:
- Around line 117-126: Restrict the public language-change API around
applyLanguagePreference so callers use setAppLanguagePreference, which
synchronizes i18n, preferences, and AppCompat. Either remove its export while
retaining internal use, or mark it internal, and preserve applyEffectiveLanguage
as the only externally accessible direct language applier.
- Line 13: Update STORE_KEY ownership in appPreferencesStore.ts by exporting the
existing constant, then remove the duplicate declaration in i18n.ts and import
the shared symbol there so both modules use the same AsyncStorage key.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c12c91ae-7754-4f58-9922-3b63e3ea418a

📥 Commits

Reviewing files that changed from the base of the PR and between c0929fb and 7cd6575.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (36)
  • SparkyFitnessMobile/AGENTS.md
  • SparkyFitnessMobile/App.tsx
  • SparkyFitnessMobile/__mocks__/expo-winter.js
  • SparkyFitnessMobile/__tests__/config/nativeLocales.test.ts
  • SparkyFitnessMobile/__tests__/config/withAppLanguage.test.ts
  • SparkyFitnessMobile/__tests__/hooks/useAppBootstrap.test.tsx
  • SparkyFitnessMobile/__tests__/hooks/useAppLanguageForegroundSync.test.tsx
  • SparkyFitnessMobile/__tests__/hooks/useScreenHeader.test.tsx
  • SparkyFitnessMobile/__tests__/localization/appLanguage.test.ts
  • SparkyFitnessMobile/__tests__/localization/i18n.test.ts
  • SparkyFitnessMobile/__tests__/screens/AppSettingsScreen.test.tsx
  • SparkyFitnessMobile/__tests__/scripts/i18nAudit.test.ts
  • SparkyFitnessMobile/__tests__/stores/appPreferencesStore.test.ts
  • SparkyFitnessMobile/app.config.ts
  • SparkyFitnessMobile/jest.setup.js
  • SparkyFitnessMobile/locales/en.json
  • SparkyFitnessMobile/locales/pl.json
  • SparkyFitnessMobile/package.json
  • SparkyFitnessMobile/plugins/withAppLanguage.ts
  • SparkyFitnessMobile/scripts/audit-mobile-i18n.mjs
  • SparkyFitnessMobile/scripts/i18n-audit/core.cjs
  • SparkyFitnessMobile/scripts/i18n-audit/localeValidator.cjs
  • SparkyFitnessMobile/scripts/i18n-audit/sourceScanner.cjs
  • SparkyFitnessMobile/src/hooks/useAppBootstrap.ts
  • SparkyFitnessMobile/src/hooks/useAppLanguageForegroundSync.ts
  • SparkyFitnessMobile/src/hooks/useScreenHeader.tsx
  • SparkyFitnessMobile/src/localization/appLanguage.ts
  • SparkyFitnessMobile/src/localization/i18n.ts
  • SparkyFitnessMobile/src/localization/index.ts
  • SparkyFitnessMobile/src/localization/locales/en/translation.json
  • SparkyFitnessMobile/src/localization/locales/pl/translation.json
  • SparkyFitnessMobile/src/screens/AppSettingsScreen.tsx
  • SparkyFitnessMobile/src/services/appLanguageNative.ts
  • SparkyFitnessMobile/src/stores/appPreferencesStore.ts
  • SparkyFitnessMobile/targets/android-language/kotlin/com/sparkyapps/sparkyfitness/language/AppLanguageModule.kt
  • SparkyFitnessMobile/targets/android-language/kotlin/com/sparkyapps/sparkyfitness/language/AppLanguagePackage.kt

Comment thread SparkyFitnessMobile/__tests__/scripts/i18nAudit.test.ts Outdated
Comment thread SparkyFitnessMobile/AGENTS.md
Comment thread SparkyFitnessMobile/scripts/i18n-audit/sourceScanner.cjs Outdated
Comment thread SparkyFitnessMobile/src/hooks/useAppBootstrap.ts Outdated
Comment thread SparkyFitnessMobile/src/hooks/useScreenHeader.tsx
Comment thread SparkyFitnessMobile/src/localization/appLanguage.ts
Comment thread SparkyFitnessMobile/src/localization/i18n.ts
Comment thread SparkyFitnessMobile/src/screens/AppSettingsScreen.tsx Outdated

@apedley apedley left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Dead code?

applyLanguagePreference, formatLocalizedNumber, getAppLocale, resolveLanguagePreference, getNativeApplicationLanguage have no callers. I checked the other PR and nothing in there calls them either. Is there yet another PR or is this dead code? applyLanguagePreference is also the only caller of the no-arg initializeI18n() path, so the AsyncStorage-parsing branch at i18n.ts:81-91 is dead in production and duplicates the store's persist shape. Make initializeI18n take a required language and drop the helpers

Same in the audit scripts: buildFingerprint, buildMigrationFingerprint, KNOWN_ICONS, and getSuppressionWithoutJustificationFindings in sourceScanner.cjs have zero consumers and look like leftovers of the removed baseline feature. Also withFileExtensions: true at sourceScanner.cjs:470 isn't a real readdirSync option, and the ts.isJsxElement && on line 345 is a truthy function check. The forbidden-files rule in core.cjs guards files that have never existed in this repo, that can go too.

  • Language names are shown as endonyms not as english names.. so Polski instead Polish in the english ui

Comment thread SparkyFitnessMobile/src/localization/i18n.ts Outdated
Comment thread SparkyFitnessMobile/src/localization/i18n.ts Outdated
Comment thread SparkyFitnessMobile/src/hooks/useAppBootstrap.ts
Review round for the i18n infrastructure PR (apedley + CodeRabbit):

- initializeI18n(language) now requires the language; the dead
  AsyncStorage/Zustand-blob parsing branch and its STORE_KEY duplicate
  are removed. One owner for preference persistence
  (appPreferencesStore), one owner for native -> effective -> i18next
  (appLanguage.ts).
- Init failure is retryable: if both the requested language and the
  English fallback fail, initPromise is cleared so a later
  bootstrap/foreground operation retries instead of permanently
  poisoning the session with a resolved failed promise.
- Runtime errors use LogService (addLog ERROR/WARNING) instead of
  console.error/warn.
- useAppBootstrap: language init and server-config loading are
  independent failure domains. A rejected initializeAppLanguage() is
  logged and never changes the route; a config failure still lands on
  Onboarding. Splash hiding is the last step, failure is logged and
  never rejects the unobserved determine() promise.
- useInitialRoute removed from useAppStartup (dead after
  useAppBootstrap); AGENTS.md now documents App.tsx + useAppBootstrap
  as the owners of language init, initial route, linking state, and
  splash hiding.
- Native language reads are resilient: getApplicationLanguage and
  getEffectiveLanguage rejections fall back to the stored preference /
  expo-localization device locale and are logged.
- Foreground sync catches and logs resync rejections so an AppState
  callback can never leak an unhandled promise rejection.
- Endonym labels: English catalog renders "Polski" (not "Polish").
- Settings language selection handles native-set failure: the previous
  stored/effective language is preserved, the error is logged, and a
  minimal settings.language.changeFailed error toast is shown.

Assisted-by: Open WebUI
Android architecture correction per maintainer agreement (CodeWithCJ#1774):

- Android 13+ (API 33+) uses the platform per-app language API via
  android.app.LocaleManager / applicationLocales in the native
  AppLanguageModule (thin bridge, SDK_INT-guarded). TS exposes
  supportsNativePerAppLanguage (Android && API >= 33 && module exists)
  and never calls native set/get on Android <=12.
- Android 12 and below: no AppCompat locale APIs, no
  AppLocalesMetadataHolderService, no autoStoreLocales. The stored
  preference stays authoritative; manual en/pl work through i18next and
  `system` resolves through expo-localization.
- The migration marker now applies only where native per-app language
  support exists: a stored manual preference seeds the platform locale
  exactly once after a successful handoff, and a failed handoff leaves
  the marker unset so it retries next bootstrap. A user who picked
  Polish on Android 12 keeps it after upgrading to Android 13.
- Config plugin keeps only what is still needed: copying the Kotlin
  bridge sources and registering AppLanguagePackage in MainApplication
  (idempotent — verified with two consecutive `expo prebuild --clean
  --platform android` runs; no AppCompat dependency, service, or
  metadata is generated).
- BottomSheetPicker gains an optional accessibilityHint prop threaded
  into PickerTrigger; the language picker passes the localized
  settings.language.pickerHint (EN/PL resources added).
- useScreenHeader native left signature now carries the resolved
  localized label/busyLabel so a language change rebuilds the native
  left item; explicit caller accessibilityLabel still wins. Native-path
  tests cover EN/PL left primary items and busy labels.

Assisted-by: Open WebUI
- sourceScanner: fix the JSX guard (was a truthy function check) and
  the expression-child branch (child is the JsxExpression; pass
  child.expression to literalText) so <Text>{'…'}</Text> and
  <Text>{`…`}</Text> are inventoried like plain text. Fixtures cover
  all three forms.
- Audit fails closed on scan failures: a source file that cannot be
  read/parsed records a blocking `source-scan-error` (file + message)
  and runAudit returns hasErrors=true; a broken-symlink fixture proves
  coverage cannot silently shrink.
- Explicit-fallback validator now requires a statically readable
  English fallback VALUE: literal second arg, defaultValue literal, or
  defaultValue template literal. Dynamic defaultValue variables /
  positional variables are rejected (tests added).
- runAudit derives default source roots from the ACTUAL rootDir
  ([rootDir/src]); custom-root fixture test added.
- Removed dead audit code: buildFingerprint, buildMigrationFingerprint,
  KNOWN_ICONS, getSuppressionWithoutJustificationFindings, and the
  invalid `withFileExtensions` readdirSync option.
- localeValidator: removed unused pluralFormsFor and the unused data
  parameter from detectSingularPluralCollision; singular placeholder
  checks now reuse samePlaceholderMultiset.
- Removed the forbidden-files legacy rule entirely (FORBIDDEN_FILES,
  checkForbiddenFiles, report.forbidden, CLI output, tests) per apedley
  — those files never existed in this repo.
- i18nAudit.test.ts: no `any` (explicit local audit types), sync
  fixture helpers without async/await, exact toBe('Readable text')
  assertion.
- CLI: an output file without --json now prints an explicit error
  instead of silently ignoring the path; forbidden output removed.

Assisted-by: Open WebUI
@Dragonk

Dragonk commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Already reviewed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
SparkyFitnessMobile/__tests__/localization/appLanguage.test.ts (1)

48-57: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use mockReset() for setApplicationLanguage to match the sibling mocks.

Line 49 uses mockClear(), while lines 50-51 use mockReset(). mockClear() clears call records only. It does not drain queued mockRejectedValueOnce implementations. Tests at lines 149 and 171 queue one-time rejections. If a future test queues a one-time rejection that is not consumed, the rejection leaks into the next test and creates an order-dependent failure. mockReset() removes the queue, and line 55 re-installs the tracking implementation immediately afterwards.

♻️ Proposed change
-    mockNative.setApplicationLanguage.mockClear();
+    mockNative.setApplicationLanguage.mockReset();
     mockNative.getApplicationLanguage.mockReset();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SparkyFitnessMobile/__tests__/localization/appLanguage.test.ts` around lines
48 - 57, Replace mockNative.setApplicationLanguage.mockClear() with mockReset()
in the test setup, preserving the existing implementation installed immediately
afterward so each test starts without queued one-time behaviors.
SparkyFitnessMobile/scripts/i18n-audit/core.cjs (1)

184-186: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Share the source-scan-error rule name instead of duplicating the literal.

buildSummary recognizes scan errors by the string 'source-scan-error'. collectFindings in SparkyFitnessMobile/scripts/i18n-audit/sourceScanner.cjs produces that same string independently. If the scanner rule name changes, this counter silently reports 0, and the audit still blocks through localeStructuralErrors.length, so no test fails. Export the rule name from sourceScanner.cjs and reference it here.

♻️ Proposed change
-    sourceScanErrors: report.localeStructuralErrors.filter(
-      (e) => e.rule === 'source-scan-error',
-    ).length,
+    sourceScanErrors: report.localeStructuralErrors.filter(
+      (e) => e.rule === SOURCE_SCAN_ERROR_RULE,
+    ).length,

Add the shared constant in SparkyFitnessMobile/scripts/i18n-audit/sourceScanner.cjs and import it at the top of core.cjs:

// sourceScanner.cjs
const SOURCE_SCAN_ERROR_RULE = 'source-scan-error';
module.exports = { /* ...existing exports, */ SOURCE_SCAN_ERROR_RULE };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SparkyFitnessMobile/scripts/i18n-audit/core.cjs` around lines 184 - 186,
Define and export a shared SOURCE_SCAN_ERROR_RULE constant from
sourceScanner.cjs, then import and use it in buildSummary’s sourceScanErrors
filter instead of the duplicated 'source-scan-error' literal. Preserve the
existing counting behavior and all other exports.
SparkyFitnessMobile/__tests__/scripts/i18nAudit.test.ts (2)

787-838: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the suppression tests into their own describe block.

These six tests cover suppression directives, not static key resolution. They currently sit inside describe('Static key resolution'), which opens at line 720. Test-report grouping is misleading as a result. Close the static-key block before line 787 and open a describe('Suppressions') block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SparkyFitnessMobile/__tests__/scripts/i18nAudit.test.ts` around lines 787 -
838, The six suppression-related tests should be grouped separately from static
key resolution. Close the existing `describe('Static key resolution')` block
before the suppression tests, then wrap the tests from “hardcoded suppression
works” through “suppression does not hide a missing static key” in a new
`describe('Suppressions')` block.

9-11: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Type groupPluralKeys like the other imported audit symbols.

Lines 9, 14, and 20 give explicit types to LocaleValidator, collectFindings, and runAudit. Line 11 leaves groupPluralKeys with the implicit any that require returns, so the plural-grouping test at lines 917-927 is unchecked. Add an explicit signature.

As per path instructions: "Keep TypeScript strict, type-safe, and compiling cleanly."

♻️ Proposed change
-const groupPluralKeys = localeMod.groupPluralKeys;
+const groupPluralKeys = localeMod.groupPluralKeys as (
+  keys: string[],
+) => Record<string, string[]>;

Adjust the return type to match the implementation in SparkyFitnessMobile/scripts/i18n-audit/localeValidator.cjs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SparkyFitnessMobile/__tests__/scripts/i18nAudit.test.ts` around lines 9 - 11,
Explicitly type the groupPluralKeys import in the i18n audit test, matching the
implementation signature in localeValidator.cjs and the typing style used for
LocaleValidator, collectFindings, and runAudit. Ensure the plural-grouping test
receives a type-safe function rather than the implicit any from require.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@SparkyFitnessMobile/__tests__/screens/AppSettingsScreen.test.tsx`:
- Around line 130-149: Await the asynchronous language-selection handler
returned by picker().onSelect?.('pl') inside the act callback, ensuring the
rejected native write and resulting toast/state updates complete before the
assertions. Keep the existing failure expectations unchanged.

In `@SparkyFitnessMobile/src/components/BottomSheetPicker.tsx`:
- Line 41: Update the default accessibilityHint in BottomSheetPicker so omitted
values use the existing i18n translation mechanism instead of hardcoded English,
while preserving caller-provided hints; alternatively require localized hints
from every caller, including the Theme picker in AppSettingsScreen, and add an
assertion covering the Polish fallback.

---

Nitpick comments:
In `@SparkyFitnessMobile/__tests__/localization/appLanguage.test.ts`:
- Around line 48-57: Replace mockNative.setApplicationLanguage.mockClear() with
mockReset() in the test setup, preserving the existing implementation installed
immediately afterward so each test starts without queued one-time behaviors.

In `@SparkyFitnessMobile/__tests__/scripts/i18nAudit.test.ts`:
- Around line 787-838: The six suppression-related tests should be grouped
separately from static key resolution. Close the existing `describe('Static key
resolution')` block before the suppression tests, then wrap the tests from
“hardcoded suppression works” through “suppression does not hide a missing
static key” in a new `describe('Suppressions')` block.
- Around line 9-11: Explicitly type the groupPluralKeys import in the i18n audit
test, matching the implementation signature in localeValidator.cjs and the
typing style used for LocaleValidator, collectFindings, and runAudit. Ensure the
plural-grouping test receives a type-safe function rather than the implicit any
from require.

In `@SparkyFitnessMobile/scripts/i18n-audit/core.cjs`:
- Around line 184-186: Define and export a shared SOURCE_SCAN_ERROR_RULE
constant from sourceScanner.cjs, then import and use it in buildSummary’s
sourceScanErrors filter instead of the duplicated 'source-scan-error' literal.
Preserve the existing counting behavior and all other exports.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: adc1288f-68e8-4d35-ace1-5fa90f83bbfe

📥 Commits

Reviewing files that changed from the base of the PR and between 7cd6575 and cd7a6fd.

📒 Files selected for processing (28)
  • SparkyFitnessMobile/AGENTS.md
  • SparkyFitnessMobile/__tests__/config/withAppLanguage.test.ts
  • SparkyFitnessMobile/__tests__/hooks/useAppBootstrap.test.tsx
  • SparkyFitnessMobile/__tests__/hooks/useAppLanguageForegroundSync.test.tsx
  • SparkyFitnessMobile/__tests__/hooks/useAppStartup.test.ts
  • SparkyFitnessMobile/__tests__/hooks/useScreenHeader.test.tsx
  • SparkyFitnessMobile/__tests__/localization/appLanguage.test.ts
  • SparkyFitnessMobile/__tests__/localization/i18n.test.ts
  • SparkyFitnessMobile/__tests__/screens/AppSettingsScreen.test.tsx
  • SparkyFitnessMobile/__tests__/scripts/i18nAudit.test.ts
  • SparkyFitnessMobile/plugins/withAppLanguage.ts
  • SparkyFitnessMobile/scripts/audit-mobile-i18n.mjs
  • SparkyFitnessMobile/scripts/i18n-audit/core.cjs
  • SparkyFitnessMobile/scripts/i18n-audit/localeValidator.cjs
  • SparkyFitnessMobile/scripts/i18n-audit/sourceScanner.cjs
  • SparkyFitnessMobile/src/components/BottomSheetPicker.tsx
  • SparkyFitnessMobile/src/hooks/useAppBootstrap.ts
  • SparkyFitnessMobile/src/hooks/useAppLanguageForegroundSync.ts
  • SparkyFitnessMobile/src/hooks/useAppStartup.ts
  • SparkyFitnessMobile/src/hooks/useScreenHeader.tsx
  • SparkyFitnessMobile/src/localization/appLanguage.ts
  • SparkyFitnessMobile/src/localization/i18n.ts
  • SparkyFitnessMobile/src/localization/index.ts
  • SparkyFitnessMobile/src/localization/locales/en/translation.json
  • SparkyFitnessMobile/src/localization/locales/pl/translation.json
  • SparkyFitnessMobile/src/screens/AppSettingsScreen.tsx
  • SparkyFitnessMobile/src/services/appLanguageNative.ts
  • SparkyFitnessMobile/targets/android-language/kotlin/com/sparkyapps/sparkyfitness/language/AppLanguageModule.kt
🚧 Files skipped from review as they are similar to previous changes (9)
  • SparkyFitnessMobile/src/localization/locales/pl/translation.json
  • SparkyFitnessMobile/src/screens/AppSettingsScreen.tsx
  • SparkyFitnessMobile/src/localization/locales/en/translation.json
  • SparkyFitnessMobile/scripts/audit-mobile-i18n.mjs
  • SparkyFitnessMobile/src/hooks/useAppLanguageForegroundSync.ts
  • SparkyFitnessMobile/scripts/i18n-audit/localeValidator.cjs
  • SparkyFitnessMobile/src/hooks/useAppBootstrap.ts
  • SparkyFitnessMobile/src/hooks/useScreenHeader.tsx
  • SparkyFitnessMobile/scripts/i18n-audit/sourceScanner.cjs

Comment thread SparkyFitnessMobile/__tests__/screens/AppSettingsScreen.test.tsx
Comment thread SparkyFitnessMobile/src/components/BottomSheetPicker.tsx
- Await the async language-selection handler inside act in the
  AppSettingsScreen failure test so the toast/state assertions run after
  the rejected native write settles.
- Use mockReset() for setApplicationLanguage in the appLanguage test
  setup to avoid leaking queued one-time rejections across tests.
- Share the SOURCE_SCAN_ERROR_RULE constant between sourceScanner.cjs
  and core.cjs instead of duplicating the literal.
- Type groupPluralKeys explicitly in i18nAudit.test.ts (no implicit
  any from require).

Assisted-by: Open WebUI
@Dragonk

Dragonk commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Disposition of the four nitpick comments from the fresh CodeRabbit review (review id 4896297245):

  1. appLanguage.test.ts — mockClear → mockReset for setApplicationLanguage: fixed in ce4acd4 (mockReset + immediate re-install of the tracking implementation).
  2. core.cjs — shared SOURCE_SCAN_ERROR_RULE: fixed in ce4acd4; the constant is exported from sourceScanner.cjs and referenced by buildSummary.
  3. i18nAudit.test.ts — suppression tests grouped separately: already satisfied — the six suppression tests live in their own describe('Per-rule suppression') block (they were never inside Static key resolution).
  4. i18nAudit.test.ts — explicit groupPluralKeys type: fixed in ce4acd4 (local PluralGroup interface, no implicit any).

@Dragonk

Dragonk commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

@apedley — follow-up on your review; everything you flagged is addressed on this new HEAD (ce4acd45):

  • Dead localization APIs removed: applyLanguagePreference, formatLocalizedNumber, getAppLocale, resolveLanguagePreference, getNativeApplicationLanguage (and NativeLanguageValue) are gone, with no production callers. initializeI18n(language) now requires the language; the AsyncStorage/Zustand-blob branch and its duplicate STORE_KEY/normalizer are deleted. Preference persistence has one owner (appPreferencesStore), and native → effective → i18next has one owner (appLanguage.ts).
  • Dead audit leftovers removed: buildFingerprint, buildMigrationFingerprint, KNOWN_ICONS, getSuppressionWithoutJustificationFindings, the invalid withFileExtensions readdir option, unused pluralFormsFor, the unused data param, and the whole forbidden-files rule (those files never existed in this repo).
  • Bad scanner code fixed: the truthy-function JSX guard and the never-running expression-child branch; {'…'} / {`…`} children are inventoried like plain text. The audit now fails closed on scan errors, and the explicit-fallback validator requires a statically readable English fallback value (dynamic defaultValue is rejected).
  • LogService, not console.error: all new localization-runtime error/warning paths use addLog with ERROR/WARNING status.
  • Language init can no longer force Onboarding: useAppBootstrap runs language init and server-config loading as independent failure domains; a broken locale never changes the route (regression test locks Tabs+linking when config is valid). useInitialRoute is deleted; AGENTS.md now names App.tsx + useAppBootstrap as owners of language init / initial route / linking / splash hiding. Splash-hide failures are logged and never reject the bootstrap promise.
  • Android architecture per [Feature]: Add localization (i18n) support to the mobile app #1774: Android 13+ (API 33+) uses the platform LocaleManager/applicationLocales via a thin SDK_INT-guarded bridge; Android ≤12 uses only the stored preference + expo-localization + i18next. No AppCompat dependency, AppLocalesMetadataHolderService, or autoStoreLocales anywhere. The migration marker applies only where native support exists and retries on failure, so a Polish selection on Android 12 survives an upgrade to 13. expo prebuild --clean --platform android twice is idempotent; the fork's build-test-apk.yml built a debug APK from the final commit successfully (run 31384852833, artifact SparkyFitness-ce4acd45-dev-release).
  • Endonym: the English catalog renders Polski (tested in English UI).
  • Scope: still infrastructure-only — ~10 representative strings localized; the hardcoded-UI inventory (2358 informational entries) and the mass translation stay in the follow-up PRs. Widgets/Live Activity localization and Weblate are untouched here.
  • CI on the final HEAD: Mobile Tests, Server Tests, Detect Changes, GitGuardian green; fresh CodeRabbit review addressed (2 actionable fixed, remaining threads closed). Device/emulator validation is pending on the produced APK (the pr-validation bot's only remaining required action).

Please wait with review. I need first build apk and test it on device.

Final SPLIT-3 correctness pass:

- Migration precedence on Android 13+: runMigration now reads the
  platform app language FIRST. An explicit Android Settings choice
  (en/pl) wins over the legacy stored preference and is adopted with no
  native write (cases A/B). A legacy explicit preference seeds the
  platform locale only when Android still follows System (cases C/D);
  system/system performs no needless write (case E). If the initial
  native read fails, the stored preference is used locally, the marker
  stays absent, and the migration retries next launch (case F) — native
  state is never overwritten when it could not be read. Unsupported
  native values are repaired to system explicitly and never written into
  the store.

- setAppLanguagePreference is now transactional: previous store
  preference, previous effective language and (on Android 13+) previous
  native preference are snapshotted; i18n is applied and the store is
  committed LAST. A failed i18n apply rolls the native value back
  best-effort; if the rollback itself fails, the real native state is
  re-read and the store/i18n are reconciled to it so store/native/i18n
  can never knowingly contradict each other. Android <=12/iOS keep the
  store uncommitted on i18n failure.

- AppSettingsScreen only awaits setAppLanguagePreference and shows the
  existing localized error toast (stale lower-layer comment fixed).

Tests: explicit-native-wins (system/en/pl stores), legacy-seeds-system,
system/system no-write, native-read-failure fallback, i18n-failure
rollback on Android 13+, i18n-failure store-preservation on Android
<=12, and rollback-failure reconciliation.

Assisted-by: Open WebUI
@Dragonk

Dragonk commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Already reviewed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
SparkyFitnessMobile/src/localization/appLanguage.ts (1)

315-317: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Narrow i18n.resolvedLanguage instead of asserting it.

resolvedLanguage is typed as a plain string. The assertion at Line 316 lets any resolved value flow into applyEffectiveLanguage during post-rollback restoration. SUPPORTED_LANGUAGES and normalizeLanguage already exist in src/localization/i18n.ts, so a guarded narrow removes the unchecked assertion without changing behavior for 'en'/'pl'.

♻️ Proposed narrowing
-    const previousEffective = i18n.resolvedLanguage as SupportedLanguage | undefined;
+    const resolved = i18n.resolvedLanguage;
+    const previousEffective: SupportedLanguage | undefined =
+      resolved && (SUPPORTED_LANGUAGES as readonly string[]).includes(resolved)
+        ? (resolved as SupportedLanguage)
+        : undefined;

Import SUPPORTED_LANGUAGES from ./i18n if it is not imported yet.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SparkyFitnessMobile/src/localization/appLanguage.ts` around lines 315 - 317,
Update the previousEffective assignment in the rollback restoration flow to
narrow i18n.resolvedLanguage against SUPPORTED_LANGUAGES instead of asserting
SupportedLanguage. Import SUPPORTED_LANGUAGES from ./i18n if needed, and ensure
unsupported or absent values become undefined before being passed to
applyEffectiveLanguage while preserving valid en/pl behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@SparkyFitnessMobile/src/localization/appLanguage.ts`:
- Around line 117-126: Update the doc comment for readNativePreference to state
that unsupported native language tags map to 'unsupported', while rejections
from AppLanguageNative.getApplicationLanguage() propagate to callers. Preserve
the existing try/catch handling at all call sites.

---

Nitpick comments:
In `@SparkyFitnessMobile/src/localization/appLanguage.ts`:
- Around line 315-317: Update the previousEffective assignment in the rollback
restoration flow to narrow i18n.resolvedLanguage against SUPPORTED_LANGUAGES
instead of asserting SupportedLanguage. Import SUPPORTED_LANGUAGES from ./i18n
if needed, and ensure unsupported or absent values become undefined before being
passed to applyEffectiveLanguage while preserving valid en/pl behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a9e5ecf5-55f9-4814-b331-60b8d3d493d0

📥 Commits

Reviewing files that changed from the base of the PR and between ce4acd4 and 47d26d5.

📒 Files selected for processing (3)
  • SparkyFitnessMobile/__tests__/localization/appLanguage.test.ts
  • SparkyFitnessMobile/src/localization/appLanguage.ts
  • SparkyFitnessMobile/src/screens/AppSettingsScreen.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • SparkyFitnessMobile/src/screens/AppSettingsScreen.tsx

Comment thread SparkyFitnessMobile/src/localization/appLanguage.ts
- Correct the readNativePreference doc comment: unsupported values map
  to 'unsupported', while rejected native reads propagate to callers
  (which all handle them) — the old wording implied the helper catches.
- Narrow i18n.resolvedLanguage through SUPPORTED_LANGUAGES instead of
  an unchecked type assertion when snapshotting the previous effective
  language for rollback.

Assisted-by: Open WebUI
@Dragonk

Dragonk commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Disposition of CodeRabbit review 4897318270 (SHA 47d26d5):

  1. readNativePreference doc comment (inline): fixed in b9f47e3 — the comment now states unsupported values map to 'unsupported' while rejected native reads propagate to callers (all call sites wrap the read in try/catch and never overwrite native state they could not read).
  2. Narrow i18n.resolvedLanguage instead of asserting (nitpick): fixed in b9f47e3SUPPORTED_LANGUAGES is imported and the previous effective language is narrowed through it, so unsupported/absent values become undefined instead of flowing into applyEffectiveLanguage.

@Dragonk

Dragonk commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

@apedley — one final Android 13+ migration edge case was found during review: an existing per-app language selected in Android Settings could be overwritten by the first legacy-preference handoff. The migration now reads native state first — an explicit Android App Language wins, while a legacy local en/pl preference seeds the native setting only when Android is still following System. I also made Settings language changes transactional so a failed i18n apply cannot leave store/native/i18n knowingly out of sync (rollback + reconciliation to actual state if the rollback itself fails). Scope remains infrastructure-only; the fresh dev-release APK for the final commit is at run 31394733850 (SparkyFitness-b9f47e3c-dev-release).

@Dragonk

Dragonk commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

@apedley tests done in real device. I don't have application language settings in my Redmi Note so I can't test that.

@CodeWithCJ

Copy link
Copy Markdown
Owner

@Dragonk THank you very much for splitting the PR. Two small things from claude. Could you take a look.

@apedley might be on vacation. So, I will merge this PR for now once you take a look at the the below two items.

1. The endonym change only landed in en.

pl/translation.json still has "english": "Angielski", so the language list reads "English / Polski" in English but "Angielski / Polski" in Polish. If we're going with endonyms, pl should be "English" / "Polski" too — otherwise the same list renders differently depending on which language you're already in, which is the thing endonyms exist to prevent.

2. iOS advertises a language switcher we don't honor.

app.config.ts sets expo-localization supportedLocales.ios: ['en', 'pl'], which makes iOS show a per-app Language entry under Settings → SparkyFitness. But we deliberately make no native call on iOS — the stored preference is authoritative. So if someone sets Polish there while the in-app preference is explicitly en, the app renders English with no way to tell why. That's the same divergence the Android 13+ LocaleManager path was built to avoid, just left open on the one platform.

Can we drop supportedLocales.ios and keep the in-app picker as the single control? (Keeping the locales map — the localized permission strings are worth having either way.) If it's intentional prep for a later PR, a comment explaining that would do.

@Dragonk

Dragonk commented Aug 15, 2026

Copy link
Copy Markdown
Contributor Author

@CodeWithCJ
Thanks — both findings are valid.

I’ll fix the endonym issue so both catalogs consistently show English / Polski.

For iOS, I agree the current implementation can diverge: iOS exposes a per-app Language setting, while SparkyFitness may still retain a separate local en/pl override. However, I don’t think removing supportedLocales.ios is the right fix, because it would undo the native-language direction previously agreed with apedley in #1774.

The intended model was:

  • Android 13+: full two-way synchronization through LocaleManager;
  • iOS: the native per-app Language setting is authoritative;
  • Android 12 and below: local in-app fallback.

Because iOS does not provide a public equivalent of Android’s language setter, I propose to keep supportedLocales.ios, read and follow the effective native iOS locale, and replace the local iOS picker with a row that shows the current language and opens the app’s iOS Settings. A stale local preference must never override the native iOS selection.

I’ll also preserve the existing safeguards: native state wins over stale local state, operations remain serialized, redundant writes are skipped, store updates happen only after successful native/i18next changes, Android writes retain rollback behavior, and foreground sync adopts external changes without creating a write-back loop.

I don’t have access to an iPhone, Mac, or Xcode, so I can implement and cover the behavior with config and unit tests, but I’ll need help validating the final iOS build on a simulator or physical device before merge.

@CodeWithCJ

Copy link
Copy Markdown
Owner

Thanks. Sure, I can review through both simulator and a real iPhone when you are ready

@CodeWithCJ

Copy link
Copy Markdown
Owner

please tag me once when it is ready for iPhone testing

@Dragonk

Dragonk commented Aug 16, 2026

Copy link
Copy Markdown
Contributor Author

Implemented in fb430c6. Typecheck, lint, the i18n audit (all blocking counters 0), expo config --type public generation, and the focused native-language regression suite all pass (8 suites / 81 tests); the bootstrap/startup/store set also passes (3 suites / 23 tests). The full mobile Jest suite passes on this exact commit (291 suites / 4861 tests, 0 failed), and the upstream checks on fb430c6 — Mobile Tests, Server Tests, Detect Changes, Validate & Label, GitGuardian, CodeRabbit — are all green. A release APK + AAB build is running via the fork's android.yml workflow on this branch for Android validation.

I still do not have access to an iPhone, Mac, or Xcode, so I would appreciate help validating the final iOS behavior on a simulator or a physical device.

@CodeWithCJ

Copy link
Copy Markdown
Owner

I tested through Simulator & real iPhone and it worked:

Settings-->SparkyFitenss-->Language

image

SParkyFitness now renders Polish language.
image

@CodeWithCJ

Copy link
Copy Markdown
Owner

Let me know if I can merge this PR

@CodeWithCJ

Copy link
Copy Markdown
Owner

Claude flagged this. I know translation is not complete, but have a look.

notifications.title = "Powiadomienia" exists in the resource file, but AppSettingsScreen.tsx:167 passes title="Notifications" as a literal instead of using it. That's a dead key — the translation is shipped but never rendered, which matches the English "Notifications" in your screenshot. Either the row should call t('notifications.title', 'Notifications') or the key should be dropped from both locale files.

@Dragonk

Dragonk commented Aug 16, 2026

Copy link
Copy Markdown
Contributor Author

@CodeWithCJ
Thanks — I’ve now completed testing on a physical Android device. Switching between System / English / Polski, persistence after restart, and the localized settings UI all worked correctly.

I also saw that you tested the iOS build — thank you for covering that platform.

Regarding the orphaned translation, I don’t think it needs to block this infrastructure PR. After this PR and the native widget localization PR, I plan to open a separate PR translating all remaining user-facing strings in the mobile app. I’ll clean up isolated or orphaned translation entries as part of that complete catalog pass.

Android screenshots:

Screenshot_2026-08-16-17-58-04-222_org SparkyApps SparkyFitnessMobile1 dev Screenshot_2026-08-16-17-58-13-325_org SparkyApps SparkyFitnessMobile1 dev Screenshot_2026-08-16-17-58-10-312_org SparkyApps SparkyFitnessMobile1 dev

@CodeWithCJ
CodeWithCJ merged commit 659f819 into CodeWithCJ:main Aug 16, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request mobile

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Mobile App - Allow to change App Language

3 participants