Skip to content

Full app localization: English + Korean, behind a feature flag - #308

Open
KamaIOps wants to merge 2 commits into
chainapsis:mainfrom
KamaIOps:kamal/full-app-i18n-ko
Open

Full app localization: English + Korean, behind a feature flag#308
KamaIOps wants to merge 2 commits into
chainapsis:mainfrom
KamaIOps:kamal/full-app-i18n-ko

Conversation

@KamaIOps

@KamaIOps KamaIOps commented Jul 3, 2026

Copy link
Copy Markdown

Full app localization: English + Korean, behind a feature flag

What this does

Migrates every user-facing string across desktop and mobile to Flutter gen-l10n (lib/l10n/app_en.arb + app_ko.arb, ~1,260 keys) and ships a complete Korean translation, with a Language setting and follow-OS locale resolution.

English builds are unaffected: every English ARB value is byte-identical to the previous hardcoded literal, so the rendered UI is string-for-string the same as before this PR. The existing find.text() test assertions (1,400+) pass unchanged and act as regression coverage for that guarantee.

Language selection

  • Settings → Language: System (Auto) / English / 한국어 — modal on desktop, sheet on mobile.
  • No stored preference → the app follows the OS locale, clamped to supportedLocales with an English fallback. A saved choice always wins.
  • New localeProvider (+ clearToSystem()) and a context-free appLocalizationsProvider for providers/services.

Kill switch

VIZOR_LANGUAGE_ENABLED (compile-time dart-define, default: enabled).

flutter build macos --release --dart-define=VIZOR_LANGUAGE_ENABLED=false ...

Disabled = locale pinned to English (OS locale and stored preference ignored) + Language rows/modal hidden — i.e. the exact pre-PR behavior, restorable per build without reverting this PR. The const flag tree-shakes the dead branch out of release builds.

Scope: the flag governs the Flutter layer (app strings, Language UI, locale resolution). The native InfoPlist.strings permission prompts are static bundle resources that follow the device language — standard OS behavior for any localized app — and are not affected by the flag.

Implementation notes

  • Screens use AppLocalizations.of(context); context-free helpers/error mappers take an AppLocalizations parameter; Riverpod providers read appLocalizationsProvider.
  • Dates go through intl DateFormat with an intlSafeLocale() guard (pure-Dart tests never initialize locale data) and locale-aware day/month ordering; the hardcoded English month tables are gone.
  • Label-text sniffing is replaced with typed kinds: SwapStatusDetailRowKind on swap status detail rows and AddressFormatIssueKind on address-format findings, so row filtering/ordering/tooltips no longer parse English label text. A final sweep converted the last three label-sniffing sites (activity-feed routine-status detection, the swap fee-divider placement, and the swap status tab keys) to localized comparisons / kind checks — these rendered wrong in Korean otherwise.
  • Rust-thrown error strings stay English as mapper inputs; only mapper output is localized. Persisted data (account names, swap nextAction) stays English.
  • test/l10n/arb_parity_test.dart fails the build if an English key ships without a Korean translation (gen-l10n falls back silently otherwise) or if Korean uses an undeclared placeholder.
  • Widgetbook gains a LocalizationAddon (locale switcher in the sidebar).
  • ~95 test harnesses receive localization delegates.
  • CFBundleLocalizations (en, ko) added to the iOS/macOS Info.plists. No Rust changes.
  • Three fixed-height cards (unlock, set-password, intro) gain 8px headroom for taller Korean type.

Coverage notes

  • Platform-specific Dart surfaces are localized too: the Windows in-app update prompt, the Linux update snackbar, endpoint settings (region groups, validation errors, latency labels, failover toasts), the desktop Keystone shield-signing overlay, send broadcast-status warnings, and the voting screens.
  • Out of scope (native OS surfaces that follow the device language, not the in-app setting): the iOS Live Activity widget strings and Info.plist permission prompts. Candidate follow-up.

Verification

  • fvm flutter analyze: clean.
  • Desktop lane: 1,424 passed / 1 failed (receive_use_cases_test — pre-existing on main).
  • Mobile lane: 325 passed / 2 failed (mobile_routes_test, mobile_receive_use_cases_test — pre-existing on main).
  • Manually verified on macOS in Korean: onboarding (create/import/Keystone), unlock, language switching + follow-OS resolution, send end-to-end (broadcast confirmed on-chain), receive, activity, swap surfaces incl. a Keystone-signed swap deposit, settings sub-screens.

