Skip to content

feat(mobile): complete EN/PL localization and locale-aware presentation - #2189

Merged
CodeWithCJ merged 315 commits into
CodeWithCJ:mainfrom
Dragonk:feat/mobile-complete-localization
Aug 24, 2026
Merged

feat(mobile): complete EN/PL localization and locale-aware presentation#2189
CodeWithCJ merged 315 commits into
CodeWithCJ:mainfrom
Dragonk:feat/mobile-complete-localization

Conversation

@Dragonk

@Dragonk Dragonk commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Update — 2026-08-24 (review round 2)

This update addresses all blocking and non-blocking findings from the latest CodeWithCJ/Claude review and re-integrates upstream/main.

Final PR HEAD: 6e1c08b33ec9a014aa0321eca50b3d77e291704f (ordinary push, no rebase/force-push).
Integrated upstream/main SHA: da63d72f87041db7dc39283508ce615bc0456215. Merge-base equals upstream/main, so GitHub reports the PR mergeable with no conflicts. Upstream telemetry/health-sync changes are retained.

Blocking findings fixed:

  1. MealDetailScreen pluralization — removed the manual count === 1 ? singular : plural ternary for servingLabel/ingredientLabel; both now use t('mealDetail.servingsLabel'/'ingredientsLabel', { count }) with a full plural family (EN _one/_other, PL _one/_few/_many/_other). Fixes 3 porcji3 porcje, 5 składniki5 składników.
  2. WorkoutReorderList pluralization — removed the count === 1 ? set : sets ternary; t('workoutReorder.sets', { count }) now drives selection with a full plural family. Fixes 1 serii1 seria, 3 serii3 serie.
  3. CalendarSheet quick month/year navigationhideHeader had removed access to the react-native-ui-datepicker month/year selector. Restored quick-jump via the picker's initialView ('day' | 'month' | 'year') and onYearChange (verified against the 3.1.2 type contract: initialView?: CalendarViews, onYearChange?: (year: number) => void). Localized month/year caption buttons switch to year-step prev/next inside the grids; selecting a month/year returns to the day grid. firstDayOfWeek is preserved across language switches; locale-aware month/weekday labels still follow the app locale; PL ↔ EN runtime switch still updates without restart; selected date stays correct.
  4. Mergeability — branch merged with current upstream/main; all upstream changes (telemetry cache, health-sync, Garmin, CI actions) are retained. No conflicts remain.

Non-blocking findings fixed:

  • F1 silent English copy drift — split reused compact keys into context-specific semantic keys so English UX copy matches pre-i18n upstream: nutrients.saturatedFatLabel ("Saturated Fat" full label) instead of the compact Sat. Fat; *.caloriesUnit ("Cal"/"cal") instead of the shared kcal; activeWorkout.bar.clear ("Clear") for the destructive alert button instead of clearWorkout ("Clear workout"). PL values remain correct ("kcal", "Wyczyść"). Existing *.caloriesShort = "kcal" stays for the contexts that already used "kcal" upstream.
  • F2 FoodPhotoImproveScreen pluralization — replaced images.length > 1 ? photos : photo with t('foodPhotoImprove.subjectLabel', { count }) and a full PL family (zdjęcie / zdjęcia / zdjęć). Fixes 5 zdjęcia5 zdjęć.

Broader plural-pattern audit (G): scanned the mobile source for manual count === 1 ? singular : plural / > 1 ? plural : singular / singular-plural sibling keys. Found and fixed WorkoutCompleteScreen allSets (was using a legacy allSetsOne for count=1 alongside a count-based allSets; now uses t('workoutComplete.labels.allSets', { count }) consistently). healthDataDisplay.unit() and foodUnitLocalization already pass count to i18next with full families and were left as-is. The mechanical i18n audit cannot statically detect this antipattern without a brittle AST parser; targeted regression tests cover the fixed call sites.

Supporting shared/src/cycle/predictions.ts change (H): the optional params field and the new upcoming_period_today alert are consumed by SparkyFitnessMobile/src/utils/cycleLocalization.ts (localizes by key + params, with generic fallbacks when params is absent). Server/frontend consumers read .message as the fallback and ignore the optional params, so the additive contract does not break them. The diff === 0 vs diff >= -3 && diff <= 0 split only changes upcoming_periodupcoming_period_today for the same-day case; other ranges are unaffected. This change is needed for semantically correct localized cycle messages and is retained.

