Skip to content

feat(mobile): localize native widgets and live activity - #2072

Draft
Dragonk wants to merge 24 commits into
CodeWithCJ:mainfrom
Dragonk:feat/mobile-native-surface-localization
Draft

feat(mobile): localize native widgets and live activity#2072
Dragonk wants to merge 24 commits into
CodeWithCJ:mainfrom
Dragonk:feat/mobile-native-surface-localization

Conversation

@Dragonk

@Dragonk Dragonk commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

⚠️ Dependency note

This PR is stacked on #2069 (final PR3) and should be reviewed/merged after the i18n infrastructure PR.

The branch includes the FINAL PR3 HEAD (b9f47e3c) via a normal merge (no rebase, no force push). It builds on the final PR3 app-language model: the persisted system | en | pl preference through appPreferencesStore, Android 13+ native LocaleManager, Android <=12 local i18next preference (no AppCompat), transactional language changes, useAppBootstrap, foreground reconciliation, and the explicit English-fallback audit contract.


Description

What problem does this PR solve?
Native surfaces (Android Glance widgets, iOS WidgetKit, Workout Live Activity) do not automatically inherit React Native i18n. This PR fully localizes them EN/PL through the accepted app-language contract from PR3.

How did you implement the solution?

  • Android Glance widgets (calorie + macro): complete EN/PL localization through native values/widget_strings.xml + values-pl/widget_strings.xml (identical key sets), localized preview/initial layouts and content descriptions, and locale-aware number formatting. A widget-only locale override (WidgetLocale.kt, dedicated SharedPreferences namespace) is used only on Android 12 and below, where the explicit app language is local to the RN app (i18next/Zustand) and Android resources would not follow it automatically. On Android 13+ the native LocaleManager / App Languages is authoritative: localizedContext() always uses the native context and setOverride() clears any stale widget-only override for every request (en/pl/system), so an out-of-app App Language change (e.g. Settings / HyperOS) wins even before the app foregrounds. system means NO widget-only override anywhere — the key is removed and widgets follow the native/device locale. useWidgetLanguageRefresh reacts to BOTH preference changes and effective-language changes (so explicit → system clears the override even when the effective language stays the same), then reloads both widgets with partial-reload isolation and retry semantics.
  • Android widget resize: The pre-existing 2×1 calorie and 2×2 macro layouts remain the default visual contract (restored from the classic pre-localization widgets). Resizing preserves the same information hierarchy and geometry: horizontal growth gives labels/progress more room, while additional macro height may expose shortcut actions. The provider minimums retain the classic launcher footprint (calorie 110×40dp 2×1, macro 110×110dp 2×2, resizeMode horizontal|vertical) rather than creating a larger default widget. Both Glance widgets use SizeMode.Exact with LocalSize.current; LocalSize drives only small typography adjustments and the single macro action threshold (height ≥ 190dp). No HyperOS-specific APIs — standard Android AppWidget/Glance only.
  • iOS WidgetKit: en.lproj/pl.lproj Localizable.strings (identical key sets) cover widget names, descriptions, kcal labels, Food/Burned/Goal, Protein/Carbs/Fat, grams, and accessibility labels, plus explicit widget.search_food / widget.scan_barcode accessibility labels on the icon-only action buttons. useIOSWidgetLanguageRefresh writes widgetLocale for explicit en/pl and removes it for system (matching SharedHelpers.swift's "absent = follow native locale" contract), reloading both timelines with retry semantics. localizedWidgetString is hardened: explicit bundle → native bundle → English bundle → stable readable fallback, never a raw key.
  • Workout Live Activity: labels from activeWorkout.liveActivity keys (EN+PL) with an explicit English defaultValue per key — a missing PL key can never leak activeWorkout.liveActivity.* text into the UI (regression test included). Custom workout/exercise names stay literal user data; action identifiers (rest-add-15, rest-skip, complete-set) are unchanged. A language change updates the existing activity in place (same instance, timers, phase, identity) through the serial queue.

Resize explanation: Android widgets are now resizable through the standard AppWidget resize contract. Glance renders against the actual available size (width AND height — both provider XMLs allow independent horizontal|vertical resizing) so longer localized labels such as Polish "Pozostało … kcal" and "Węglowodany" remain readable instead of being forced into the original fixed footprint. Height classes keep the primary data readable at the honest minimum sizes and add actions/stacking only when the height supports them.

Linked Issue: Related to #1774 (does not close it — #1774 closes in PR5 after full UI localization + Weblate)

How to Test

  1. cd SparkyFitnessMobile && pnpm install
  2. pnpm run i18n:audit → expect user-facing t() without English fallback: 0, dynamic t() keys: 0, structural errors 0.
  3. npx jest --runInBand __tests__/config/widgetResourceContract.test.ts __tests__/config/iosWidgetResources.test.ts __tests__/config/iosWidgetSwiftContract.test.ts __tests__/config/withCalorieWidget.test.ts __tests__/hooks/useWidgetLanguageRefresh.test.tsx __tests__/hooks/useIOSWidgetLanguageRefresh.test.tsx __tests__/services/workoutLiveActivityLabels.test.ts __tests__/services/workoutLiveActivityLayoutContract.test.ts __tests__/services/workoutLiveActivity.test.ts
  4. Android: build + add widgets → switch app language System/EN/PL → widgets refresh without restart; resize handles appear; wider/taller sizes actually use the extra space; no clipped Polish labels.
  5. iOS: add widgets → switch app language → widgets reload; start a workout → change language mid-workout → Live Activity labels update in place (same instance).

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 in this environment — no emulator/connected device; see Notes. Existing PR3 device testing does not count as PR4 widget validation; a fresh APK from the final PR4 HEAD must be physically tested.)

Screenshots

Click to expand

Screenshots pending: after the final PR4 APK is built, Android screenshots will be added (calorie EN/PL default, calorie PL resized wider without clipped "Pozostało … kcal", macro PL with full "Węglowodany", macro larger resized state, optionally resize handles). iOS runtime screenshots remain pending unless a real iOS build/device becomes available. No PR3 language-settings screenshots are attached as proof of PR4.

Notes for Reviewers

  • Scope: native surfaces only (Android Glance widgets, iOS WidgetKit, Workout Live Activity). No full app string migration, no Weblate, no date/calendar localization, no B1/C1, no other PR5 findings.
  • Android: expo prebuild --clean --platform android run twice — idempotent (widget Kotlin sources 1×, widget string resources 1×, receivers 2×, AppLanguage package 1×, resize provider XML preserved). The fork Build Test APK workflow from the EXACT final SHA (75307602) succeeded (exact-SHA guard passed; run 31479807092 (final re-run on the same SHA), artifact SparkyFitness-75307602-dev-release, APK SparkyFitness-feat-mobile-native-surface-localization-75307602-dev-release.apk, SHA256 d26bfc75…). Physical device/emulator test pending (no device) — the Device checkbox stays unchecked until the new APK is tested.
  • iOS: static config + Swift contract + resource parity + Jest coverage; iOS runtime/Xcode validation pending (no Mac available).
  • Weblate / translations repo / sync-translations.yml: untouched (PR5).
  • Follow-up: PR5 = full UI string migration + Weblate component; closes [Feature]: Add localization (i18n) support to the mobile app #1774.

Summary by CodeRabbit

  • New Features

    • Added English and Polish language support with system-language detection and selection in App Settings.
    • Localized navigation, save states, permissions, accessibility labels, widgets, and workout Live Activities.
    • Widgets now support locale-aware formatting, responsive layouts, resizing, and improved compact views.
    • Language changes synchronize across Android and iOS widgets and Live Activities.
  • Bug Fixes

    • Improved language-change recovery, fallback behavior, accessibility, and widget refresh reliability.
  • Tests

    • Expanded coverage for localization, accessibility, language synchronization, widgets, Live Activities, and translation consistency.

