You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
i18n audit: contentDescription set inside a semantics {} lambda is still invisible (a11y copy stays English) #571
Follow-up to #540 / #558. #558 fixed the conditional-literal blind spot (Text(if x "a" else "b")), but a different axis remains invisible: a UI-text kwarg set inside a lambda.
The gap
ANDROID_KWARG_PATTERN includes contentDescription, and the audit catches it as a normal call argument:
Foo(contentDescription =if (hr) "Recovery ready"else"Recovery, no data yet") // CAUGHT
But the standard Compose accessibility idiom sets it inside a semantics {} lambda, and that brace is opaque (correctly — semantics isn't a transparent keyword, so the anti-489-explosion split skips it):
Modifier.semantics { contentDescription =if (hr) "Recovery ready"else"Recovery, no data yet" } // MISSED
Verified against scan_android() on merged main: the plain-arg form is flagged, the semantics {} form is not, and the strings pass is_probably_ui_text — so it's a genuine scan gap, not the filter.
Why it matters
This is exactly the real bug behind the earlier review finding: CoupledScreen.kt builds three hardcoded-English a11y strings ("Recovery calibrating, $n of $seed nights", "Recovery, no data yet", …) and assigns them via .semantics { contentDescription = a11y }. A German TalkBack user hears English there. The Apple twin localizes it (String(localized:)); Android doesn't. After #558 the audit still reports clean for this class, so the baseline can't track it and it silently never gets localized.
Suggested fix
A targeted rule rather than reopening all braces: scan contentDescription = (and any other genuine UI-text kwarg) even inside a trailing/semantics lambda. That catches the a11y idiom without sweeping unrelated lambda content back in (the case #558 deliberately kept opaque). A regression test should assert Modifier.semantics { contentDescription = if (x) "A" else "B" } is flagged while AlertDialog(text = { Column { … } }) still isn't.
Not urgent — no correctness impact, only untranslated a11y copy — but worth tracking so it doesn't hide behind a green audit.
Val-laundered — val a11y = when { … } then semantics { contentDescription = a11y }, which is exactly what CoupledScreen.kt does. Still NOT caught: the assignment's value is a variable, so there's no literal at the contentDescription site, and the literals live in a val … = when{} that i18n_audit: catch UI literals hidden behind if/else, when, and .let (#540) #558 deliberately doesn't scan. Catching it needs light dataflow (scan a val whose value flows into a contentDescription), which is more false-positive-prone and deserves its own change.
So keeping this open for the val-laundered variant after #581 lands. The CoupledScreen a11y strings remain untracked/unlocalized until that's done.
Activity
ryanbr commentedon Jul 18, 2026
Scoping note after implementing the fix (#581): this issue splits into two cases, and #581 only handles the first.
semantics { contentDescription = if (x) "A" else "B" }. Fixed by i18n_audit: catch contentDescription inside a semantics {} lambda (fixes #571) #581 (22 real sites surfaced across 9 screens).val a11y = when { … }thensemantics { contentDescription = a11y }, which is exactly whatCoupledScreen.ktdoes. Still NOT caught: the assignment's value is a variable, so there's no literal at thecontentDescriptionsite, and the literals live in aval … = when{}that i18n_audit: catch UI literals hidden behind if/else, when, and .let (#540) #558 deliberately doesn't scan. Catching it needs light dataflow (scan avalwhose value flows into acontentDescription), which is more false-positive-prone and deserves its own change.So keeping this open for the val-laundered variant after #581 lands. The CoupledScreen a11y strings remain untracked/unlocalized until that's done.
i18n_audit: catch contentDescription set inside a semantics {} lambda (…
i18n_audit: catch contentDescription set inside a semantics {} lambda (…
i18n_audit: catch contentDescription set inside a semantics {} lambda (…
Make the widget say something to TalkBack instead of reading its own …
stress: say "resumes in the morning" when nothing is coming until mor…
stress: do not say Calibrating when the day has simply not started yet (