Regression tests (I): added/extended tests against the real EN/PL catalogs (not just inline defaultValue):

  • MealDetailScreen: EN singular/plural + PL one/few/many for servings/ingredients (1, 2, 3, 5, 12, 22, 25).
  • WorkoutReorderList: EN singular/plural + PL one/few/many for sets (1, 2, 3, 5, 12, 22, 25).
  • FoodPhotoImproveScreen: PL one/few/many for subjectLabel (1, 2, 3, 5, 12, 22).
  • CalendarSheet: month/year quick-jump reachable, returns to day grid, year-step prev/next labels, localized caption, firstDayOfWeek preserved across language switch, prev/next month still works.
  • All new semantic keys exist in both EN and PL catalogs (i18n audit enforces EN/PL structural parity).

Full validation (J): from SparkyFitnessMobile/:

  • pnpm run validate (typecheck + lint + i18n:audit): PASS — 0 errors, 0 warnings.
  • pnpm run i18n:audit: PASS — 0 locale structural errors, 0 missing static keys, 0 placeholder errors, 0 plural errors, 0 missing English fallbacks, 0 dynamic keys, 0 source scan errors.
  • jest --ci --coverage (full mobile test suite): 352 suites / 5759 tests PASS.
  • git diff --check: PASS.

GitHub CI (final HEAD 6e1c08b33): run 32770751200SUCCESS. Mobile Tests PASS (5m20s), Server Tests PASS, Detect Changes PASS, Validate & Label PASS, Auto-Merge Translations PASS. No force-push; ordinary git push origin feat/mobile-complete-localization.

The exact final-HEAD build was not re-tested on a physical Android/iOS device in this round; the prior physical-device evidence below (recorded from an earlier SHA) remains representative of the UI layout, but it is not an exact-final-HEAD recording. Device-level verification of the new HEAD is left to the maintainer's environment.


Description

What problem does this PR solve?

SparkyFitnessMobile previously did not have complete localization coverage. Large parts of the React Native UI, controlled domain values, native Android/iOS surfaces, widgets, accessibility text, and locale-sensitive date, number, and unit presentation were English-only or not consistently tied to the selected application language.

This PR completes the mobile i18n refactor and introduces a reusable localization foundation for the mobile app, with full English and Polish coverage across the current mobile product and supported native/runtime surfaces.

How did you implement the solution?

The mobile app now uses bundled semantic i18next catalogs with deterministic English fallback, system/per-app language integration, a manual language selector, and reactive runtime language switching initialized before localized UI presentation. The architecture was introduced in merged PR #2069, which added the mobile i18n bootstrap, bundled catalogs, fallback contract, and platform language integration. Merged PR #2072 extended that foundation to native widgets and Live Activity. This PR completes the application-wide migration by localizing the remaining React Native/mobile presentation surfaces and making locale-sensitive display consistent across the product.

Localization is applied across React Native screens and components, native Android/iOS resources, widgets/activity surfaces, permission and accessibility text, alerts/notifications where applicable, and controlled values received from shared/server contracts. Dates, times, numbers, decimal separators, measurements, units, charts/tooltips, and runtime calendar presentation now follow the selected SparkyFitness app language while preserving the configured first day of week. Canonical raw API/storage values remain stable; custom and user-provided text remains literal.

Coverage spans dashboard/diary, nutrition and foods, meal builder/types, workouts and active-workout timers, fasting, medications, health synchronization/presentation, cycle tracking, TTC, pregnancy (including Baby Development and safety content), settings, accessibility, and native widget/activity surfaces. The change also adds localization audits and regression coverage for catalog structure, static keys, placeholders, plurals, hardcoded UI text, dynamic keys, semantic/linguistic behavior, and locale-sensitive presentation.

Linked Issue: #1774
Implements the mobile app-language functionality requested in #1490.
Builds on merged PRs #2069 and #2072.

How to Test

  1. Build/install the final SparkyFitnessMobile APK from final PR HEAD e97926fb9cba13a6faba4a39dcbc9d5d87a9a03f, or install Build Test APK #42 / artifact 9498301607.
  2. Start the app in Polish and check representative areas: Dashboard/Diary, food and nutrition, workouts, fasting, medications, settings, cycle/pregnancy, and native-facing flows.
  3. Runtime retest after all final review fixes:
    • Cycle Hub
    • Copy Meal
    • Workout Complete
    • meal-type labels in Edit Logged Meal
    • Dashboard
    • CalendarSheet
    • PL -> EN without restarting
    • EN -> PL without restarting
    • configured first day of week remains unchanged
    • built-in/system labels are localized
    • custom/user-provided text remains literal
  4. Verify locale-sensitive numbers, units, dates, and times follow the selected app language and preferences.