Dragonk added 8 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
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
Localize the existing Glance widgets (calorie + macro) through native
Android resources and keep them in sync with the effective app locale:

- values/widget_strings.xml (EN) + values-pl/widget_strings.xml (PL)
  with the exact strings the widget layouts/receivers use (Calories,
  Macros, kcal left, Protein/Carbs/Fat, search/scan shortcuts, preview
  placeholders). Same key set in both locales.
- CalorieWidget/MacroWidget Kotlin: resolve strings from resources and
  format numbers via locale-aware helpers so the active application
  locale (system, English override, or Polish override) is reflected.
- CalorieWidgetModule: expose widget language reload; useWidgetLanguageRefresh
  hooks into i18n 'languageChanged' and reloads both widget types with
  Promise.allSettled so one failing widget never blocks the other and a
  later language change can retry (no permanent dedupe on failure).
- App.tsx: single useWidgetLanguageRefresh() call (Android hook).
- withCalorieWidget: copy the locale resource folders into the generated
  project (idempotent).
- Tests: EN/PL string content + resource parity, locale selection,
  language-switch triggers reload of both widget types, partial reload
  failure isolation in both directions, both-fail handling, and retry
  after failure.

Assisted-by: Open WebUI
Localize WidgetKit and the Workout Live Activity through the shared
app-language contract from PR3:

- targets/widget/en.lproj + pl.lproj Localizable.strings with the exact
  widget strings (Calories, Protein/Carbs/Fat, remaining, Today, No
  data) — same key set in both locales, no full translation tree.
- Swift: SharedHelpers reads the shared locale from the App Group
  UserDefaults suite (canonical key, no second storage) and formats
  numbers via the locale; widgets/macroWidget resolve labels from
  Localizable.strings.
- useIOSWidgetLanguageRefresh: on i18n 'languageChanged' it writes the
  shared locale, then calls WidgetCenter.reloadAllTimelines(); the
  'synced' marker is only set after a successful write+reload, so a
  failed sync stays retryable on the next language change.
- Workout Live Activity: labels built from activeWorkout.liveActivity
  keys (Rest, Paused, Elapsed, Workout complete, Complete, +15s, Skip
  rest, Workout, Exercise, Set, of) with explicit English fallbacks.
  Custom exercise/workout names stay literal; action identifiers are
  unchanged. Language change updates the existing activity instance
  (same identity, no restart) via a subscription that is cleaned up on
  unmount; update failures are logged and retryable.
- App.tsx: useIOSWidgetLanguageRefresh() alongside the Android hook.
- Locale bundle: 12 new activeWorkout.liveActivity keys EN+PL.
- Tests: EN/PL widget resources + parity, Swift contract, shared-locale
  write + reloadAllTimelines, retry after write/reload failure, Live
  Activity EN/PL labels, literal custom names, unchanged action IDs,
  same-instance update on language change, retry after update failure,
  and subscription cleanup.

Assisted-by: Open WebUI
@coderabbitai

coderabbitai Bot commented Aug 8, 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, persisted language preferences, Android and iOS language synchronization, localized widgets and Live Activities, localized settings and navigation, and an i18n audit integrated into validation.

Changes

Mobile localization

Layer / File(s) Summary
Language management and app bootstrap
SparkyFitnessMobile/src/localization/..., SparkyFitnessMobile/src/hooks/..., SparkyFitnessMobile/App.tsx
Adds retryable i18n initialization, persisted language preferences, native language synchronization, bootstrap route selection, foreground synchronization, and localized navigation.
Settings, headers, and accessibility
SparkyFitnessMobile/src/screens/AppSettingsScreen.tsx, SparkyFitnessMobile/src/hooks/useScreenHeader.tsx, SparkyFitnessMobile/src/components/BottomSheetPicker.tsx
Adds language selection, localized Save and Saving labels, translated accessibility labels, and configurable picker hints.
Native and widget integration
SparkyFitnessMobile/targets/android-language/..., SparkyFitnessMobile/targets/android-widget/..., SparkyFitnessMobile/targets/widget/..., SparkyFitnessMobile/plugins/...
Adds native language bridges, locale-aware Android and iOS widgets, responsive widget layouts, Live Activity labels, and Expo plugin helpers.
Localization resources and audit
SparkyFitnessMobile/locales/..., SparkyFitnessMobile/src/localization/locales/..., SparkyFitnessMobile/scripts/..., SparkyFitnessMobile/package.json
Adds English and Polish resources, permission descriptions, locale and source validation, audit reporting, CLI output, and validation integration.
Contract and behavior tests
SparkyFitnessMobile/__tests__/...
Adds coverage for language initialization, migration, settings, widgets, Live Activities, accessibility, plugin wiring, locale resources, and audit behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: refactor

Suggested reviewers: codewithcj

Sequence Diagram(s)

sequenceDiagram
  participant AppContent
  participant useAppBootstrap
  participant AppLanguage
  participant i18n
  participant WidgetLanguageRefresh
  participant NativeWidgets
  AppContent->>useAppBootstrap: initialize language and route
  useAppBootstrap->>AppLanguage: initializeAppLanguage()
  AppLanguage->>i18n: initialize effective locale
  i18n-->>AppContent: language-ready state
  AppContent->>WidgetLanguageRefresh: observe language changes
  WidgetLanguageRefresh->>NativeWidgets: set locale override
  WidgetLanguageRefresh->>NativeWidgets: reload calorie and macro widgets
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.27% 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 the native-surface localization portion of issue #1774, including language handling, overrides, fallbacks, and localized Android and iOS surfaces.
Out of Scope Changes check ✅ Passed The changes support native localization, language synchronization, responsive widgets, Live Activity updates, and related tests without unrelated scope.
Title check ✅ Passed The title clearly summarizes the primary change: localization of native widgets and the live activity.
Description check ✅ Passed The description covers the required sections, implementation, testing steps, issue context, checklist, and explicitly documents pending device validation and screenshots.
✨ 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 8, 2026
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

PR Validation Results

Change Detection

  • 📱 Mobile changes detected

❌ Required Actions (1)

  • [MANDATORY for Mobile changes] Check the "Tested on device or emulator" checkbox.

⚠️ Recommendations (1)

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

Dragonk added 10 commits August 10, 2026 13:22
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
- 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
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
- 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
Merge the final accepted PR3 (b9f47e3) into the native-surface
localization branch. Final PR3 architecture wins for the app-language
model (Android 13+ LocaleManager, Android <=12 local preference,
transactional language changes, useAppBootstrap, final audit contract);
PR4-native widget/Live Activity work is preserved.

Assisted-by: Open WebUI
After the final PR3 app-language model, explicit en/pl is local to the RN
app on Android <=12, so Glance needs a widget-only locale override in a
dedicated SharedPreferences namespace (WidgetLocale.kt). Both widget-sync
hooks now react to preference changes AND effective-language changes:
'system' removes the override (iOS removes widgetLocale, Android removes
the native override), explicit en/pl persists it. The write/remove happens
before reloads; failures keep the state unapplied so the next signal
retries. Reloads stay isolated per widget.

Assisted-by: Open WebUI
Both provider XMLs switch from resizeMode="none" to
resizeMode="horizontal|vertical" with sane min/default constraints, and
both Glance widgets use SizeMode.Exact with LocalSize.current so the
layout genuinely uses the actual available size. The compact calorie
presentation splits the remaining phrase into caption + value lines
(Pozostało / 1 240 kcal) instead of ellipsizing it; the macro widget
stacks label above value at narrow widths so Węglowodany is never
truncated, and restores the richer horizontal layout when wider.
Preview/initial layouts mirror the compact layouts so picker fallbacks
are localization-safe. Source-contract tests cover resizeMode,
minResizeWidth/Height, SizeMode.Exact, LocalSize usage, and regression
to resizeMode="none".