Reviewer pointers

  • The ARB files are large but mechanical; the interesting diffs are lib/src/providers/locale_provider.dart, lib/app.dart, the kind-enum refactors in lib/src/features/swap/models/, and lib/src/core/formatting/date_format.dart.
  • Copy conventions follow the repo's sentence-case rules; language names ("English" / "한국어") are intentionally never localized so users can always find their way back.
  • Two pre-existing title-case labels were aligned to the repo's sentence-case keys when reusing them ("Get signature", "After you scanned, click Get signature." on the desktop shield overlay).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50655d3611

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread lib/l10n/app_ko.arb Outdated
"receivePreviewShieldedRenew": "받기 페이지를 열거나 갱신 버튼을\n누를 때마다 새로운 Zcash 쉴드\n주소가 생성됩니다.",
"receivePreviewShieldedDiversified": "새 주소는 모두 같은 키에서 파생된\n다변화 주소입니다.\n모두 같은 지갑으로 입금됩니다.",
"passwordTooShort": "비밀번호는 최소 {min}자 이상이어야 합니다.",
"passwordAsciiOnly": "영문자, 숫자, 기호만 사용하세요.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the password charset warning literal

Under the Wallet Password Policy in AGENTS.md, the charset validation message must stay exactly Use only English letters, numbers, and symbols.. With Korean selected, every screen using validateWalletPasswordLocalized now shows this translated value for non-ASCII passwords, so the required support/security copy no longer matches the documented invariant; keep this key's displayed value literal across locales or avoid localizing this specific validation message.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

addressed in f1be969