Physical device evidence

Final Android physical-device retest after all review fixes: PASS.

  • Final PR SHA: e97926fb9cba13a6faba4a39dcbc9d5d87a9a03f
  • Final upstream CI: CI Tests #3077 / 32658109951 (SUCCESS)
  • Build Test APK: run #42 / 32658445281 (SUCCESS)
  • Requested ref: feat/mobile-complete-localization
  • Actual application commit checked out by the build: e97926fb9cba13a6faba4a39dcbc9d5d87a9a03f
  • Exact expected-SHA guard: PASS
  • App variant: dev
  • Gradle build type: release
  • Android architecture: arm64-v8a
  • Android package: org.SparkyApps.SparkyFitnessMobile1.dev
  • APK artifact: SparkyFitness-e97926fb-dev-release
  • APK file: SparkyFitness-feat-mobile-complete-localization-e97926fb-dev-release.apk
  • APK SHA-256: 1442c180d5c0b76a2ebaf5083e77c20dc6309197def2ab92914795b6d089e420
  • Signing: temporary test keystore
  • Physical Android device: final APK installed/tested, PASS

Physical iOS testing was not performed because an iOS device was not available.

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:

Frontend changes (SparkyFitnessFrontend/):

  • [MANDATORY for Frontend changes] Quality: I have run pnpm run validate and it passes.
  • [MANDATORY for Frontend changes] Translations: I have only updated the English (en) translation file.

Backend changes (SparkyFitnessServer/):

  • [MANDATORY for Backend changes] Code Quality: I have run typecheck, lint, and tests. New files use TypeScript, new endpoints have Zod schemas, and new endpoints include tests.
  • [MANDATORY for Backend changes] Database Security: I have updated rls_policies.sql for any new user-specific tables.

UI changes (components, screens, pages):

  • [MANDATORY for UI changes] Screenshots: I have attached Before/After screenshots below. An existing Android recording is attached below as representative visual before/after evidence; it predates the final review-cleanup commits.

Mobile changes (SparkyFitnessMobile/):

  • [MANDATORY for Mobile changes] Tested on device or emulator: I have verified the changes work on iOS or Android. Final Android physical-device test: PASS.

Screenshots

Final Android physical-device test

The recording below predates the final review-cleanup commits and is retained as representative visual evidence because the subsequent changes did not materially alter the UI layout or appearance. It was recorded from earlier SHA 1067c4b579910d5d7219a3cecaeff887876ab084; it is not an exact-final-SHA recording. The exact final SHA (at the time) was separately re-tested on a real Android device using the final APK documented above; that SHA is now historical (see the round-2 update at the top of this body).

SparkyFitness-PR2189-device-test.mp4

