Repository navigation
Description
The problem
Tools/i18n_audit.py reports "OK no hardcoded literals" on main today — while at least seven user-facing English strings ship untranslated to German, Spanish and French.
The Android scan matches a literal only when it sits directly after the composable's opening paren:
ANDROID_PATTERN = re.compile(
r"\b(?:Text|Snackbar|AlertDialog|TopAppBar)\s*\(\s*\"((?:[^\"\\]|\\.)*)\""
r"|"
r"\b(?:title|label|text|contentDescription|placeholder)\s*=\s*\"((?:[^\"\\]|\\.)*)\""
)So Text("Save") is caught, but Text(if (saved) "Saved" else "Save") is not — Text( is followed by if, not ". The regex slides straight past, and the gate reports clean.
Evidence
grep -rnE "\b(Text|Snackbar|AlertDialog|TopAppBar)\s*\(\s*(if|when)\b" android/app/src/main/java/com/noop/ui/
JournalLog.kt:411 Text(if (item.kind.isNumeric) "Change to Yes/No" else "Change to Number")
SkinTempCardsScreen.kt:280 Text(if (plan == null) "Plan a trip or shift" else "View the full plan")
HrvSnapshotScreen.kt:290 Text(if (saved) "Saved" else "Save")
WorkoutsScreen.kt:1621 Text(if (editing == null) "Add Workout" else "Edit Workout")
WorkoutsScreen.kt:1667 Text(if (editing == null) "Add" else "Save")
BreatheScreen.kt:491 Text(if (running) "Stop session" else "Start session")
JournalLog.kt:300 Text(if (collapsed) "▸" else "▾") <- glyphs, not translatable
Every one of these is a button or label a German/Spanish/French user reads in English.
The eighth was the "Keep NOOP alive overnight" row — a whole multi-sentence battery warning, hidden the same way. Fixed in #539, which is what surfaced this.
Why it matters more than the count suggests
This is a gate that reports false confidence. #514 (a release shipping an unlocalised changelog title) and #535 (auditing main pushes) both leaned on this audit as the thing that catches drift. It does — but only for literals in the one shape it can see. Anything behind a conditional is invisible, and every future Text(if …) slips the same way, silently.
For these sites, a gate that says "clean" is arguably worse than no gate: it stops anyone looking.
Possible directions
- Extend the regex to allow
if/whenbetween the paren and the literal. Cheapest, but regex-over-Kotlin stays fragile — the next shape (Text(buildString { … }), a?:, alet) slips again. - Scan the argument span: find literals anywhere inside a
Text(...)/Button(...)call, using balanced-paren matching rather than a flat regex. Catches conditionals and most other shapes. - Invert it: flag every string literal in
ui//widget/that passes the existingis_probably_ui_textheuristic and is not already a resource reference, with an explicit allowlist for the genuine non-UI cases (log lines, pref keys, test ids). Strictest, and it fails closed — but needs an allowlist seeded once.
The third is the only one that fails closed. The second is probably the best effort/robustness trade.
No urgency on the seven sites themselves — they are short labels. The gate is the point.
Activity
i18n: real <plurals> instead of hand-rolled singular/plural pairs (#541)
i18n_audit: catch UI literals hidden behind if/else, when, and .let (#…
Health: say WHY a vital tile is empty on Android, not just "No data" (#…
i18n: close the gaps the merged, stricter audit surfaced across the c…
i18n: real <plurals> instead of hand-rolled singular/plural pairs (ry…
i18n_audit: catch UI literals hidden behind if/else, when, and .let (r…
i18n: close the gaps the merged, stricter audit surfaced across the c…
Health: say WHY a vital tile is empty on Android, not just "No data" (r…
i18n: real <plurals> instead of hand-rolled singular/plural pairs (ry…
i18n_audit: catch UI literals hidden behind if/else, when, and .let (r…
Health: say WHY a vital tile is empty on Android, not just "No data" (r…
i18n: mask Swift comments before scanning for copy
fix(i18n): translate the strength, app-lock and export copy