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:
+30
-8
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user