Notes for Reviewers

  • feat(mobile): add i18n infrastructure and language settings #2069 introduced the core mobile i18n and language-settings/platform-language contract; feat(mobile): localize native widgets and live activity #2072 extended it to native widgets and Workout Live Activity; feat(mobile): complete EN/PL localization and locale-aware presentation #2189 completes the application-wide EN/PL migration and locale-aware presentation. The architecture discussion is in [Feature]: Add localization (i18n) support to the mobile app #1774, and this implements the mobile app-language functionality requested in [Feature]: Mobile App - Allow to change App Language #1490. RTL remains outside the scope of this initial mobile-localization contribution.

  • English and Polish are included here; the bundled mobile catalog is structured for a dedicated Weblate component that can reuse the project glossary/translation memory later. Application-owned controlled values are localized at the UI boundary; custom/user-generated values remain literal.

  • Native iOS localization/static coverage is included, but full physical iOS/Xcode validation was not available in the Linux/Android test environment.

  • (Historical, review round 1) Final PR HEAD: e97926fb9cba13a6faba4a39dcbc9d5d87a9a03f — superseded by the round-2 HEAD 6e1c08b33 documented in the update section at the top of this PR body. Final local validation: pnpm run validate PASS; typecheck PASS; lint PASS with 0 errors and 0 warnings; i18n audit PASS; locale structural errors: 0; missing static keys: 0; placeholder errors: 0; plural errors: 0; missing English fallbacks: 0; dynamic i18n keys: 0; source scan errors: 0; hardened hardcoded UI strings: 0; git diff --check PASS. Final exact upstream CI Tests #3077 / 32658109951 SUCCESS, including Mobile Tests, Mobile validation, Mobile tests with coverage, Server Tests, Server validation, and Server tests with coverage.

  • Since the last review: the blocking plural regression was removed; the branch was updated against main and the merge conflict was resolved; the i18n audit was rerun after the update; catalog/defaultValue drift and plural families are mechanically guarded; runtime localization helpers now use the injected TFunction contract with corrected memo dependencies; the convention is documented in SparkyFitnessMobile/AGENTS.md; the reviewed as any cases were removed/fixed; LOCALIZATION_WORK_LEDGER.md is not part of the final deliverable; and the later CalendarSheet regression was found and fixed.

  • The final guard validates EN defaultValue/catalog parity and plural families (EN _one/_other; PL _one/_few/_many/_other). Final CI is green.

  • Final audit improvements include controlled exercise taxonomy presentation localized through a shared injected-TFunction helper, removal of remaining application-owned hardcoded UI literals, and a hardened hardcoded-UI scanner covering conditional/logical presentation expressions and Unicode text. Unknown/custom/user/server content remains literal; the final hardened hardcoded UI count is zero.

  • Final Android physical-device runtime test: PASS after all final review fixes, using the exact e97926fb final-HEAD build documented in Physical device evidence above. Physical iOS testing was not performed because an iOS device was not available.

Dragonk added 30 commits August 18, 2026 16:16
@Dragonk

Dragonk commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

Re: #2189 (comment)

Thanks — these are now addressed.

  • MealLibraryRow plural regression: fixed the foodSearch.labels.itemCount plural family. EN now has _one/_other; PL has _one/_few/_many/_other. The mechanical i18n audit/plural validation passes.
  • Merge conflict / branch drift: resolved against base CodeWithCJ/main at 72217967c97e1f82185e70c34138c7f6c66f5879; current PR HEAD is 1067c4b579910d5d7219a3cecaeff887876ab084. The i18n audit was rerun after the update.
  • EN catalog/defaultValue drift: the final audit mechanically compares every static defaultValue with the shipped EN catalog; this is no longer a manual-only review.
  • PL plural gaps: workoutComplete.labels.sets, mealDetail.items, and medications.scheduleSummary.everyNDays now have the required plural families and pass the mechanical audit.
  • Screen/component catalog coverage: the guard scans the source defaults against the real shipped EN/PL catalogs and validates plural families, rather than relying only on the Jest singleton with empty resources.
  • as any: the CycleCard.tsx cast was removed/reused from the existing typed value, and CorrelationCards.tsx now accepts the injected TFunction type.
  • Guide: SparkyFitnessMobile/AGENTS.md now documents reactive/injected helpers, injected TFunction, static semantic keys, EN defaultValue parity, the plural-family contract, and pnpm run i18n:audit.
  • Ledger: LOCALIZATION_WORK_LEDGER.md was removed from the final deliverable.

Validation: typecheck PASS; lint PASS with 0 errors/0 warnings; i18n audit PASS; full Jest 345 suites / 5597 tests / 0 failures; final CI is green.

@Dragonk

Dragonk commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

Re: #2189 (comment)

Thanks — these are now addressed.

  1. Runtime language switching: presentation helpers now use the injected reactive t contract, and memoized results include the language-sensitive dependencies where needed. I also re-tested the final Android build on a physical device after all of these changes. Runtime PL <-> EN switching was re-checked on Cycle Hub, Copy Meal, Workout Complete, Edit Logged Meal meal-type labels, Dashboard, and CalendarSheet without restarting the app; the configured first day of week remained unchanged and custom/user text stayed literal.

  2. One helper convention: React UI gets t from useTranslation(); presentation helpers receive an injected TFunction; reactive presentation paths do not hide singleton i18n.t(). This is documented in SparkyFitnessMobile/AGENTS.md.

  3. Guard test: this is part of the final deliverable, not deferred. The final audit mechanically checks static defaultValue against the EN catalog and verifies that every key used with count has EN _one/_other and PL _one/_few/_many/_other (plus the other catalog/key, placeholder, fallback, dynamic-key, and source-scan checks). Final i18n audit: PASS.

