Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0f12019
Prepare modules for translation by externalizing sample app strings a…
TheKalpeshPawar May 2, 2026
09f92c7
Add translations for 39 locales matching openMF/kmp-project-template …
TheKalpeshPawar May 2, 2026
4d456c3
Align translations with Google Pay localization conventions
TheKalpeshPawar May 2, 2026
32c3ac7
Localize biometric prompt strings via consumer-supplied parameters; a…
TheKalpeshPawar May 2, 2026
c302c51
Add values-he/ as sibling of values-iw/ for Hebrew Android 13+ per-ap…
TheKalpeshPawar May 2, 2026
7cdae53
Add Arabic (ar) translations — important for Mifos MENA microfinance …
TheKalpeshPawar May 2, 2026
53986d6
Add 17 locales matching Google Pay benchmark + Mifos microfinance mar…
TheKalpeshPawar May 2, 2026
fe2fb0f
Localize numeric keypad digits via Compose Resources
TheKalpeshPawar May 2, 2026
c3e3108
Fix Spanish layout issues + localize PasscodeLengthSwitch labels
TheKalpeshPawar May 2, 2026
685b1f8
Use native digit glyphs in passcode_length_*_digits for non-Latin num…
TheKalpeshPawar May 2, 2026
f2fb021
added translations
TheKalpeshPawar May 2, 2026
145a88b
Add specific biometric error types and multi-language localizations
TheKalpeshPawar May 2, 2026
e3ee15e
Fill biometric_error_* gap across 40 locales + translate fil titles
TheKalpeshPawar May 2, 2026
cb93960
Update L10N_FIX_PLAN.md: P0 fully done
TheKalpeshPawar May 2, 2026
07fc468
Fix locale-specific quality issues per L10N_FIX_PLAN.md P1
TheKalpeshPawar May 2, 2026
9f57ed7
Update L10N_FIX_PLAN.md: P1 commit landed
TheKalpeshPawar May 2, 2026
993319e
L10N_FIX_PLAN.md: P1 manual verification complete
TheKalpeshPawar May 2, 2026
64f0ff3
Apply formal register across 16 P2 locales
TheKalpeshPawar May 2, 2026
0859a6e
Remove error logging from Windows Hello registration.
TheKalpeshPawar May 2, 2026
4bf04f7
spotless fixes
TheKalpeshPawar May 3, 2026
363e3d9
Improve `PasscodeLengthSwitch` layout to support localized labels
TheKalpeshPawar May 5, 2026
27935e7
Introduce customizable string support for passcode components.
TheKalpeshPawar May 5, 2026
f2cebd5
spotless fixes
TheKalpeshPawar May 5, 2026
028828b
Introduce PasscodeStrings; require explicit strings on leaf composables
TheKalpeshPawar May 5, 2026
d70d2f4
Add localized resources and integrate `PasscodeStrings` API
TheKalpeshPawar May 6, 2026
d2ca147
Remove bundled translations from lib; relocate to l10n-templates/
TheKalpeshPawar May 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 195 additions & 0 deletions L10N_FIX_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# L10n Remediation Plan

> **Source of truth:** `l10n-review/REPORT.md` (cross-agent verification report, 2026-05-02).
> Every action below traces back to a finding there. If reality and report disagree, run the verification commands in this doc and trust the filesystem.

## How to resume from a fresh session

1. `cd` into repo root, read this file, read `l10n-review/REPORT.md`.
2. Find the lowest unchecked `[ ]` row in the active phase below — that's the resume point.
3. Run the **Verification command** for that phase first to confirm reality matches what's checked. If a `[x]` row turns out to be undone, uncheck it and redo.
4. Phases must complete in order: P0 → P1 → P2 → P3. P0 blocks shipping. P1 is small fixes worth landing before review. P2/P3 are deferred (recorded for later).

## Branch strategy

