docs(i18n): name the oracle's blindness to attribute-borne copy (#2208)

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
João Salavisa
2026-08-03 15:16:33 +02:00
committed by GitHub
parent aae550705c
commit 0cfdf86d02
+30 -8
View File
@@ -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 `<n>` 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