@Dragonk
Dragonk marked this pull request as draft August 23, 2026 19:18
@Dragonk
Dragonk marked this pull request as ready for review August 23, 2026 19:23
@CodeWithCJ

Copy link
Copy Markdown
Owner

Could you take a look at the Claude comments once.

Blocking:

  1. SparkyFitnessMobile/src/screens/MealDetailScreen.tsx:292 — Polish plural
    regression, same class as the itemCount one from last round, and the new
    guard cannot see it. The plural form is chosen with a JS ternary and then
    interpolated as {{servingLabel}}/{{ingredientLabel}}, so i18next never gets
    a count for those words. PL catalog only has two forms:
    serving="porcja" / servings="porcji" → 3 servings renders "3 porcji"
    (needs "porcje"; few missing)
    ingredient="składnik" / ingredients="składniki"
    → 5 ingredients renders "5 składniki"
    (needs "składników"; many missing)
    Both halves are wrong, in opposite directions.

  2. SparkyFitnessMobile/src/components/WorkoutReorderList.tsx:268 — same pattern.
    PL workoutReorder.sets and workoutReorder.setsPlural are both "serii",
    so it renders "1 serii" (should be "seria") and "3 serii" (should be "serie").
    The correct shape already exists two files over — workoutComplete.labels.sets
    has _one/_few/_many/_other and passes count.

  3. SparkyFitnessMobile/src/components/CalendarSheet.tsx:99 — hideHeader plus a
    chevron-only custom header removes the month/year quick-jump. In
    react-native-ui-datepicker, header/month-button.tsx and header/year-button.tsx
    are the only callers of setCalendarView(), and components/calendar.tsx gates
    the whole Header on !hideHeader. With it hidden there is no way to reach the
    month or year grid — jumping back a year is now 12 chevron taps. Corroborating
    symptom: the Month: component override at line 111 can never render.
    Affects DashboardScreen, EditLoggedMealScreen, WorkoutDetailScreen,
    MedicationScheduleFormScreen, ActivityDetailScreen.

  4. Merge conflict, again. 37 commits behind main; git merge-tree conflicts in
    SparkyFitnessMobile/AGENTS.md and
    SparkyFitnessMobile/tests/screens/MedicationScheduleFormScreen.test.tsx.
    Both trivial. Upstream drift is services-only (telemetry/health-sync), so the
    i18n audit should stay green after the rebase — but rerun it.

Non-blocking:
5. Silent English copy changes from key reuse. Previously reported as "64
places"; the fix added a mechanical defaultValue↔catalog parity check, which
enforces source/catalog agreement but is blind to divergence from the string
main actually rendered. Still present and confirmed:
- FoodForm.tsx:1108 — full-width form field label "Saturated Fat" → "Sat. Fat"
(reuses the compact-row key nutrients.saturatedFatShort)
- ExerciseProgressCard.tsx:151 — unit "Cal" → "kcal"
- MealLibraryRow.tsx:112 — "cal" → "kcal"
- ActiveWorkoutBar.tsx:571 — destructive alert button "Clear" → "Clear workout"
(one key shared with the accessibilityLabel at :632)
6. FoodPhotoImproveScreen.tsx:513 — foodPhotoImprove.photos = "zdjęcia" is
wrong for 5+ ("zdjęć"), same ternary pattern. Low impact given photo counts.
7. shared/src/cycle/predictions.ts — the upcoming_period_today split and the
params fields are behavior changes, not localization. Verified safe:
buildCycleAlerts has exactly one consumer (mobile CycleHubScreen), the new key
is handled in cycleLocalization.ts, and dropping && diff <= 0 is a no-op
because the preceding branch is diff > 0. detectAnomalies' new optional
params doesn't affect the server consumer. Worth a line in the PR body.

…e-localization

# Conflicts:
#	SparkyFitnessMobile/AGENTS.md
#	SparkyFitnessMobile/__tests__/screens/MedicationScheduleFormScreen.test.tsx
…lendarSheet month/year nav

Blocker 1 — MealDetailScreen: replace manual count === 1 ? singular : plural
with i18next count pluralization for servingsLabel/ingredientsLabel (EN _one/_other,
PL _one/_few/_many/_other). Fixes "3 porcji" -> "3 porcje", "5 składniki" -> "5 składników".