Comment thread lib/src/providers/locale_provider.dart Outdated
/// OS locale when no preference is stored, clamped to the supported set
/// (English fallback).
Locale resolveAppLocale(Locale? preference) {
final locale = preference ?? PlatformDispatcher.instance.locale;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve provider locale from the full OS list

When no language preference is saved, MaterialApp.locale is null and Flutter resolves from the full preferred OS locale list, but this provider-side resolver only checks the primary PlatformDispatcher.instance.locale. For users whose primary OS language is unsupported but Korean is later in their preferred languages, widget text can render Korean while provider/service messages from appLocalizationsProvider fall back to English, producing mixed-language errors and status text; resolve against PlatformDispatcher.instance.locales using the same supported-locale matching.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

addressed in f1be969

@KamaIOps
KamaIOps force-pushed the kamal/full-app-i18n-ko branch from 50655d3 to f1be969 Compare July 3, 2026 11:44
@KamaIOps

KamaIOps commented Jul 3, 2026

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f1be9697f1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ios/Runner/Info.plist
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>ko</string>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add Korean InfoPlist strings for native prompts

Adding ko here advertises a Korean native localization, but the camera and Face ID permission prompts still come from the English NSCameraUsageDescription / NSFaceIDUsageDescription values because there is no ko.lproj/InfoPlist.strings under ios/Runner (and the same search shows only Base.lproj under macOS). In the QR-scan or biometric flows, Korean users will see English OS dialogs while the Flutter UI is Korean; add localized InfoPlist strings before declaring Korean support.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed — ios/Runner/en.lproj/InfoPlist.strings, ios/Runner/ko.lproj/InfoPlist.strings, and the macOS equivalents are in this branch (wired into both Runner.xcodeproj variant groups, ko added to knownRegions). Verified the built app bundles contain ko.lproj/InfoPlist.strings on both platforms.

@KamaIOps
KamaIOps force-pushed the kamal/full-app-i18n-ko branch from f1be969 to e7047e3 Compare July 3, 2026 12:23
@KamaIOps

KamaIOps commented Jul 3, 2026

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e7047e32ef

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

return ref
.read(deviceOwnerAuthProvider)
.verify(reason: kWalletResetDeviceAuthReason);
.verify(reason: reason ?? kWalletResetDeviceAuthReason);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pass the localized reset auth reason

When the locale is Korean and a wallet reset reaches the OS device-owner prompt, both reset flows still call verifyDeviceOwnerForWalletReset(ref) without a reason, so this fallback sends the hardcoded English Confirm reset Vizor even though walletResetDeviceAuthReason was added to the ARB files. Please thread the localized reason into those calls; otherwise the destructive reset confirmation remains outside the language feature.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed — both reset flows now pass a localized reason: forgot_passcode_sheet.dart threads deviceAuthConfirmReset via appLocalizationsProvider, and lost_password_screen.dart passes it from AppLocalizations.of(context). The unused walletResetDeviceAuthReason key was removed.

@KamaIOps

KamaIOps commented Jul 3, 2026

Copy link
Copy Markdown
Author

@codex review

@KamaIOps
KamaIOps force-pushed the kamal/full-app-i18n-ko branch from e7047e3 to 101dd46 Compare July 3, 2026 12:48
…flag

Migrate every user-facing string across desktop and mobile to Flutter
gen-l10n (lib/l10n/app_en.arb + app_ko.arb, ~1,160 keys) and ship a full
Korean translation. English ARB values are byte-identical to the previous
hardcoded literals, so English builds render exactly as before.

Language selection:
- Settings gains a Language option — System (Auto) / English / 한국어 —
  on desktop (modal) and mobile (sheet).
- With no stored preference the app follows the OS locale, clamped to
  supportedLocales with an English fallback; a saved choice always wins.
- localeProvider gains clearToSystem(); a context-free
  appLocalizationsProvider serves providers and services.

Feature flag (kill switch):
- VIZOR_LANGUAGE_ENABLED (compile-time dart-define, default true).
  Building with --dart-define=VIZOR_LANGUAGE_ENABLED=false pins the app
  to English, ignores stored/OS locale, and hides the Language rows —
  restoring the pre-i18n behavior verbatim without a revert. The const
  flag is tree-shaken from release builds.

Localization mechanics:
- Screens use AppLocalizations.of(context); context-free helpers and
  friendly-error mappers take an AppLocalizations parameter; Riverpod
  providers read appLocalizationsProvider (test-safe fallback).
- Dates use intl DateFormat guarded by intlSafeLocale(), with
  locale-aware day/month ordering; hardcoded month tables removed.
- Label-text sniffing replaced by typed kinds: SwapStatusDetailRowKind
  on swap status rows and AddressFormatIssueKind on address findings, so
  filtering/ordering/tooltips no longer parse English labels.
- Rust-thrown error strings remain English as mapper inputs; only mapper
  output is localized. Persisted data (account names, swap records)
  stays English.

Tooling and tests:
- Widgetbook gains a LocalizationAddon with a locale switcher.
- ~95 test harnesses receive AppLocalizations delegates (MaterialApp,
  MaterialApp.router, and bare AppTheme wrappers).
- CFBundleLocalizations (en, ko) added to iOS and macOS Info.plists.
- Fixed-height cards on the unlock/set-password/intro screens gain 8px
  headroom so taller Korean type fits.

Verification: flutter analyze clean; desktop lane 1422 passed / 1 failed
(receive_use_cases_test, pre-existing on main); mobile lane 325 passed /
2 failed (mobile_routes_test, mobile_receive_use_cases_test, both
pre-existing on main). Manually verified on macOS: language switching,
follow-OS resolution, onboarding/create/import flows, Keystone connect,
send end-to-end, activity and swap surfaces in Korean.
@KamaIOps
KamaIOps force-pushed the kamal/full-app-i18n-ko branch from 101dd46 to 7295b2b Compare July 3, 2026 13:59
@KamaIOps

KamaIOps commented Jul 3, 2026

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7295b2b76a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ios/Runner/Info.plist
Comment on lines +26 to +30
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>ko</string>
</array>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor the language kill switch for native localizations

When a build is launched with VIZOR_LANGUAGE_ENABLED=false, the Dart app is pinned to English, but this bundle still advertises Korean as a supported native localization (and the same pattern is added on macOS). On a Korean-system device, iOS will still use the new ko.lproj/InfoPlist.strings for camera/Face ID permission prompts, so the rollout kill switch no longer restores the pre-i18n English-only behavior and users can see mixed-language system prompts while the app UI is forced to English.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Intended scope: the kill switch is a compile-time Dart define and governs the Flutter layer (app strings, Language UI, locale resolution) — it exists to roll back app-string regressions. The InfoPlist strings are static bundle resources rendered by the OS in the device language, which is standard iOS/macOS behavior for any app that ships localizations; they can't observe a dart-define without custom build-phase scripting, which seems disproportionate for two static permission sentences with no regression surface. Documented this scope in the PR description. If the team prefers, a follow-up could gate the lproj resources per build flavor, but I'd argue an OS prompt in the device language alongside an English-pinned app is the platform-correct state, not a regression.

Re-apply localization over main's redesigns: mobile send status screen,
Keystone mobile signing refresh (Step 1/2 / 2/2 full-screen scanner),
method selection (wallet-link card), address-book contact name surfaces,
and swap review/composer updates. Adds en/ko keys for the new copy and
threads l10n through the new call sites.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review


P2 Badge Localize the wallet-link entry and flow

When Korean is selected, this Settings row still shows a hard-coded English label and opens /settings/link-mobile; I checked lib/src/features/wallet_link and it has no AppLocalizations usage, with the linked screen still rendering hard-coded English titles/buttons such as Link Vizor Mobile and Regenerate. Because this PR enables Korean for the app, desktop users can enter an entire mixed-language wallet-link flow from Korean Settings, so localize this row and the wallet-link screens or hide the entry until they are localized.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant