feat(mobile): localize native widgets and live activity - #2072
Conversation
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
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe 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. ChangesMobile localization
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 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
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
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. Comment |
PR Validation ResultsChange Detection
❌ Required Actions (1)
|
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
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (12)
SparkyFitnessMobile/scripts/audit-mobile-i18n.mjs (1)
24-29: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winfileandlinefor structural errors that have nokey.
report.localeStructuralErrorsalso receivessource-scan-errorentries and suppression issues. Those entries carryfileandlinebut nokey, 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 tradeoffConsider moving the scanner state into a per-run context object.
findings,suppressionRecords,suppressionIssues,alertButtonTextProps, andtoastTextPropsare module-level.collectFindingsclears onlyfindingsandsuppressionIssues, andgetAllSuppressionIssuesreads state that belongs to the last run. Two overlapping runs in the same process would mix results. A per-run context passed tovisitSourceFileand returned fromcollectFindingsremoves that coupling and makes the exportedvisitSourceFilesafe 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 winDerive the default locale paths from
rootDir.
sourceRootsdefaults torootDir, butenLocalePathandplLocalePathdefault to the productionMOBILE_ROOTpaths. If a caller passes onlyrootDir, 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 winAdd coverage for the CLI entry point.
The tests cover
runAudit,LocaleValidator, andcollectFindings. No test coversscripts/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 winAccept formatted interpolation names in
placeholderNames.
/\{\{(\w+)\}\}/gdoes 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 winSplit "Set X of Y" fragments in both catalogs. Both catalogs expose
setandsetOfas 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: replacesetandsetOfwith"setProgress": "Set {{current}} of {{total}}".SparkyFitnessMobile/src/localization/locales/pl/translation.json#L38-L39: replacesetandsetOfwith"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 winDerive
WidgetLocaleOverridefrom the app-language type instead of duplicating the union.
useWidgetLanguageRefreshpassesLanguagePreferencevalues excluding'system'intosetWidgetLocale. 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/sharedover 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 valueConsider failing fast when the manifest application node is missing.
addWidgetReceiversreturns silently whenapplicationisundefined. 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 missingconfig.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:104asserts theundefinedno-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 winBoth 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 localvar firstFailure: Exception?and pass it as the cause when rejecting withE_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:233assertscatch (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 winThe unhandled-rejection assertion cannot detect a real unhandled rejection; remove it or make it effective.
Node emits
unhandledRejectionon a later turn of the event loop.flushReloadawaits three microtasks, soexpect(onUnhandledRejection).not.toHaveBeenCalled()at Line 270 always passes, even if the hook did leak a rejection. The test gives false confidence.
originalOnUnhandledat Line 263 is never used; Line 281 only voids it. Remove that binding.The remaining
addLogassertions 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 winMake
extractStringResourcestolerate extra attributes on<string>.The regex requires
nameto be the only attribute. Android string resources often carryformatted="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 winReplace the character-distance assertions with structural XML checks.
The calorie test uses
[\s\S]{0,200}and the macro test usesgramsCarbsIndex - 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-parseris 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (70)
SparkyFitnessMobile/AGENTS.mdSparkyFitnessMobile/App.tsxSparkyFitnessMobile/__mocks__/expo-winter.jsSparkyFitnessMobile/__tests__/config/iosWidgetResources.test.tsSparkyFitnessMobile/__tests__/config/iosWidgetSwiftContract.test.tsSparkyFitnessMobile/__tests__/config/nativeLocales.test.tsSparkyFitnessMobile/__tests__/config/widgetResourceContract.test.tsSparkyFitnessMobile/__tests__/config/withAppLanguage.test.tsSparkyFitnessMobile/__tests__/config/withCalorieWidget.test.tsSparkyFitnessMobile/__tests__/hooks/useAppBootstrap.test.tsxSparkyFitnessMobile/__tests__/hooks/useAppLanguageForegroundSync.test.tsxSparkyFitnessMobile/__tests__/hooks/useAppStartup.test.tsSparkyFitnessMobile/__tests__/hooks/useIOSWidgetLanguageRefresh.test.tsxSparkyFitnessMobile/__tests__/hooks/useScreenHeader.test.tsxSparkyFitnessMobile/__tests__/hooks/useWidgetLanguageRefresh.test.tsxSparkyFitnessMobile/__tests__/localization/appLanguage.test.tsSparkyFitnessMobile/__tests__/localization/i18n.test.tsSparkyFitnessMobile/__tests__/screens/AppSettingsScreen.test.tsxSparkyFitnessMobile/__tests__/scripts/i18nAudit.test.tsSparkyFitnessMobile/__tests__/services/workoutLiveActivity.test.tsSparkyFitnessMobile/__tests__/services/workoutLiveActivityLabels.test.tsSparkyFitnessMobile/__tests__/services/workoutLiveActivityLayoutContract.test.tsSparkyFitnessMobile/__tests__/stores/appPreferencesStore.test.tsSparkyFitnessMobile/app.config.tsSparkyFitnessMobile/jest.setup.jsSparkyFitnessMobile/locales/en.jsonSparkyFitnessMobile/locales/pl.jsonSparkyFitnessMobile/package.jsonSparkyFitnessMobile/plugins/withAppLanguage.tsSparkyFitnessMobile/plugins/withCalorieWidget.tsSparkyFitnessMobile/scripts/audit-mobile-i18n.mjsSparkyFitnessMobile/scripts/i18n-audit/core.cjsSparkyFitnessMobile/scripts/i18n-audit/localeValidator.cjsSparkyFitnessMobile/scripts/i18n-audit/sourceScanner.cjsSparkyFitnessMobile/src/components/BottomSheetPicker.tsxSparkyFitnessMobile/src/hooks/useAppBootstrap.tsSparkyFitnessMobile/src/hooks/useAppLanguageForegroundSync.tsSparkyFitnessMobile/src/hooks/useAppStartup.tsSparkyFitnessMobile/src/hooks/useIOSWidgetLanguageRefresh.tsSparkyFitnessMobile/src/hooks/useScreenHeader.tsxSparkyFitnessMobile/src/hooks/useWidgetLanguageRefresh.tsSparkyFitnessMobile/src/localization/appLanguage.tsSparkyFitnessMobile/src/localization/i18n.tsSparkyFitnessMobile/src/localization/index.tsSparkyFitnessMobile/src/localization/locales/en/translation.jsonSparkyFitnessMobile/src/localization/locales/pl/translation.jsonSparkyFitnessMobile/src/screens/AppSettingsScreen.tsxSparkyFitnessMobile/src/services/CalorieWidgetBridge.tsSparkyFitnessMobile/src/services/WorkoutLiveActivityLayout.tsxSparkyFitnessMobile/src/services/appLanguageNative.tsSparkyFitnessMobile/src/services/workoutLiveActivity.ios.tsSparkyFitnessMobile/src/services/workoutLiveActivityLabels.tsSparkyFitnessMobile/src/stores/appPreferencesStore.tsSparkyFitnessMobile/targets/android-language/kotlin/com/sparkyapps/sparkyfitness/language/AppLanguageModule.ktSparkyFitnessMobile/targets/android-language/kotlin/com/sparkyapps/sparkyfitness/language/AppLanguagePackage.ktSparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/CalorieWidget.kt.tmplSparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/CalorieWidgetModule.kt.tmplSparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/MacroWidget.kt.tmplSparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/WidgetLocale.kt.tmplSparkyFitnessMobile/targets/android-widget/res/layout/sparky_macro_widget_initial_layout.xmlSparkyFitnessMobile/targets/android-widget/res/layout/sparky_widget_initial_layout.xmlSparkyFitnessMobile/targets/android-widget/res/values-pl/widget_strings.xmlSparkyFitnessMobile/targets/android-widget/res/values/widget_strings.xmlSparkyFitnessMobile/targets/android-widget/res/xml/sparky_calorie_widget_info.xmlSparkyFitnessMobile/targets/android-widget/res/xml/sparky_macro_widget_info.xmlSparkyFitnessMobile/targets/widget/SharedHelpers.swiftSparkyFitnessMobile/targets/widget/en.lproj/Localizable.stringsSparkyFitnessMobile/targets/widget/macroWidget.swiftSparkyFitnessMobile/targets/widget/pl.lproj/Localizable.stringsSparkyFitnessMobile/targets/widget/widgets.swift
| function collectFindings(rootDir, sourceRoots) { | ||
| findings.length = 0; | ||
| suppressionIssues.clear(); | ||
| const sourceFilesSet = new Set(); | ||
| for (const sourceRoot of sourceRoots) { | ||
| walkFiles(sourceRoot, rootDir, sourceFilesSet); | ||
| } |
There was a problem hiding this comment.
🩺 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.
| 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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.
| 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) | ||
| } |
There was a problem hiding this comment.
🗄️ 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.
| 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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
|
@coderabbitai review |
✅ Action performedReview finished.
|
…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
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
SparkyFitnessMobile/__tests__/config/widgetResourceContract.test.ts (1)
362-369: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe test asserts the opposite of its name.
The title states "no header/action requirement", but the body requires
CalorieHeader(context = widgetContextto be present in the source. The assertion also cannot prove that the header is skipped atSHORT, or thatMacroRowsrenders 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
📒 Files selected for processing (7)
SparkyFitnessMobile/__tests__/config/widgetResourceContract.test.tsSparkyFitnessMobile/src/hooks/useWidgetLanguageRefresh.tsSparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/CalorieWidget.kt.tmplSparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/MacroWidget.kt.tmplSparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/WidgetLocale.kt.tmplSparkyFitnessMobile/targets/android-widget/res/xml/sparky_calorie_widget_info.xmlSparkyFitnessMobile/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
|
@coderabbitai review |
|
- 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
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@coderabbitai review |
|
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
|
@coderabbitai review |
✅ Action performedReview finished.
|
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
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
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 liftDo 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 = 1causes 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
📒 Files selected for processing (7)
SparkyFitnessMobile/__tests__/config/widgetResourceContract.test.tsSparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/CalorieWidget.kt.tmplSparkyFitnessMobile/targets/android-widget/kotlin/com/sparkyapps/sparkyfitness/widget/MacroWidget.kt.tmplSparkyFitnessMobile/targets/android-widget/res/layout/sparky_macro_widget_initial_layout.xmlSparkyFitnessMobile/targets/android-widget/res/layout/sparky_widget_initial_layout.xmlSparkyFitnessMobile/targets/android-widget/res/xml/sparky_calorie_widget_info.xmlSparkyFitnessMobile/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
|
@coderabbitai review |
✅ Action performedReview finished.
|
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 persistedsystem | en | plpreference throughappPreferencesStore, Android 13+ nativeLocaleManager, 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?
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 nativeLocaleManager/ App Languages is authoritative:localizedContext()always uses the native context andsetOverride()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.systemmeans NO widget-only override anywhere — the key is removed and widgets follow the native/device locale.useWidgetLanguageRefreshreacts to BOTH preference changes and effective-language changes (so explicit →systemclears the override even when the effective language stays the same), then reloads both widgets with partial-reload isolation and retry semantics.en.lproj/pl.lprojLocalizable.strings(identical key sets) cover widget names, descriptions, kcal labels, Food/Burned/Goal, Protein/Carbs/Fat, grams, and accessibility labels, plus explicitwidget.search_food/widget.scan_barcodeaccessibility labels on the icon-only action buttons.useIOSWidgetLanguageRefreshwriteswidgetLocalefor expliciten/pland removes it forsystem(matchingSharedHelpers.swift's "absent = follow native locale" contract), reloading both timelines with retry semantics.localizedWidgetStringis hardened: explicit bundle → native bundle → English bundle → stable readable fallback, never a raw key.activeWorkout.liveActivitykeys (EN+PL) with an explicit EnglishdefaultValueper key — a missing PL key can never leakactiveWorkout.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|verticalresizing) 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
cd SparkyFitnessMobile && pnpm installpnpm run i18n:audit→ expectuser-facing t() without English fallback: 0,dynamic t() keys: 0, structural errors0.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.tsPR Type
Checklist
All PRs:
New features only:
Mobile changes (
SparkyFitnessMobile/):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
expo prebuild --clean --platform androidrun twice — idempotent (widget Kotlin sources 1×, widget string resources 1×, receivers 2×, AppLanguage package 1×, resize provider XML preserved). The forkBuild Test APKworkflow from the EXACT final SHA (75307602) succeeded (exact-SHA guard passed; run 31479807092 (final re-run on the same SHA), artifactSparkyFitness-75307602-dev-release, APKSparkyFitness-feat-mobile-native-surface-localization-75307602-dev-release.apk, SHA256d26bfc75…). Physical device/emulator test pending (no device) — the Device checkbox stays unchecked until the new APK is tested.Summary by CodeRabbit
New Features
Bug Fixes
Tests