Blocker 2 — WorkoutReorderList: replace manual count === 1 ? set : sets ternary
with t('workoutReorder.sets', { count }) using a full plural family (EN _one/_other,
PL _one/_few/_many/_other). Fixes "1 serii" -> "1 seria", "3 serii" -> "3 serie".

Blocker 3 — CalendarSheet: restore quick month/year navigation removed by hideHeader
by remounting the picker with initialView ('day'|'month'|'year') and onYearChange
(react-native-ui-datepicker 3.1.2 contract). Localized month/year caption buttons,
year-step prev/next inside the grids, firstDayOfWeek preserved across language switches.

Blocker 4 — merge with upstream/main (telemetry/health-sync changes retained).

F1 — Silent EN copy drift: split reused compact keys into context-specific semantic
keys so English UX copy matches pre-i18n upstream (Sat. Fat -> Saturated Fat full label,
Cal/kcal and cal/kcal unit labels, Clear workout alert button -> Clear).

F2 — FoodPhotoImproveScreen: replace length > 1 ? photos : photo ternary with
i18next count pluralization for subjectLabel (PL _one/_few/_many/_other).

G — Broader audit: WorkoutCompleteScreen allSets now uses count pluralization
instead of a manual allSetsOne/allSets ternary.

H — shared/cycle/predictions.ts: optional params + upcoming_period_today are consumed
by mobile cycleLocalization and fall back safely for server/frontend (.message).

Regression tests: MealDetail, WorkoutReorder, FoodPhotoImprove pluralization tests
against the real EN/PL catalogs (PL one/few/many for 1/2/3/5/12/22/25). CalendarSheet
tests cover month/year quick-jump, prev/next, locale-aware labels, firstDayOfWeek.
useScreenHeader duplicate-press mock returns defaultValue so localized Save resolves.

validate (typecheck+lint+i18n:audit): 0 errors, 0 warnings.
test:ci: 352 suites / 5759 tests PASS.
@Dragonk

Dragonk commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review — all blocking and non-blocking findings are addressed in the latest push (HEAD 6e1c08b33, no rebase/force-push). CI run 32770751200 is green and GitHub reports the PR mergeable against main.

1. MealDetailScreen pluralization — fixed. Removed the count === 1 ? singular : plural ternary for servingLabel/ingredientLabel; both now use t('mealDetail.servingsLabel'/'ingredientsLabel', { count }) with a full family (EN _one/_other, PL _one/_few/_many/_other). 3 porcji3 porcje, 5 składniki5 składników. Regression tests against the real PL catalog cover 1/2/3/5/12/22/25 for both servings and ingredients.

2. WorkoutReorderList pluralization — fixed. Removed the count === 1 ? set : sets ternary and the legacy setsPlural; t('workoutReorder.sets', { count }) now drives selection with PL _one/_few/_many/_other (same shape as workoutComplete.labels.sets). 1 serii1 seria, 3 serii3 serie. Tests cover 1/2/3/5/12/22/25 (EN + PL).

3. CalendarSheet month/year navigation — fixed. hideHeader is kept (for the custom chevron header), but quick month/year jump is restored via the picker's initialView ('day' | 'month' | 'year') and onYearChange — both verified against the react-native-ui-datepicker 3.1.2 type contract. The month and year caption segments are now pressable and remount the picker with the chosen initialView; selecting a month/year calls onMonthChange/onYearChange and returns to the day grid. Prev/next switches to year-step inside the grids. firstDayOfWeek is preserved across language switches; locale-aware month/weekday labels still follow the app locale; PL ↔ EN runtime switch updates without restart; selected date stays correct. Tests assert day/month/year view transitions, year-step labels, localized caption, and firstDayOfWeek preservation.

4. Merge conflict / current main — resolved. Branch merged with upstream/main da63d72; merge-base equals upstream/main. All upstream telemetry/health-sync/Garmin/CI changes are retained. No conflicts remain; GitHub reports mergeable: MERGEABLE. The i18n audit was rerun and stays green.

