Repository navigation
i18n_audit: catch contentDescription inside a semantics {} lambda (fixes #571) - #581
Merged
Merged
Conversation
…#571) #558 fixed conditional literals but left a different axis invisible: a UI-text kwarg assigned INSIDE a lambda. `contentDescription` is in ANDROID_KWARG_PATTERN, but that pass is gated on an arg boundary (`(` or `,`), so the standard Compose a11y idiom Modifier.semantics { contentDescription = if (hr) "Ready" else "No data" } was skipped — its `{` isn't an arg boundary — and the audit reported clean while TalkBack read English regardless of device language. Adds a dedicated assignment pass: a bare `contentDescription =` (not a `==` comparison, a `.member` read, or a `val`/`var` local decl) is a UI-text site wherever it's assigned. It reads only its own value span via _argument_span_end (which stops at the enclosing `}`), so it does NOT reopen unrelated slot lambdas — the 489-explosion guard still holds. Surfaces 22 pre-existing hardcoded a11y strings across 9 screens (AppRoot, CoachScreen, FusedRecordScreen, HealthScreen, PipBar, SettingsScreen, StepsCalibrationScreen, UpdatesInboxScreen, WorkoutsScreen). Per the #558 pattern these land in the baseline as tracked backlog (170 -> 192 android) rather than forcing 22 rushed translations; --ci stays green and they get localized incrementally. NO new hardcoded literal can still land. Tested (Tools-only Python, fully runnable — no app-target/CI gap): python3 -m unittest test_i18n_audit -> 26/26 (4 new: semantics idiom, multi-line, decl/member exclusion, and the no-reopen-unrelated-slot guard) python3 Tools/i18n_audit.py --ci origin/main -> exit 0
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
…ryanbr#571) (ryanbr#581) ryanbr#558 fixed conditional literals but left a different axis invisible: a UI-text kwarg assigned INSIDE a lambda. `contentDescription` is in ANDROID_KWARG_PATTERN, but that pass is gated on an arg boundary (`(` or `,`), so the standard Compose a11y idiom Modifier.semantics { contentDescription = if (hr) "Ready" else "No data" } was skipped — its `{` isn't an arg boundary — and the audit reported clean while TalkBack read English regardless of device language. Adds a dedicated assignment pass: a bare `contentDescription =` (not a `==` comparison, a `.member` read, or a `val`/`var` local decl) is a UI-text site wherever it's assigned. It reads only its own value span via _argument_span_end (which stops at the enclosing `}`), so it does NOT reopen unrelated slot lambdas — the 489-explosion guard still holds. Surfaces 22 pre-existing hardcoded a11y strings across 9 screens (AppRoot, CoachScreen, FusedRecordScreen, HealthScreen, PipBar, SettingsScreen, StepsCalibrationScreen, UpdatesInboxScreen, WorkoutsScreen). Per the ryanbr#558 pattern these land in the baseline as tracked backlog (170 -> 192 android) rather than forcing 22 rushed translations; --ci stays green and they get localized incrementally. NO new hardcoded literal can still land. Tested (Tools-only Python, fully runnable — no app-target/CI gap): python3 -m unittest test_i18n_audit -> 26/26 (4 new: semantics idiom, multi-line, decl/member exclusion, and the no-reopen-unrelated-slot guard) python3 Tools/i18n_audit.py --ci origin/main -> exit 0
colpa176
added a commit
to colpa176/personalnoop
that referenced
this pull request
Aug 2, 2026
Daily screen time arrives via a noop://import-screentime deep link, following ryanbr#581's discipline: version-gated, strictly validated, staged for explicit confirmation before any write, and stored under a dedicated source id that is neither a strap nor an Apple source. The double-tap trigger needed no code — FrameRouter already decodes DOUBLE_TAP and MacActionKind.runShortcut already fires shortcuts://run-shortcut?name=<name> from Automations, so a double-tap can run the Shortcut that reads the number and opens this link. This is the receiving end. Adds the screen_time metric under Mind, so it gets the standard detail page, dot/line trend and correlation scan with no per-metric UI. App build (macOS+iOS), i18n Coverage and Source Hygiene green on the branch. # Conflicts: # Strand/Resources/Localizable.xcstrings
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…ryanbr#571) (ryanbr#581) ryanbr#558 fixed conditional literals but left a different axis invisible: a UI-text kwarg assigned INSIDE a lambda. `contentDescription` is in ANDROID_KWARG_PATTERN, but that pass is gated on an arg boundary (`(` or `,`), so the standard Compose a11y idiom Modifier.semantics { contentDescription = if (hr) "Ready" else "No data" } was skipped — its `{` isn't an arg boundary — and the audit reported clean while TalkBack read English regardless of device language. Adds a dedicated assignment pass: a bare `contentDescription =` (not a `==` comparison, a `.member` read, or a `val`/`var` local decl) is a UI-text site wherever it's assigned. It reads only its own value span via _argument_span_end (which stops at the enclosing `}`), so it does NOT reopen unrelated slot lambdas — the 489-explosion guard still holds. Surfaces 22 pre-existing hardcoded a11y strings across 9 screens (AppRoot, CoachScreen, FusedRecordScreen, HealthScreen, PipBar, SettingsScreen, StepsCalibrationScreen, UpdatesInboxScreen, WorkoutsScreen). Per the ryanbr#558 pattern these land in the baseline as tracked backlog (170 -> 192 android) rather than forcing 22 rushed translations; --ci stays green and they get localized incrementally. NO new hardcoded literal can still land. Tested (Tools-only Python, fully runnable — no app-target/CI gap): python3 -m unittest test_i18n_audit -> 26/26 (4 new: semantics idiom, multi-line, decl/member exclusion, and the no-reopen-unrelated-slot guard) python3 Tools/i18n_audit.py --ci origin/main -> exit 0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses the inline case of #571. Tools-only Python, so unlike the recent app-target PRs it's fully tested here — 26/26 and
--ciclean, no verification gap.The gap
#558 taught the audit to see conditional literals (
Text(if x "a" else "b")), but a UI-text kwarg assigned inside a lambda stayed invisible.contentDescriptionis already inANDROID_KWARG_PATTERN, but that pass is gated on an argument boundary ((or,), so the standard Compose idiomis skipped — its
{isn't an arg boundary — and the audit reports clean while TalkBack reads English regardless of device language.The fix
A dedicated assignment pass (
ANDROID_A11Y_ASSIGN_PATTERN): a barecontentDescription =— not a==comparison, a.memberread, or aval/varlocal declaration — is a UI-text site wherever it's assigned. It reads only its own value span via_argument_span_end(which stops at the enclosing}), so it does not reopen unrelated slot lambdas. The 489-explosion guard from #558 still holds (there's a test for exactly that).Surfaces 22 pre-existing hardcoded a11y strings across 9 screens (AppRoot, CoachScreen, FusedRecordScreen, HealthScreen, PipBar, SettingsScreen, StepsCalibrationScreen, UpdatesInboxScreen, WorkoutsScreen) — all
contentDescription = "literal"/= if …forms. Per the #558 pattern these land in the baseline as tracked backlog (170 → 192 android), not 22 rushed translations;--cistays green and a genuinely new hardcoded literal still fails the gate.Known limitation — val-laundered a11y is still NOT caught (correction to my first draft)
On re-review I confirmed this does not catch the specific case #571 cited as motivation.
CoupledScreen.ktuses:The assignment's value is the variable
a11y, so my pass finds no literal there; the literals live in theval a11y = when { … }declaration, which is deliberately not scanned (the #558 local-decl exclusion, there to avoid flagging aval text = when{}that isn't UI). Catching this needs light dataflow — "scan avalwhose value flows into acontentDescription" — which is a distinct, more false-positive-prone change. So CoupledScreen's a11y strings remain invisible after this PR, and I'd keep #571 open (or spin a narrower follow-up) for the val-laundered variant rather than have this close it.A few of the 22 are pure-interpolation (
"$label: $valueText") that may need no translation — the baseline is a snapshot to triage, not a mandate.Verification
python3 -m unittest test_i18n_audit→ 26/26 (4 new: the semantics idiom, multi-line, decl/member-read exclusion, the no-reopen-unrelated-slot guard)python3 Tools/i18n_audit.py --ci origin/main→ exit 0