From 0cfdf86d0253614b4e8b629b6282b37f74b78a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Salavisa?= Date: Mon, 3 Aug 2026 15:16:33 +0200 Subject: [PATCH] docs(i18n): name the oracle's blindness to attribute-borne copy (#2208) Co-authored-by: Kandev Agent Co-authored-by: Claude Opus 5 (1M context) --- docs/i18n.md | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/docs/i18n.md b/docs/i18n.md index 1e756511cb..eb4fb5c3ee 100644 --- a/docs/i18n.md +++ b/docs/i18n.md @@ -403,15 +403,37 @@ through `t()` — that is the acceptance check for a migrated screen, and the on one that sees the guard's blind spots. Interpolation placeholders and `` tag markers are preserved by the generator. -Two qualifications, both below: an **interpolated API or network payload** stays -English by design, so it is not a miss even though it renders unaccented; and -plain English is not a *complete* signal, because a payload inside a translated -frame renders accented and reads as done. +One thing it shows you is **not** a miss: an interpolated API or network payload +stays English by design, so it renders unaccented and is still correct. See +[the interpolated-value limit](#and-it-is-weakest-at-an-interpolated-value). -It has one blind spot of its own: **module-scope `t()`**. That copy *is* -translated, just frozen at the boot locale, so it renders accented under pseudo -and looks correct while never responding to a switch. `check-module-scope-t.mjs` -is what catches it. +Three things it cannot show you at all — the second and third are detailed below: + +1. **Module-scope `t()`.** That copy *is* translated, just frozen at the boot + locale, so it renders accented and looks correct while never responding to a + switch. `check-module-scope-t.mjs` is what catches it. +2. **Copy that never becomes a text node** — an attribute string leaves no trace + on screen at all. +3. **An un-migrated value interpolated into a migrated frame** — it renders + inside accented copy and reads as done. + +### It cannot see copy that is not a text node + +`findUnlocalizedText` in `e2e/tests/i18n/pseudo-coverage.spec.ts` walks +`NodeFilter.SHOW_TEXT`, and a human looking at the screen is no different: +**copy carried in an attribute — `aria-label`, `aria-description`, `title`, +`placeholder`, `alt` — renders nothing visible and leaves no trace under +pseudo.** The Settings → System → Storage review demonstrated it by reverting an +`aria-label` to an English template literal; nothing failed. + +The guard is the primary defence here, not the oracle: those five attributes are +in `jsx-attributes.include`, so on an allowlisted path the lint rule does flag +them. The consequence is that for attribute copy there is **no second check** — on +a path not yet allowlisted, or for a fragment the `words.exclude` patterns skip, +nothing downstream catches it. The copy only screen-reader users receive is +therefore the copy with the weakest safety net. Read attribute props by eye when +you migrate, and prefer a `t()` call over a template literal in them so the value +stays greppable. ### …and it is weakest at an interpolated value