5. English-copy drift — fixed. Split reused compact keys into context-specific semantic keys so English matches pre-i18n upstream: nutrients.saturatedFatLabel ("Saturated Fat" full form) for FoodForm; *.caloriesUnit ("Cal"/"cal") for the contexts that rendered "Cal"/"cal" upstream (ExerciseProgressCard, ServingAdjustSheet, SwipeableFoodRow, FoodSummary, workoutSession, FoodLibraryRow, FoodResultRow, FoodSearchResultRow, MealLibraryRow, MealAddScreen); activeWorkout.bar.clear ("Clear") for the destructive alert button, with clearWorkout ("Clear workout") kept for the accessibilityLabel. PL values remain correct ("kcal", "Wyczyść"). The shared *.caloriesShort = "kcal" stays only where upstream already rendered "kcal".

6. FoodPhotoImproveScreen pluralization — fixed. Replaced images.length > 1 ? photos : photo with t('foodPhotoImprove.subjectLabel', { count }) and a full PL family (zdjęcie / zdjęcia / zdjęć). Tests cover 1/2/3/5/12/22 against the real PL catalog.

7. shared/cycle/predictions.ts — verified and retained. Added a note to the PR body: params is optional and additive; upcoming_period_today is consumed by cycleLocalization.ts; server/frontend consumers read .message and ignore params; the diff === 0 split only changes the same-day case. Needed for semantically correct localized cycle messages.

Broader plural-pattern audit — scanned the mobile source for the count === 1 ? singular : plural / > 1 ? plural : singular / singular-plural sibling-key antipattern. Found and fixed one additional call site: WorkoutCompleteScreen allSets (was using a legacy allSetsOne for count=1 alongside a count-based allSets; now uses t('workoutComplete.labels.allSets', { count }) consistently). healthDataDisplay.unit() and foodUnitLocalization already pass count to i18next with full families and were left as-is. The mechanical i18n audit cannot statically detect this antipattern without a brittle AST parser; targeted regression tests cover the fixed call sites and the limitation is noted in the PR body.

Final HEAD: 6e1c08b33ec9a014aa0321eca50b3d77e291704f
Validation: pnpm run validate PASS (0 errors, 0 warnings); pnpm run i18n:audit PASS (0 errors of every kind); full mobile Jest --ci --coverage 352 suites / 5759 tests PASS; git diff --check PASS.
CI: run 32770751200 — Mobile Tests PASS (5m20s), Server Tests PASS, Detect Changes PASS, Validate & Label PASS, Auto-Merge Translations PASS.

The exact final-HEAD build was not re-tested on a physical Android/iOS device in this round; the earlier device recording in the PR body is marked as historical. Device-level verification of the new HEAD is left to your environment. I won't request re-review until the new CI confirms green — which it now has.

@CodeWithCJ

Copy link
Copy Markdown
Owner

Awesome. Thank you. Two things left.

  1. SparkyFitnessMobile/src/components/CalendarSheet.tsx:65 — react-native-ui-datepicker
    only forwards onSelectMonth to your onMonthChange when the value actually
    changed (datetime-picker.tsx:527), but months.tsx:73 calls it on every tap. So
    opening the month grid and tapping the month that's already showing closes the
    grid internally while pickerView stays 'month'. The chevrons then step by 12
    months with "Previous year" labels, and the next press changes the key and
    re-opens the grid. Same for the year grid. Resetting pickerView to 'day' in
    shiftVisible covers it, at the cost of the year-step nicety.

    The new tests can't catch this — the datepicker is fully mocked in
    CalendarSheet.test.tsx, so the "only fire if changed" path never runs.

  2. SparkyFitnessMobile/src/stores/activeWorkoutStore.ts:830-834 — the rest
    notification title is localized on line 836 but the body above it is still raw
    English: Set ${n} of ${m}, ${...} target, and
    ${reps} rep${reps === 1 ? '' : 's'} target. Localized title over an English
    body, and it's the last hardcoded plural suffix in the diff. The scanner misses
    it because it's template-literal assembly in a store, not JSX.

Also: the shared-key pattern you fixed for activeWorkout.bar.clear is still live
at ActiveWorkoutScreen.tsx:483 and :852workout.removeExercise backs two
alert buttons and the context-menu label at :738, and on main both buttons said
"Remove" (main:480, main:848). Worth a scanner rule for one key backing two
different English strings, since that's twice now.

@CodeWithCJ

Copy link
Copy Markdown
Owner

I will merge this for now. you can fix these in the next PR.

@CodeWithCJ
CodeWithCJ merged commit de2fbc7 into CodeWithCJ:main Aug 24, 2026
12 of 13 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.

2 participants