Assisted-by: Open WebUI
iOS WidgetKit: localizedWidgetString now falls back explicit widget-locale
bundle -> native bundle -> English bundle -> stable readable map, never a
raw key. The icon-only action buttons expose localized accessibility
labels (widget.search_food / widget.scan_barcode) instead of relying on
SF Symbol names; EN/PL catalogs gain the two keys with identical sets.
Workout Live Activity: buildWorkoutLiveActivityLabels passes an explicit
English defaultValue per key so a missing PL key can never leak
activeWorkout.liveActivity.* into the label object (regression test
added). Removes the last as-any casts from the Live Activity test via
typed exercise snapshot / hydration listener interfaces. Updates stale
AppCompat-era App.tsx comment to the final PR3 app-language model.

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 performed

Review finished.

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: 10

🧹 Nitpick comments (12)
SparkyFitnessMobile/scripts/audit-mobile-i18n.mjs (1)

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

Print file and line for structural errors that have no key.

report.localeStructuralErrors also receives source-scan-error entries and suppression issues. Those entries carry file and line but no key, so the human report drops their location. CI triage then needs the JSON mode to find the offending file.

♻️ Proposed change to include the location
   if (report.localeStructuralErrors.length > 0) {
     console.log('\nLocale structural errors:');
     for (const e of report.localeStructuralErrors) {
-      console.log(`  - ${e.rule} ${e.key ? e.key : ''}: ${e.message}`);
+      const location = e.file ? ` (${e.file}${e.line ? `:${e.line}` : ''})` : '';
+      console.log(`  - ${e.rule} ${e.key ?? ''}${location}: ${e.message}`);
     }
   }
🤖 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 24 - 29,
Update the structural-error reporting loop in the audit script to include each
error’s file and line when its key is absent, while preserving the existing
key-based output for errors that have a key. Use the file and line fields from
each localeStructuralErrors entry so source-scan and suppression issues show
their location.
SparkyFitnessMobile/scripts/i18n-audit/sourceScanner.cjs (1)

220-226: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider moving the scanner state into a per-run context object.

findings, suppressionRecords, suppressionIssues, alertButtonTextProps, and toastTextProps are module-level. collectFindings clears only findings and suppressionIssues, and getAllSuppressionIssues reads state that belongs to the last run. Two overlapping runs in the same process would mix results. A per-run context passed to visitSourceFile and returned from collectFindings removes that coupling and makes the exported visitSourceFile safe to call directly.

This is not a defect today, because the CLI performs one run and Jest runs the tests in a file sequentially.

🤖 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 220 -
226, Refactor the scanner state around collectFindings and visitSourceFile into
a per-run context object instead of module-level findings, suppressionRecords,
suppressionIssues, alertButtonTextProps, and toastTextProps. Initialize and pass
the context through visitSourceFile, return the collected state from
collectFindings, and update getAllSuppressionIssues to consume that run’s
context so overlapping runs and direct visitSourceFile calls cannot share
results.
SparkyFitnessMobile/scripts/i18n-audit/core.cjs (1)

38-43: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Derive the default locale paths from rootDir.

sourceRoots defaults to rootDir, but enLocalePath and plLocalePath default to the production MOBILE_ROOT paths. If a caller passes only rootDir, the audit scans the custom source tree and validates the production locale files. The static-key check then compares two unrelated trees.