- Working branch: `translate-strings` (existing, not yet PR'd as of 2026-05-03).
- One commit per phase (P0, P1) keeps the diff readable. P0 ships independently if needed.

---

## P0 — Critical: missing keys cause English fallback at runtime

### P0a: 6 legacy biometric error strings missing from sample-shared in ~40 locales

**Root cause:** `biometric_error_lockout`, `_hardware_unavailable`, `_not_enrolled`, `_timeout`, `_no_space`, `_unknown` were added to `cmp-sample-shared/src/commonMain/composeResources/values/strings.xml` after the bulk-translation pass on most locales.

**18 locales that DO have them** (verified 2026-05-02 via grep): `am, ar, bn, fil, gu, kn, lt, mk, mr, pa, si, sl, sq, sr, sw, ta, te, ur`.

**Verification command:**
```bash
grep -L "biometric_error_lockout" cmp-sample-shared/src/commonMain/composeResources/values-*/strings.xml | wc -l
# 0 = done. Anything else = locales still missing.
```

**Action:** Add the 6 keys with locale-appropriate translations to every file returned by:
```bash
grep -L "biometric_error_lockout" cmp-sample-shared/src/commonMain/composeResources/values-*/strings.xml
```

Translation seed: copy the English source from `values/strings.xml`, machine-translate per locale matching Google Pay convention, then visually spot-check via `cmp-sample-android` install on at least 3 sampled locales (de, hi, ar) before considering P0a complete.

- [x] P0a executed (script run, all 6 × 40 locales added — translations sourced from `l10n-review/batch_*.json` `suggested_fix` payloads, except he/iw which were translated manually)
- [x] P0a verification grep returns 0 (all 58 locales now have all 6 keys)

### P0b: Filipino has 3 untranslated English strings

Per `l10n-review/REPORT.md:501-503`:
- `login_screen_title` = "Login Screen" (English) → "Screen ng Pag-login"
- `home_screen_title` = "Home Screen" (English) → "Home Screen" (Filipino keeps loanword) or "Pangunahing Screen"
- `biometric_setup_title` = "Biometric Setup" (English) → "Pag-setup ng Biometrics"

File: `cmp-sample-shared/src/commonMain/composeResources/values-fil/strings.xml`.

**Verification command:**
```bash
grep -E "(Login Screen|Home Screen|Biometric Setup)<" cmp-sample-shared/src/commonMain/composeResources/values-fil/strings.xml
# Empty result = done.
```

- [x] P0b: 3 fil strings translated (Screen ng Pag-login / Pangunahing Screen / Pag-setup ng Biometric)

### P0 Final verification

```bash
./gradlew :cmp-sample-android:assembleDebug :cmp-sample-shared:compileDebugKotlinAndroid
# Build green = no XML parse errors introduced.
```

- [x] Build green (verified `:cmp-sample-android:assembleDebug` + `:cmp-sample-shared:compileDebugKotlinAndroid` + `:compileKotlinDesktop`)
- [x] Single P0 commit landed on `translate-strings` (commit `e3ee15e`)

---

## P1 — High-value targeted fixes, locale-by-locale

### P1a: `af` cd_* labels use wrong verb "Stawe"

Per `l10n-review/REPORT.md:529`. File: `cmp-sample-shared/src/commonMain/composeResources/values-af/strings.xml`.

Replace `Stawe met X` → `Verifieer met X` for the 4 keys: `cd_fingerprint_icon`, `cd_face_scan_icon`, `cd_iris_scan_icon`, `cd_device_credential_icon`.

Verification: `grep -c "Stawe" cmp-sample-shared/src/commonMain/composeResources/values-af/strings.xml` → 0.

- [x] P1a executed (4 cd_* keys; `Stawe met` → `Verifieer met`)

### P1b: `ko` passcode terminology split (암호 vs 비밀번호)

Per `l10n-review/REPORT.md:130, 138`. Standardize on `비밀번호` (Google Pay KR convention) across:
- `mifos-authenticator-passcode/src/commonMain/composeResources/values-ko/strings.xml`
- `cmp-sample-shared/src/commonMain/composeResources/values-ko/strings.xml`

Replace every `암호` → `비밀번호`. Watch for compounds (e.g., `암호화` = "encryption" — DO NOT replace that one if it appears).

Verification:
```bash
grep -n "암호" mifos-authenticator-passcode/src/commonMain/composeResources/values-ko/strings.xml cmp-sample-shared/src/commonMain/composeResources/values-ko/strings.xml
# Empty result, or only 암호화/암호화된 contexts = done.
```

- [x] P1b executed (7 occurrences across both ko files; `암호` → `비밀번호`. Verified no `암호화` false positives.)

### P1c: `ca` "Desbloca" → "Desbloqueja"

Per `l10n-review/REPORT.md:654`. File: `cmp-sample-shared/src/commonMain/composeResources/values-ca/strings.xml`.

Affected keys: `biometric_prompt_title`, `biometric_prompt_subtitle`, `unlock_with_biometrics`.

Also per `REPORT.md:655-657`: `Configureu` → `Configura`, `Torneu-ho` → `Torna-ho` in 3 strings.

Verification: `grep -E "Desbloca[^q]|Configureu|Torneu-ho" cmp-sample-shared/src/commonMain/composeResources/values-ca/strings.xml` → empty.

- [x] P1c executed (3× `Desbloca` → `Desbloqueja`, 1× `Configureu` → `Configura`, 2× `Torneu-ho` → `Torna-ho`)

### P1d: `hi` ASCII period `.` → danda `।` in 3 new error strings

Per `l10n-review/REPORT.md:250`. File: `cmp-sample-shared/src/commonMain/composeResources/values-hi/strings.xml`.

Affected keys: `biometric_error_invalid_registration_data`, `biometric_error_invalid_arguments_auth`, `biometric_error_invalid_arguments_registration`.

Replace each `.` (after Hindi text) with `।`.

Verification:
```bash
grep -E "biometric_error_invalid.*\." cmp-sample-shared/src/commonMain/composeResources/values-hi/strings.xml
# Should return 0 lines (no ASCII periods in those 3 strings).
```

- [x] P1d executed (3 strings rewritten; ASCII `.` → danda `।` ×6)

### P1 Final verification

- [x] Build green: `:cmp-sample-android:assembleDebug` + `:cmp-sample-shared:compileDebugKotlinAndroid` + `:mifos-authenticator-passcode:compileDebugKotlinAndroid` all green
- [x] Manual on-device check of af, ko, ca, hi (2026-05-03): all 4 login screens render in correct script/language; ko deep-nav confirms `비밀번호 만들기` (create_passcode) and `비밀번호 표시 전환` (cd_toggle_passcode_visibility) — fix is live, no `암호` form visible. UI dumps + screenshots saved to `/tmp/p1_verify/`
- [x] P1 commit landed (`07fc468` on `translate-strings`)

---

## P2 — Register violations: done (formal applied across all 16 locales)

**Decision (2026-05-03):** Override the original "deferred" status and the `REPORT.md` case-by-case recommendation (which suggested informal for ~14 of these locales per Google Pay convention). Per user instruction, applied **formal register everywhere** — Sie/Ön/вы/ви/вие/Ви/Vi/Ju/Dvs/Jūs/Вие/εσείς/siz — across all 16 locales.

**Approach:** 16 parallel `general-purpose` subagents, one per locale, each producing JSON `{key, old, new}` diffs limited to register markers (pronouns, verb endings, possessives, polite-imperative forms). 228 string substitutions applied via Python script. lt was already 100% formal (0 changes). Build green across android-debug + sample-shared compile + passcode-lib compile + desktop targets.

**Affected locales** (16): `de, hu, ru, uk, bg, be, sr, hr, sl, sq, ro, lt, lv, mk, el, tr`.

Per-locale specifics in `l10n-review/REPORT.md`:
- `de`: 556 (sample uses `du`, banking standard is `Sie`)
- `hu`: 671-679 (uses formal `Ön`, Google Pay HU uses informal `te`)
- `ru`: 765-776
- `uk`: 783-796
- `bg`: 805-816
- `be`: 892-895
- `sr`: 824-836
- `hr`: 843-853
- `sl`: 860-868
- `sq`: 875-885
- `ro`: 912-915
- `lt`: 939-947
- `lv`: 954-960
- `mk`: 969-974
- `el`: 983-994
- `tr`: 1003-1012

- [x] P2 executed in-line (no GH issues — formal register applied directly)
- [x] All 16 locales have formal register; build green; native-speaker review still recommended for low-resource locales (be, mk, sq, sl, lv, lt, sr, hr) before final ship

---

## P3 — Stylistic / verbosity / terminology drift: deferred indefinitely

**Decision (2026-05-02):** Skip until P0+P1 are merged and feedback comes in. Many of these are individually-defensible style choices that don't move user-visible quality the way P0/P1 fixes do.

Examples (non-exhaustive, see `REPORT.md` for full list):
- `cd_toggle_passcode_visibility` verbosity in mr (317), gu (258), bn (296), nb (721)
- "ઉપકરણ"/"ડિવાઇસ" inconsistency in gu (271)
- "ਬਾਇਓਮੈਟ੍ਰਿਕ"/"ਬਾਇਓਮੀਟ੍ਰਿਕ" spelling inconsistency in pa (287)
- ja `try_again` "再試行" preferred over "もう一度試す" (111)
- ar accessibility labels carry definite article (164)
- Many cd_* labels in various locales are nominal phrases instead of imperatives

- [ ] Optional: open one umbrella GH issue `l10n/style-cleanup` linking to this section. No commits required.

---

## Status log

| Date | Phase | Action |
|------|-------|--------|
| 2026-05-02 | Plan created | This file written |
| 2026-05-03 | P0a complete | 240 strings added across 40 locales |
| 2026-05-03 | P0b complete | fil 3 strings translated |
| 2026-05-03 | P0 verified | Build green, all 6 × 58 = 348 keys present |
| 2026-05-03 | P0 committed | `e3ee15e` on branch `translate-strings`. P0 fully done. |
| 2026-05-03 | P1a-P1d complete | af / ko / ca / hi quality fixes applied; all 4 verification greps pass; build green. |
| 2026-05-03 | P2 complete | 228 formal-register substitutions across 16 locales × 2 modules. lt unchanged (already formal). Build green. |

161 changes: 161 additions & 0 deletions LOCALIZATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Localization

This document describes how `mifos-passcode-cmp` handles user-facing text and how a consuming application controls the language shown at runtime.

## Bundled locales

Both `mifos-authenticator-passcode` and `cmp-sample-shared` ship Compose Multiplatform Resource bundles for **57 locale variants plus default English**. The base set matches [`openMF/kmp-project-template`](https://github.com/openMF/kmp-project-template); 18 additional locales were added to align with Google Pay's supported-language list and Mifos's MENA, South Asia, and Sub-Saharan Africa microfinance markets. Default text is English (`values/`).

| Code (Compose dir) | Language |
| --- | --- |
| `values-af` | Afrikaans |
| `values-am` | Amharic |
| `values-ar` | Arabic (RTL) |
| `values-be` | Belarusian |
| `values-bg` | Bulgarian |
| `values-bn` | Bengali |
| `values-ca` | Catalan |
| `values-cs` | Czech |
| `values-da` | Danish |
| `values-de` | German |
| `values-el` | Greek |
| `values-en-rGB` | English (United Kingdom) |
| `values-es` | Spanish |
| `values-et` | Estonian |
| `values-fa` | Persian (RTL) |
| `values-fi` | Finnish |
| `values-fil` | Filipino |
| `values-fr` | French |
| `values-gu` | Gujarati |
| `values-he` | Hebrew (modern code, paired with `values-iw`) |
| `values-hi` | Hindi |
| `values-hr` | Croatian |
| `values-hu` | Hungarian |
| `values-in` | Indonesian (legacy code; modern is `id`) |
| `values-it` | Italian |
| `values-iw` | Hebrew (legacy code, paired with `values-he` for Android <13) |
| `values-ja` | Japanese |
| `values-kn` | Kannada |
| `values-ko` | Korean |
| `values-lt` | Lithuanian |
| `values-lv` | Latvian |
| `values-mk` | Macedonian |
| `values-ml` | Malayalam |
| `values-mr` | Marathi |
| `values-nb` | Norwegian Bokmål |
| `values-nl` | Dutch |
| `values-pa` | Punjabi (Gurmukhi) |
| `values-pl` | Polish |
| `values-pt-rBR` | Portuguese (Brazil) |
| `values-pt-rPT` | Portuguese (Portugal) |
| `values-ro` | Romanian |
| `values-ru` | Russian |
| `values-si` | Sinhala |
| `values-sk` | Slovak |
| `values-sl` | Slovenian |
| `values-sq` | Albanian |
| `values-sr` | Serbian (Cyrillic) |
| `values-sv` | Swedish |
| `values-sw` | Swahili |
| `values-ta` | Tamil |
| `values-te` | Telugu |
| `values-th` | Thai |
| `values-tr` | Turkish |
| `values-uk` | Ukrainian |
| `values-ur` | Urdu (RTL) |
| `values-vi` | Vietnamese |
| `values-zh-rCN` | Chinese (Simplified) |
| `values-zh-rTW` | Chinese (Traditional) |

## Translation status

> **Machine-translated first drafts.** All locale files are generated by an LLM with audit-driven corrections. They are good enough for review and integration work but **have not been native-speaker reviewed**. Treat translations as drafts pending native review before shipping to production.
>
> Specifically flagged for native review: **Thai (`th`)**, **Persian (`fa`)**, **Malayalam (`ml`)**, **Sinhala (`si`)**, **Amharic (`am`)** — auditor declined to certify these. All other locales should be reviewed by a fluent speaker before shipping to that market, particularly the smaller European languages (`af`, `be`, `et`, `lv`, `ca`, `lt`, `sl`, `mk`, `sq`).
>
> **Known sentence-fragment issue:** `biometric_error_lockout` reads as a noun phrase ("Too many failed attempts.") in many Indic languages where the source English would read more naturally as a sentence. This is acceptable as a label but native review may want to recast it as a full sentence per language.
>
> Style decisions follow Google Pay / Wallet conventions: informal `du` in German, `tu` imperative in Romanian, Latin "OK" in Cyrillic locales, `합쇼체` (-습니다) for Korean transactional flows.

## How locale selection works

Compose Multiplatform Resources reads the **OS-level locale** at runtime and resolves `stringResource(Res.string.…)` calls against the most specific matching `values-XX/` directory, falling back to the default `values/`. This works automatically across all targets (Android, iOS, Desktop, Web) without any per-platform setup.

The library does **not** ship its own language picker or persistence — that is the consumer's responsibility. Switching language at runtime requires the consumer to update the OS-reported locale.

## Per-app locale switching (Android)

The recommended consumer recipe matches `kmp-project-template`. Persist the chosen locale in your app preferences, then on startup and on user change:

```kotlin
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat

fun applyAppLocale(localeTag: String?) {
val desiredLocales = if (localeTag != null) {
LocaleListCompat.forLanguageTags(localeTag)
} else {
// System default
LocaleListCompat.getEmptyLocaleList()
}
val currentLocales = AppCompatDelegate.getApplicationLocales()
if (currentLocales != desiredLocales) {
AppCompatDelegate.setApplicationLocales(desiredLocales)
}
}
```

After this call, all Compose Resources lookups in the library and your app code resolve to the requested locale automatically. No further wiring is needed in the library itself.

For a complete reference integration including a language picker UI and persistence layer, see:

- `cmp-navigation/src/commonMain/kotlin/cmp/navigation/AppViewModel.kt` — the `observeLanguage` / `UpdateAppLocale` plumbing
- `cmp-android/src/main/kotlin/cmp/android/app/AndroidApp.kt:restoreSavedLanguage()` — the per-app-locale call site
- `core/model/src/commonMain/kotlin/org/mifos/core/model/LanguageConfig.kt` — the supported-locales enum
- `feature/settings/src/commonMain/kotlin/org/mifos/feature/settings/LanguageDialog.kt` — the picker UI

in [openMF/kmp-project-template](https://github.com/openMF/kmp-project-template).

## Per-app locale switching (iOS / Desktop / Web)

Compose Multiplatform Resources picks up the OS locale on these platforms automatically, but **per-app overrides are not yet wired up by either this library or `kmp-project-template`**:

- **iOS**: Override requires manipulating `NSBundle.preferredLocalizations` or shipping `LSPreferredLocales` in the consumer app's `Info.plist`. Not yet provided.
- **Desktop (JVM)**: `java.util.Locale.setDefault(Locale.forLanguageTag(tag))` plus a Compose recomposition trigger works in practice. Not yet abstracted.
- **Web (wasmJs / js)**: `navigator.language` is read-only; per-app switching requires a `CompositionLocal`-based override or URL-param scheme. Not yet provided.

Consumers needing cross-platform locale switching should treat this as an open task and contribute back.

## Biometric prompt strings

The biometrics module's `PlatformAuthenticator.authenticate(...)` and `PlatformAuthenticator.registerUser(...)` accept `title`, `subtitle`, `description`, and `negativeButtonText` parameters that are passed through to the platform's biometric prompt UI. **The library does not bundle translations for these strings** — the consumer is expected to pass already-localized strings sourced from its own resources (e.g. via `stringResource(Res.string.your_biometric_prompt_title)`).

This applies on Android (`BiometricPrompt.PromptInfo`) and iOS (`LAContext.localizedReason` — `title` only; subtitle/description/negativeButtonText are silently ignored). Desktop and Web ignore all four.

The system-rendered chrome inside the prompt (cancel / "Use PIN" / "Touch sensor" / recoverable error toasts) is rendered by the OS using the **device locale**, not the app locale — this is unavoidable on both Android and iOS.

## Biometric error mapping

Authentication and registration errors come back as `BiometricError` sealed cases (`Lockout`, `LockoutPermanent`, `HardwareUnavailable`, `NotEnrolled`, `Timeout`, `NoSpace`, `Unknown`). The consumer maps these to localized strings from its own resources — the library deliberately does not surface platform error prose because those strings are device-locale, not app-locale.

See `cmp-sample-shared/src/commonMain/kotlin/cmp/sample/shared/platformAuthentication/BiometricKey.kt` for the `rememberBiometricErrorMessages()` pattern used by the sample app.

## Adding a new locale

1. Copy `mifos-authenticator-passcode/src/commonMain/composeResources/values/strings.xml` to `values-XX/strings.xml`.
2. Translate every `<string>` value while preserving the `name="…"` keys verbatim.
3. Repeat for `cmp-sample-shared/src/commonMain/composeResources/values/strings.xml` (the sample app keys).
4. Build to verify Compose Resources picks up the new locale: `./gradlew :mifos-authenticator-passcode:assemble :cmp-sample-shared:assemble`.
5. Submit for native-speaker review.

## Adding a new string

1. Add the `<string>` to `values/strings.xml` in the relevant module first (the English source of truth).
2. Add the same key with a placeholder or auto-translated value to all `values-XX/strings.xml` in that module — Compose Resources falls back to the default file for any missing key, but mismatched key sets across locales are a code smell.
3. Reference it via `stringResource(Res.string.your_key)` from a `@Composable` context.

## Notes for library consumers

- Strings used by `PasscodeScreen` and friends ship inside the library's resources. Consumers do **not** override them via constructor parameters; the standard Compose Resources locale-resolution mechanism above is the only override path.
- This is a deliberate design choice matching the `kmp-project-template` ecosystem convention. If your app needs to override library strings (rare), contribute the strings upstream rather than monkey-patching.
- Strings rendered as part of the biometric prompt UI come from your app's resources, not the library's. See above.
Loading