♻️ Proposed change to keep the defaults consistent
 function runAudit(options = {}) {
   const rootDir = options.rootDir || MOBILE_ROOT;
-  const enLocalePath = options.enLocalePath || EN_LOCALE_PATH;
-  const plLocalePath = options.plLocalePath || PL_LOCALE_PATH;
+  const localeDir = path.join(rootDir, 'src', 'localization', 'locales');
+  const enLocalePath = options.enLocalePath || path.join(localeDir, 'en', 'translation.json');
+  const plLocalePath = options.plLocalePath || path.join(localeDir, 'pl', 'translation.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/scripts/i18n-audit/core.cjs` around lines 38 - 43, Update
the default assignments for enLocalePath and plLocalePath in the audit setup to
derive from rootDir, while preserving explicitly provided locale paths. Use the
corresponding locale file locations under the selected rootDir so custom-root
runs validate the same tree scanned by sourceRoots.
SparkyFitnessMobile/__tests__/scripts/i18nAudit.test.ts (1)

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

Add coverage for the CLI entry point.

The tests cover runAudit, LocaleValidator, and collectFindings. No test covers scripts/audit-mobile-i18n.mjs. Its argument parsing, the warning for an output path without --json, the JSON file write, and the exit code 1 on errors are all untested. The exit code is the contract that the validation pipeline depends on.

Do you want me to generate a test that spawns the CLI against a fixture root and asserts the exit code and the JSON output?

🤖 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 884 -
921, Add a subprocess-based test for the scripts/audit-mobile-i18n.mjs CLI entry
point, using a fixture root to verify argument parsing, the warning when an
output path is supplied without --json, JSON output file creation and contents,
and exit code 1 when validation errors are found. Keep the assertions focused on
the CLI contract rather than the already-covered runAudit behavior.
SparkyFitnessMobile/scripts/i18n-audit/localeValidator.cjs (1)

78-81: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Accept formatted interpolation names in placeholderNames.

/\{\{(\w+)\}\}/g does not match {{count, number}}, {{date, datetime}}, or {{- name}}, so those placeholders are omitted from comparison. Capture the variable name while allowing an optional unescape prefix and format suffix.

🤖 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 78 -
81, Update placeholderNames to recognize formatted interpolation placeholders
such as {{count, number}} and {{date, datetime}}, while also allowing the
optional unescape prefix in forms like {{- name}}. Capture only the variable
name, continue sorting the results, and preserve the existing non-string
behavior.
SparkyFitnessMobile/src/localization/locales/en/translation.json (1)

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

Split "Set X of Y" fragments in both catalogs. Both catalogs expose set and setOf as separate fragments, so the caller must concatenate them. Concatenation fixes word order and blocks inflection and plural rules in future locales. Replace the fragments with one interpolated key in both catalogs and update the Live Activity label builder accordingly.

  • SparkyFitnessMobile/src/localization/locales/en/translation.json#L38-L39: replace set and setOf with "setProgress": "Set {{current}} of {{total}}".
  • SparkyFitnessMobile/src/localization/locales/pl/translation.json#L38-L39: replace set and setOf with "setProgress": "Seria {{current}} z {{total}}".
🤖 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/locales/en/translation.json` around
lines 38 - 39, Replace the separate set and setOf entries with the interpolated
setProgress key in
SparkyFitnessMobile/src/localization/locales/en/translation.json lines 38-39 and
SparkyFitnessMobile/src/localization/locales/pl/translation.json lines 38-39,
using the specified English and Polish wording. Update the Live Activity label
builder to call setProgress with current and total values instead of
concatenating separate translations.
SparkyFitnessMobile/src/services/CalorieWidgetBridge.ts (1)

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

Derive WidgetLocaleOverride from the app-language type instead of duplicating the union.

useWidgetLanguageRefresh passes LanguagePreference values excluding 'system' into setWidgetLocale. The union is declared twice. When a third language is added, this local copy can drift and the mismatch appears only at the call site. Derive the type from the localization module so both stay aligned.

♻️ Proposed change
-export type WidgetLocaleOverride = 'en' | 'pl';
+import type { LanguagePreference } from '../localization';
+
+export type WidgetLocaleOverride = Exclude<LanguagePreference, 'system'>;

Based on learnings: "Prefer schemas, constants, date/timezone helpers, and types from @workspace/shared over local duplicates."

🤖 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/services/CalorieWidgetBridge.ts` around lines 3 - 8,
Update WidgetLocaleOverride in CalorieWidgetNativeModule to derive from the
localization module’s LanguagePreference type while excluding the 'system'
value, and remove the duplicated literal union. Ensure useWidgetLanguageRefresh
and setWidgetLocale continue accepting only concrete app languages.

Source: Coding guidelines

SparkyFitnessMobile/plugins/withCalorieWidget.ts (1)

50-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider failing fast when the manifest application node is missing.

addWidgetReceivers returns silently when application is undefined. The caller at Line 201 ignores the return value. A missing application node then produces a build with no widget receivers and no error. The plugin already throws for a missing config.android.package, so an explicit error keeps the failure modes consistent.

♻️ Proposed change
 export function addWidgetReceivers(
   application: AndroidManifestApplication | undefined,
-): AndroidManifestApplication | undefined {
-  if (!application) return application;
+): AndroidManifestApplication {
+  if (!application) {
+    throw new Error(
+      '[withCalorieWidget] AndroidManifest has no <application> node; widget receivers cannot be registered.',
+    );
+  }

Note: the existing test at SparkyFitnessMobile/__tests__/config/withCalorieWidget.test.ts:104 asserts the undefined no-op behavior, so update that test if you take this change.

🤖 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/plugins/withCalorieWidget.ts` around lines 50 - 53,
Update addWidgetReceivers to throw an explicit error when the Android manifest
application node is undefined instead of returning silently. Preserve the
existing receiver-insertion behavior for valid applications, and update the test
covering the undefined application case to expect the failure.
SparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/CalorieWidgetModule.kt.tmpl (1)

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

Both reload loops discard the failure cause and report only a count. catch (ignored: Exception) drops the throwable, so the rejection carries no information about why a widget instance failed to update. Field diagnosis of a partial reload failure becomes guesswork. Keep the first failure and attach it as the cause of the rejection.

  • SparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/CalorieWidgetModule.kt.tmpl#L106-L118: capture the first exception in a local var firstFailure: Exception? and pass it as the cause when rejecting with E_RELOAD_FAILED.
  • SparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/CalorieWidgetModule.kt.tmpl#L132-L144: apply the same change to the macro reload loop.
♻️ Proposed change (calorie loop; mirror it for the macro loop)
                 var failures = 0
+                var firstFailure: Exception? = null
                 manager.getGlanceIds(CalorieWidget::class.java).forEach { id ->
                     try {
                         widget.update(ctx, id)
-                    } catch (ignored: Exception) {
+                    } catch (e: Exception) {
                         failures++
+                        if (firstFailure == null) firstFailure = e
                     }
                 }
                 if (failures > 0) {
-                    promise.reject("E_RELOAD_FAILED", RuntimeException("$failures calorie widget instance(s) failed to update"))
+                    promise.reject(
+                        "E_RELOAD_FAILED",
+                        RuntimeException("$failures calorie widget instance(s) failed to update", firstFailure),
+                    )
                 } else {

Note: SparkyFitnessMobile/__tests__/config/widgetResourceContract.test.ts:233 asserts catch (ignored: Exception) is present. Update that assertion if you take this change.

🤖 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/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/CalorieWidgetModule.kt.tmpl`
around lines 106 - 118, Update both reload loops in CalorieWidgetModule.kt.tmpl
(lines 106-118 and 132-144) to retain the first caught Exception in a
firstFailure variable, then pass it as the rejection cause alongside the
existing E_RELOAD_FAILED code and failure count. Update the assertion in
SparkyFitnessMobile/__tests__/config/widgetResourceContract.test.ts at lines 233
as needed to match the new catch behavior.
SparkyFitnessMobile/__tests__/hooks/useWidgetLanguageRefresh.test.tsx (1)

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

The unhandled-rejection assertion cannot detect a real unhandled rejection; remove it or make it effective.

Node emits unhandledRejection on a later turn of the event loop. flushReload awaits three microtasks, so expect(onUnhandledRejection).not.toHaveBeenCalled() at Line 270 always passes, even if the hook did leak a rejection. The test gives false confidence.

originalOnUnhandled at Line 263 is never used; Line 281 only voids it. Remove that binding.

The remaining addLog assertions already prove both rejections were handled. Consider dropping the listener plumbing and keeping only those assertions, or await a macrotask before asserting.

♻️ Proposed change
-    const onUnhandledRejection = jest.fn();
-    const originalOnUnhandled = process.on.bind(process);
-    process.on('unhandledRejection', onUnhandledRejection);
-
     renderHook(() => useWidgetLanguageRefresh());
 
     await flushReload();
 
-    expect(onUnhandledRejection).not.toHaveBeenCalled();
     expect(mockAddLog).toHaveBeenCalledWith(
       '[useWidgetLanguageRefresh] Macro widget reload failed',
       'ERROR',
     );
-
-    process.removeListener('unhandledRejection', onUnhandledRejection);
-    void originalOnUnhandled;
   });
🤖 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__/hooks/useWidgetLanguageRefresh.test.tsx` around
lines 257 - 282, Update the test case “settles both reload rejections without an
unhandled rejection” to remove the ineffective process unhandledRejection
listener and the unused originalOnUnhandled binding. Retain the existing
mockAddLog assertions, which verify both reload failures are handled, and keep
the reload flushing behavior unchanged.
SparkyFitnessMobile/__tests__/config/widgetResourceContract.test.ts (2)

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

Make extractStringResources tolerate extra attributes on <string>.

The regex requires name to be the only attribute. Android string resources often carry formatted="false" when a value mixes positional arguments with a literal %. A resource written as <string name="widget_grams" formatted="false"> is skipped. The key then disappears from both parsed sets, and the parity test at Line 101 still passes.

♻️ Proposed change
-  const regex = /<string\s+name="([^"]+)">([^<]*)<\/string>/g;
+  const regex = /<string\s+[^>]*name="([^"]+)"[^>]*>([^<]*)<\/string>/g;
🤖 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/widgetResourceContract.test.ts` around
lines 28 - 39, Update extractStringResources so its XML matching accepts
additional attributes on <string> elements while still capturing the name
attribute and text value. Ensure resources such as formatted="false" are
included, preserving the existing apostrophe and entity unescaping behavior.

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

Replace the character-distance assertions with structural XML checks.

The calorie test uses [\s\S]{0,200} and the macro test uses gramsCarbsIndex - carbsIndex < 900; both break on reformatting or comments without changing stacked layout. Parse the layouts and assert that the carbs label and grams value are in sibling subgroups below a vertical container. fast-xml-parser is in the lockfile, but add/update a mobile package dependency before importing it.

🤖 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/widgetResourceContract.test.ts` around
lines 402 - 428, Replace the substring-distance checks in the calorie and macro
tests with parsed structural XML assertions. Add fast-xml-parser to the mobile
package dependencies, parse both layouts, and verify the relevant caption/value
elements are arranged in sibling subgroups under the vertical container,
preserving the existing clipping-prevention expectations without relying on
formatting or comments.
🤖 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__/hooks/useIOSWidgetLanguageRefresh.test.tsx`:
- Around line 298-341: Reset mock implementations between tests for
SparkyFitnessMobile/__tests__/hooks/useIOSWidgetLanguageRefresh.test.tsx lines
298-341 by updating the relevant beforeEach to reset mockReload and
storage/bridge mocks, or replace the persistent implementations with Once
variants; ensure the reloadFailure closures cannot leak into the test at lines
343 onward. Apply the same beforeEach reset fix to the persistent
mockRejectedValue calls in
SparkyFitnessMobile/__tests__/hooks/useWidgetLanguageRefresh.test.tsx lines
257-260; no direct changes are required in its later affected tests.

In `@SparkyFitnessMobile/__tests__/hooks/useScreenHeader.test.tsx`:
- Around line 139-145: Add onPress?: () => void to the item fixture type used by
nativeRightItem(), preserving the existing label and accessibilityLabel fields
so subsequent item?.onPress?.() calls typecheck correctly.

In `@SparkyFitnessMobile/__tests__/hooks/useWidgetLanguageRefresh.test.tsx`:
- Around line 43-44: Update the test setup around beforeEach in
useWidgetLanguageRefresh.test.tsx to reset mock implementations as well as call
history between tests. Use the appropriate Jest reset mechanism for
module-factory jest.fn() mocks so the mockRejectedValue configurations from the
affected tests do not persist into subsequent tests.

In `@SparkyFitnessMobile/scripts/i18n-audit/sourceScanner.cjs`:
- Around line 495-501: Update collectFindings to handle each sourceRoot scan
failure without aborting the audit: catch errors from walkFiles, record a
source-scan-error in the rootErrors collection using the existing error shape,
and continue processing remaining roots. Include rootErrors in the returned
errors array so missing or unreadable roots preserve the documented fail-closed
report behavior.

In `@SparkyFitnessMobile/src/hooks/useWidgetLanguageRefresh.ts`:
- Around line 49-61: Serialize syncWidgets executions so only one native
widget-language update runs at a time, preventing overlapping writes from
completing out of order. After each awaited native operation, re-read the
current preference and resolved language before updating lastAppliedRef, and
retry or continue syncing when the desired state changed during the run;
preserve the existing deduplication behavior for unchanged state.

In `@SparkyFitnessMobile/src/localization/appLanguage.ts`:
- Around line 46-57: Update hydratePreferences so its promise also settles when
persist.rehydrate() fails, rather than waiting only for onFinishHydration. Reuse
the returned rehydration promise to handle rejection and allow
initializeAppLanguage, setAppLanguagePreference, and syncAppLanguageFromSystem
to continue with default preferences; preserve the existing successful-hydration
cleanup.

In `@SparkyFitnessMobile/src/localization/i18n.ts`:
- Around line 59-82: Update initializeI18n so it rethrows or otherwise rejects
with the initialization error when both the requested language and English
fallback fail, instead of resolving with an uninitialized i18n instance; retain
resetting initPromise for retry. In the appLanguage flow, guard
language-dependent reads and changeLanguage calls with i18n.isInitialized so
changeLanguage is skipped when initialization remains incomplete.

In `@SparkyFitnessMobile/src/screens/AppSettingsScreen.tsx`:
- Around line 104-118: Localize the remaining hard-coded visible labels in
AppSettingsScreen, including Theme, Liquid Glass navigation, Notifications,
Haptic Feedback, and Camera shutter. Add matching keys and translations to the
locale resources, then replace each rendered English string with t(...) while
preserving the existing fallback text and behavior.

In
`@SparkyFitnessMobile/targets/android-language/kotlin/com/sparkyapps/sparkyfitness/language/AppLanguageModule.kt`:
- Around line 48-58: Update the language-setting flow around localeManager() and
applicationLocales so it explicitly obtains and validates a LocaleManager before
writing locales. If the manager is unavailable or has an incompatible type,
reject with E_SET_LANGUAGE_FAILED instead of resolving; only call
promise.resolve(null) after the locale write succeeds, while preserving the
existing exception rejection path.

In
`@SparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/CalorieWidget.kt.tmpl`:
- Around line 249-256: Extract the duplicated currentLocale and formatInt logic
into shared formatWidgetInt in WidgetLocale.kt.tmpl, using one consistent
rounding rule (roundToLong). In CalorieWidget.kt.tmpl lines 249-256 and
MacroWidget.kt.tmpl lines 424-431, remove the local helpers and call
formatWidgetInt for remaining values. Update widgetResourceContract.test.ts line
219 to validate the shared helper location.

---

Nitpick comments:
In `@SparkyFitnessMobile/__tests__/config/widgetResourceContract.test.ts`:
- Around line 28-39: Update extractStringResources so its XML matching accepts
additional attributes on <string> elements while still capturing the name
attribute and text value. Ensure resources such as formatted="false" are
included, preserving the existing apostrophe and entity unescaping behavior.
- Around line 402-428: Replace the substring-distance checks in the calorie and
macro tests with parsed structural XML assertions. Add fast-xml-parser to the
mobile package dependencies, parse both layouts, and verify the relevant
caption/value elements are arranged in sibling subgroups under the vertical
container, preserving the existing clipping-prevention expectations without
relying on formatting or comments.

In `@SparkyFitnessMobile/__tests__/hooks/useWidgetLanguageRefresh.test.tsx`:
- Around line 257-282: Update the test case “settles both reload rejections
without an unhandled rejection” to remove the ineffective process
unhandledRejection listener and the unused originalOnUnhandled binding. Retain
the existing mockAddLog assertions, which verify both reload failures are
handled, and keep the reload flushing behavior unchanged.

In `@SparkyFitnessMobile/__tests__/scripts/i18nAudit.test.ts`:
- Around line 884-921: Add a subprocess-based test for the
scripts/audit-mobile-i18n.mjs CLI entry point, using a fixture root to verify
argument parsing, the warning when an output path is supplied without --json,
JSON output file creation and contents, and exit code 1 when validation errors
are found. Keep the assertions focused on the CLI contract rather than the
already-covered runAudit behavior.

In `@SparkyFitnessMobile/plugins/withCalorieWidget.ts`:
- Around line 50-53: Update addWidgetReceivers to throw an explicit error when
the Android manifest application node is undefined instead of returning
silently. Preserve the existing receiver-insertion behavior for valid
applications, and update the test covering the undefined application case to
expect the failure.

In `@SparkyFitnessMobile/scripts/audit-mobile-i18n.mjs`:
- Around line 24-29: Update the structural-error reporting loop in the audit
script to include each error’s file and line when its key is absent, while
preserving the existing key-based output for errors that have a key. Use the
file and line fields from each localeStructuralErrors entry so source-scan and
suppression issues show their location.

In `@SparkyFitnessMobile/scripts/i18n-audit/core.cjs`:
- Around line 38-43: Update the default assignments for enLocalePath and
plLocalePath in the audit setup to derive from rootDir, while preserving
explicitly provided locale paths. Use the corresponding locale file locations
under the selected rootDir so custom-root runs validate the same tree scanned by
sourceRoots.

In `@SparkyFitnessMobile/scripts/i18n-audit/localeValidator.cjs`:
- Around line 78-81: Update placeholderNames to recognize formatted
interpolation placeholders such as {{count, number}} and {{date, datetime}},
while also allowing the optional unescape prefix in forms like {{- name}}.
Capture only the variable name, continue sorting the results, and preserve the
existing non-string behavior.

In `@SparkyFitnessMobile/scripts/i18n-audit/sourceScanner.cjs`:
- Around line 220-226: Refactor the scanner state around collectFindings and
visitSourceFile into a per-run context object instead of module-level findings,
suppressionRecords, suppressionIssues, alertButtonTextProps, and toastTextProps.
Initialize and pass the context through visitSourceFile, return the collected
state from collectFindings, and update getAllSuppressionIssues to consume that
run’s context so overlapping runs and direct visitSourceFile calls cannot share
results.

In `@SparkyFitnessMobile/src/localization/locales/en/translation.json`:
- Around line 38-39: Replace the separate set and setOf entries with the
interpolated setProgress key in
SparkyFitnessMobile/src/localization/locales/en/translation.json lines 38-39 and
SparkyFitnessMobile/src/localization/locales/pl/translation.json lines 38-39,
using the specified English and Polish wording. Update the Live Activity label
builder to call setProgress with current and total values instead of
concatenating separate translations.

In `@SparkyFitnessMobile/src/services/CalorieWidgetBridge.ts`:
- Around line 3-8: Update WidgetLocaleOverride in CalorieWidgetNativeModule to
derive from the localization module’s LanguagePreference type while excluding
the 'system' value, and remove the duplicated literal union. Ensure
useWidgetLanguageRefresh and setWidgetLocale continue accepting only concrete
app languages.

In
`@SparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/CalorieWidgetModule.kt.tmpl`:
- Around line 106-118: Update both reload loops in CalorieWidgetModule.kt.tmpl
(lines 106-118 and 132-144) to retain the first caught Exception in a
firstFailure variable, then pass it as the rejection cause alongside the
existing E_RELOAD_FAILED code and failure count. Update the assertion in
SparkyFitnessMobile/__tests__/config/widgetResourceContract.test.ts at lines 233
as needed to match the new catch 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: 263439ee-e8aa-4fd9-8034-bad7ab5b8d64

📥 Commits

Reviewing files that changed from the base of the PR and between 2241581 and cee1f9c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (70)
  • SparkyFitnessMobile/AGENTS.md
  • SparkyFitnessMobile/App.tsx
  • SparkyFitnessMobile/__mocks__/expo-winter.js
  • SparkyFitnessMobile/__tests__/config/iosWidgetResources.test.ts
  • SparkyFitnessMobile/__tests__/config/iosWidgetSwiftContract.test.ts
  • SparkyFitnessMobile/__tests__/config/nativeLocales.test.ts
  • SparkyFitnessMobile/__tests__/config/widgetResourceContract.test.ts
  • SparkyFitnessMobile/__tests__/config/withAppLanguage.test.ts
  • SparkyFitnessMobile/__tests__/config/withCalorieWidget.test.ts
  • SparkyFitnessMobile/__tests__/hooks/useAppBootstrap.test.tsx
  • SparkyFitnessMobile/__tests__/hooks/useAppLanguageForegroundSync.test.tsx
  • SparkyFitnessMobile/__tests__/hooks/useAppStartup.test.ts
  • SparkyFitnessMobile/__tests__/hooks/useIOSWidgetLanguageRefresh.test.tsx
  • SparkyFitnessMobile/__tests__/hooks/useScreenHeader.test.tsx
  • SparkyFitnessMobile/__tests__/hooks/useWidgetLanguageRefresh.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__/services/workoutLiveActivity.test.ts
  • SparkyFitnessMobile/__tests__/services/workoutLiveActivityLabels.test.ts
  • SparkyFitnessMobile/__tests__/services/workoutLiveActivityLayoutContract.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/plugins/withCalorieWidget.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/useIOSWidgetLanguageRefresh.ts
  • SparkyFitnessMobile/src/hooks/useScreenHeader.tsx
  • SparkyFitnessMobile/src/hooks/useWidgetLanguageRefresh.ts
  • 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/CalorieWidgetBridge.ts
  • SparkyFitnessMobile/src/services/WorkoutLiveActivityLayout.tsx
  • SparkyFitnessMobile/src/services/appLanguageNative.ts
  • SparkyFitnessMobile/src/services/workoutLiveActivity.ios.ts
  • SparkyFitnessMobile/src/services/workoutLiveActivityLabels.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
  • SparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/CalorieWidget.kt.tmpl
  • SparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/CalorieWidgetModule.kt.tmpl
  • SparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/MacroWidget.kt.tmpl
  • SparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/WidgetLocale.kt.tmpl
  • SparkyFitnessMobile/targets/android-widget/res/layout/sparky_macro_widget_initial_layout.xml
  • SparkyFitnessMobile/targets/android-widget/res/layout/sparky_widget_initial_layout.xml
  • SparkyFitnessMobile/targets/android-widget/res/values-pl/widget_strings.xml
  • SparkyFitnessMobile/targets/android-widget/res/values/widget_strings.xml
  • SparkyFitnessMobile/targets/android-widget/res/xml/sparky_calorie_widget_info.xml
  • SparkyFitnessMobile/targets/android-widget/res/xml/sparky_macro_widget_info.xml
  • SparkyFitnessMobile/targets/widget/SharedHelpers.swift
  • SparkyFitnessMobile/targets/widget/en.lproj/Localizable.strings
  • SparkyFitnessMobile/targets/widget/macroWidget.swift
  • SparkyFitnessMobile/targets/widget/pl.lproj/Localizable.strings
  • SparkyFitnessMobile/targets/widget/widgets.swift

Comment thread SparkyFitnessMobile/__tests__/hooks/useIOSWidgetLanguageRefresh.test.tsx Outdated
Comment thread SparkyFitnessMobile/__tests__/hooks/useScreenHeader.test.tsx
Comment on lines +495 to +501
function collectFindings(rootDir, sourceRoots) {
findings.length = 0;
suppressionIssues.clear();
const sourceFilesSet = new Set();
for (const sourceRoot of sourceRoots) {
walkFiles(sourceRoot, rootDir, sourceFilesSet);
}

@coderabbitai coderabbitai Bot Aug 10, 2026

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

A missing source root aborts the audit with an unhandled ENOENT.

walkFiles calls fs.readdirSync on each entry in sourceRoots without an existence check, and this loop sits outside the per-file try/catch at lines 515-526. If a source root does not exist, the audit throws before it builds a report. The documented fail-closed behavior records a source-scan-error instead.

🐛 Proposed fix to record a scan error for an unreadable source root
 function collectFindings(rootDir, sourceRoots) {
   findings.length = 0;
   suppressionIssues.clear();
   const sourceFilesSet = new Set();
+  const rootErrors = [];
   for (const sourceRoot of sourceRoots) {
-    walkFiles(sourceRoot, rootDir, sourceFilesSet);
+    try {
+      walkFiles(sourceRoot, rootDir, sourceFilesSet);
+    } catch (err) {
+      const message = err instanceof Error ? err.message : String(err);
+      rootErrors.push({
+        rule: SOURCE_SCAN_ERROR_RULE,
+        file: getFileRelativePath(sourceRoot, rootDir),
+        message: `Failed to walk source root: ${message}`,
+      });
+    }
   }

Then include rootErrors in the returned errors array:

   return {
     findings: findings.map((f) => ({ ...f })),
-    errors: scanErrors,
+    errors: [...rootErrors, ...scanErrors],
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function collectFindings(rootDir, sourceRoots) {
findings.length = 0;
suppressionIssues.clear();
const sourceFilesSet = new Set();
for (const sourceRoot of sourceRoots) {
walkFiles(sourceRoot, rootDir, sourceFilesSet);
}
function collectFindings(rootDir, sourceRoots) {
findings.length = 0;
suppressionIssues.clear();
const sourceFilesSet = new Set();
const rootErrors = [];
for (const sourceRoot of sourceRoots) {
try {
walkFiles(sourceRoot, rootDir, sourceFilesSet);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
rootErrors.push({
rule: SOURCE_SCAN_ERROR_RULE,
file: getFileRelativePath(sourceRoot, rootDir),
message: `Failed to walk source root: ${message}`,
});
}
}
🤖 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 495 -
501, Update collectFindings to handle each sourceRoot scan failure without
aborting the audit: catch errors from walkFiles, record a source-scan-error in
the rootErrors collection using the existing error shape, and continue
processing remaining roots. Include rootErrors in the returned errors array so
missing or unreadable roots preserve the documented fail-closed report behavior.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The i18n audit script is final PR3-owned infrastructure (added in PR3, accepted at b9f47e3). PR4's scope is native surfaces only; modifying the audit tool would diverge from the accepted PR3 architecture. Flagged for PR3/PR5 follow-up.

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.

@Dragonk, acknowledged. This finding is outside the scope of this PR and should not require a change here.

Would you like me to create a GitHub follow-up issue for the PR3/PR5 audit-tool work? It can track fail-closed handling for missing or unreadable sourceRoots in collectFindings.

(ᵕ—ᴗ—) 🐇

You are interacting with an AI system.

Comment thread SparkyFitnessMobile/src/hooks/useWidgetLanguageRefresh.ts Outdated
Comment thread SparkyFitnessMobile/src/localization/appLanguage.ts
Comment thread SparkyFitnessMobile/src/localization/i18n.ts
Comment thread SparkyFitnessMobile/src/screens/AppSettingsScreen.tsx
Comment on lines +48 to +58
try {
val locales = if (normalized == null) {
LocaleList.getEmptyLocaleList()
} else {
LocaleList.forLanguageTags(normalized)
}
localeManager()?.applicationLocales = locales
promise.resolve(null)
} catch (error: Exception) {
promise.reject("E_SET_LANGUAGE_FAILED", error)
}

@coderabbitai coderabbitai Bot Aug 10, 2026

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Reject when LocaleManager is unavailable instead of resolving success.

On API 33+ the safe call localeManager()?.applicationLocales = locales skips the write when getSystemService(Context.LOCALE_SERVICE) returns null or a non-LocaleManager. The promise still resolves. ensureNativeLanguage in SparkyFitnessMobile/src/localization/appLanguage.ts then treats the write as successful and commits the store preference, so the store and the platform locale disagree and no error is logged.

🛠️ Proposed fix
         try {
             val locales = if (normalized == null) {
                 LocaleList.getEmptyLocaleList()
             } else {
                 LocaleList.forLanguageTags(normalized)
             }
-            localeManager()?.applicationLocales = locales
+            val manager = localeManager()
+            if (manager == null) {
+                promise.reject("E_LOCALE_MANAGER_UNAVAILABLE", "LocaleManager is not available")
+                return
+            }
+            manager.applicationLocales = locales
             promise.resolve(null)
         } catch (error: Exception) {
             promise.reject("E_SET_LANGUAGE_FAILED", error)
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
val locales = if (normalized == null) {
LocaleList.getEmptyLocaleList()
} else {
LocaleList.forLanguageTags(normalized)
}
localeManager()?.applicationLocales = locales
promise.resolve(null)
} catch (error: Exception) {
promise.reject("E_SET_LANGUAGE_FAILED", error)
}
try {
val locales = if (normalized == null) {
LocaleList.getEmptyLocaleList()
} else {
LocaleList.forLanguageTags(normalized)
}
val manager = localeManager()
if (manager == null) {
promise.reject("E_LOCALE_MANAGER_UNAVAILABLE", "LocaleManager is not available")
return
}
manager.applicationLocales = locales
promise.resolve(null)
} catch (error: Exception) {
promise.reject("E_SET_LANGUAGE_FAILED", error)
}
🤖 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/targets/android-language/kotlin/com/sparkyapps/sparkyfitness/language/AppLanguageModule.kt`
around lines 48 - 58, Update the language-setting flow around localeManager()
and applicationLocales so it explicitly obtains and validates a LocaleManager
before writing locales. If the manager is unavailable or has an incompatible
type, reject with E_SET_LANGUAGE_FAILED instead of resolving; only call
promise.resolve(null) after the locale write succeeds, while preserving the
existing exception rejection path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

AppLanguageModule.kt is final PR3's native per-app-language bridge (Android 13+ LocaleManager). PR4 must preserve final PR3's native architecture; changing the module would diverge from the accepted PR3. Flagged for PR3 follow-up.

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.

@Dragonk, understood. I will leave this finding unresolved in PR4 because this module is part of the accepted PR3 native architecture.

The PR3 follow-up should ensure that setApplicationLanguage rejects when LocaleManager is unavailable. If no follow-up issue exists, would you like me to create one?

You are interacting with an AI system.

- Serialize widget-sync runs in both hooks: a later preference change can
  no longer lose to an in-flight write (two overlapping runs could leave
  the native override in the older language while the ref claimed the
  newer one, with no retry path). Queued runs re-read the store at
  execution time so rapid signals converge on the newest state without a
  reload storm; covered by serialization regression tests.
- Reset module-factory mock implementations in both hook test files so
  mockRejectedValue/mockImplementation from one test cannot leak into a
  later test.
- Extract one shared locale-aware integer formatter (formatWidgetInt,
  roundToLong) in WidgetLocale.kt and drop the duplicated per-widget
  copies so both widgets render the same remaining value identically.

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 performed

Review finished.

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.

…uts to height

BLOCKER 1 - Android 13+ App Languages are authoritative:
WidgetLocale now short-circuits on API 33+ (TIRAMISU): localizedContext()
always returns the original native context, override() never returns a
stored value, and setOverride() clears any stale widget-only override for
every request (en/pl/system). A stale <=12 'pl' preference can no longer
keep the widget Polish after the user changes the native App Language to
English while the app is closed. Android <=12 keeps the widget-only
override (en/pl) unchanged, without AppCompat and without calling the PR3
native module.

BLOCKER 2 - layouts respond to BOTH width and height:
Both Glance widgets branch on a width flag AND a height class derived
from LocalSize.current. Calorie: SHORT (<100dp) renders value + progress
only (actions omitted rather than clipping 'Pozostało ... kcal'); NORMAL
and TALL keep the full compact content and both actions. Macro: SHORT
(<170dp) renders the three inline macro rows only (header + actions
omitted); NORMAL keeps header + inline rows + actions; TALL on narrow
uses stacked rows so 'Węglowodany' never truncates. Provider minimums are
now honest: calorie 110x56dp (SHORT layout fits), macro 150x110dp (inline
rows fit at 12sp). Source-contract tests cover the API 32/33+ override
contract, OS-upgrade stale-override safety, width+height branches, and
the honest minimums.

Assisted-by: Open WebUI
@Dragonk

Dragonk commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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 (1)
SparkyFitnessMobile/__tests__/config/widgetResourceContract.test.ts (1)

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

The test asserts the opposite of its name.

The title states "no header/action requirement", but the body requires CalorieHeader(context = widgetContext to be present in the source. The assertion also cannot prove that the header is skipped at SHORT, or that MacroRows renders at the minimum height, because both checks only test for substring presence.

Assert the conditional structure instead, so the test fails if the header becomes unconditional.

♻️ Proposed change: assert the SHORT guard around the header
-    it('keeps the primary macro rows at minimum height (no header/action requirement)', () => {
+    it('renders the macro rows unconditionally and gates the header on !short', () => {
       const src = fs.readFileSync(path.join(KOTLIN_ROOT, 'MacroWidget.kt.tmpl'), 'utf8');
       // The short layout must still render all three macro rows with progress.
       expect(src).toMatch(/MacroRows\(/);
-      // The kcal header is skipped when short (it would overflow 110dp).
-      const headerCall = src.match(/CalorieHeader\(context = widgetContext/);
-      expect(headerCall).not.toBeNull();
+      // The kcal header is skipped when short (it would overflow 110dp).
+      expect(src).toMatch(
+        /if \(!short\) \{\s*CalorieHeader\(context = widgetContext/,
+      );
     });
🤖 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/widgetResourceContract.test.ts` around
lines 362 - 369, Update the test case around the “keeps the primary macro rows
at minimum height” assertion so its expectations match the title: verify the
short-layout conditional structure, ensuring `MacroRows` remains rendered while
`CalorieHeader` is guarded against `SHORT` rather than unconditional. Replace
the substring-presence check with an assertion that fails when the header is
rendered in the short layout.
🤖 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/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/CalorieWidget.kt.tmpl`:
- Around line 143-222: The SHORT layout in the when branch uses excessive
vertical space for the 11.sp caption, 16.sp value, spacers, and progress bar at
minHeight 56dp. Reduce the SHORT-specific text and spacing dimensions, or
increase the widget’s android:minResizeHeight, so the value and progress bar
remain fully visible at font scales 1.0 and 1.3; preserve the existing non-SHORT
layout.

In
`@SparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/MacroWidget.kt.tmpl`:
- Around line 139-141: Update the macro Column modifier near the heightClass
check to use GlanceModifier.fillMaxSize() instead of fillMaxWidth(), ensuring
TALL widgets allocate height to defaultWeight() spacers and place the action row
at the bottom.

---

Nitpick comments:
In `@SparkyFitnessMobile/__tests__/config/widgetResourceContract.test.ts`:
- Around line 362-369: Update the test case around the “keeps the primary macro
rows at minimum height” assertion so its expectations match the title: verify
the short-layout conditional structure, ensuring `MacroRows` remains rendered
while `CalorieHeader` is guarded against `SHORT` rather than unconditional.
Replace the substring-presence check with an assertion that fails when the
header is rendered in the short layout.
🪄 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: 0d574ab0-66ef-46db-b235-67775a85055f

📥 Commits

Reviewing files that changed from the base of the PR and between f8024e3 and 79c950d.

📒 Files selected for processing (7)
  • SparkyFitnessMobile/__tests__/config/widgetResourceContract.test.ts
  • SparkyFitnessMobile/src/hooks/useWidgetLanguageRefresh.ts
  • SparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/CalorieWidget.kt.tmpl
  • SparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/MacroWidget.kt.tmpl
  • SparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/WidgetLocale.kt.tmpl
  • SparkyFitnessMobile/targets/android-widget/res/xml/sparky_calorie_widget_info.xml
  • SparkyFitnessMobile/targets/android-widget/res/xml/sparky_macro_widget_info.xml
🚧 Files skipped from review as they are similar to previous changes (1)
  • SparkyFitnessMobile/src/hooks/useWidgetLanguageRefresh.ts

@Dragonk

Dragonk commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 11, 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.

- Calorie SHORT typography is more compact (10sp caption / 15sp value /
  3dp progress / 3dp spacer) and the honest provider minimum is raised to
  60dp so the value + progress stay fully visible at font scales 1.0-1.3
  instead of clipping at the old 56dp.
- Macro outer Column now uses fillMaxSize() instead of fillMaxWidth() so
  the defaultWeight() spacers actually distribute TALL vertical space and
  the action row sits at the bottom instead of hugging the content.
- Source-contract tests updated/added for the 60dp minimum, the macro
  fillMaxSize contract, and the compact SHORT typography.

Assisted-by: Open WebUI
@Dragonk

Dragonk commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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.

@Dragonk

Dragonk commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 11, 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.

Physical Android/HyperOS validation showed the compact macro layout lost
the remaining-kcal context and the compact calorie layout pinned content
to the top. New compact contract:

- Macro SHORT now keeps the kcal header + all three macro rows with
  progress and drops only the action row; the header is rendered for
  every height class instead of being guarded by `if (!short)`.
- Macro SHORT uses compact typography (10sp caption / 14sp value /
  11sp rows / 3dp row spacing / 4dp header gap) and vertical balancing.
- Macro provider minimum raised to 140dp (minHeight/minResizeHeight) so
  the compact information block fits honestly at font scales up to 1.3.
- Calorie SHORT is vertically balanced: the caption/value/progress core
  sits between two flexible spacers instead of being pinned to the top.
- Source-contract tests updated to the new compact contract (header
  present in SHORT, actions guarded by !short, honest 140dp minimum,
  vertical balancing in both widgets).

Assisted-by: Open WebUI
@Dragonk

Dragonk commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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.

Physical Android comparison against the pre-PR4 widgets established a new
authoritative layout contract: the classic 2x1 calorie and 2x2 macro
compositions are the default visual language, and resizing adapts the SAME
design instead of switching into unrelated SHORT/NORMAL/TALL compositions.

- Calorie: restore classic one-line localized kcal heading (18sp wide /
  14sp narrow), 8dp progress, flexible spacer, and the always-visible 32dp
  search/scan action row; 12dp padding; provider back to the classic
  110x40 2x1 footprint with resize minimums (resizeMode horizontal|vertical).
- Macro: restore the classic Box(contentAlignment=Center) with a
  content-sized fillMaxWidth() column (one-line kcal header + three inline
  macro rows with progress); provider back to the classic 110x110 2x2
  footprint; shortcut actions become an expansion feature gated by one
  simple threshold (height >= 170dp) instead of a separate height class.
- Remove CalorieHeightClass/MacroHeightClass, extraCompact, stacked rows,
  and the two-line caption/value kcal split entirely.
- Source-contract tests rewritten to protect the classic default: provider
  minimums must stay 110x40 / 110x110, SizeMode.Exact + LocalSize.current
  remain, and no height-class composition may reappear.

Assisted-by: Open WebUI
@Dragonk

Dragonk commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
SparkyFitnessMobile/targets/android-widget/res/xml/sparky_macro_widget_info.xml (1)

8-11: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Do not support a 110dp width with this one-line localized layout.

At 110dp, the runtime layout has 82dp after horizontal padding. The preview has 86dp. The fixed dot, gaps, and value leave insufficient width for Polish labels such as "Węglowodany". The localized calorie header also cannot fit reliably. maxLines = 1 causes clipped or truncated widget content.

  • SparkyFitnessMobile/targets/android-widget/res/xml/sparky_macro_widget_info.xml#L8-L11: increase the minimum supported width, or prevent resizing below a measured safe width.
  • SparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/MacroWidget.kt.tmpl#L88-L99: add a true narrow-width layout if 110dp resizing must remain supported.
  • SparkyFitnessMobile/targets/android-widget/res/layout/sparky_macro_widget_initial_layout.xml#L14-L23: apply the same narrow-width behavior to the initial and preview layout.
🤖 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/targets/android-widget/res/xml/sparky_macro_widget_info.xml`
around lines 8 - 11, The one-line macro widget does not fit localized content at
110dp. In
SparkyFitnessMobile/targets/android-widget/res/xml/sparky_macro_widget_info.xml
lines 8-11, either raise the minimum supported width or prevent resizing below a
measured safe width; if 110dp support must remain, add a genuine narrow-width
layout in MacroWidget.kt.tmpl lines 88-99 and apply the same narrow-width
behavior to
SparkyFitnessMobile/targets/android-widget/res/layout/sparky_macro_widget_initial_layout.xml
lines 14-23.
🤖 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/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/MacroWidget.kt.tmpl`:
- Around line 89-99: Increase the showActions threshold in the MacroWidget
layout so the narrow composition has at least 145.dp of content height plus
font-scale headroom before rendering the 48.dp action row, preventing macro or
action clipping. Update the boundary layout test to verify actions remain hidden
below the safe threshold and appear at or above it.

---

Outside diff comments:
In
`@SparkyFitnessMobile/targets/android-widget/res/xml/sparky_macro_widget_info.xml`:
- Around line 8-11: The one-line macro widget does not fit localized content at
110dp. In
SparkyFitnessMobile/targets/android-widget/res/xml/sparky_macro_widget_info.xml
lines 8-11, either raise the minimum supported width or prevent resizing below a
measured safe width; if 110dp support must remain, add a genuine narrow-width
layout in MacroWidget.kt.tmpl lines 88-99 and apply the same narrow-width
behavior to
SparkyFitnessMobile/targets/android-widget/res/layout/sparky_macro_widget_initial_layout.xml
lines 14-23.
🪄 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: 67bcae58-caf5-483b-a259-70f308a862b4

📥 Commits

Reviewing files that changed from the base of the PR and between 7530760 and 53994e4.

📒 Files selected for processing (7)
  • SparkyFitnessMobile/__tests__/config/widgetResourceContract.test.ts
  • SparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/CalorieWidget.kt.tmpl
  • SparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/MacroWidget.kt.tmpl
  • SparkyFitnessMobile/targets/android-widget/res/layout/sparky_macro_widget_initial_layout.xml
  • SparkyFitnessMobile/targets/android-widget/res/layout/sparky_widget_initial_layout.xml
  • SparkyFitnessMobile/targets/android-widget/res/xml/sparky_calorie_widget_info.xml
  • SparkyFitnessMobile/targets/android-widget/res/xml/sparky_macro_widget_info.xml
🚧 Files skipped from review as they are similar to previous changes (1)
  • SparkyFitnessMobile/targets/android-widget/res/xml/sparky_calorie_widget_info.xml

The 170dp threshold leaves only ~142dp of content after the 14+14dp
padding, which is too tight for the kcal header, three inline macro rows
and the 40dp action row. Raise the single action-expansion threshold to
190dp (~162dp of content) so shortcut actions only appear when they
actually fit cleanly.

Assisted-by: Open WebUI
@Dragonk

Dragonk commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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.

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]: Add localization (i18n) support to the mobile app

1 participant