docs(i18n): record the two blind spots the Agents and Storage migrations hit (#2205)

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 14:58:40 +02:00
committed by GitHub
parent 1e5fce99c4
commit aae550705c
+69 -1
View File
@@ -282,7 +282,19 @@ That is why module-scope config objects survived the sweep, and why the
new-code ratchet inherits the same hole. `check-module-scope-t.mjs` catches the
adjacent case (a module-scope `t()` call, frozen at the boot locale) but not
module-scope literal copy. Reviewing a new `const TABLE = [...]` of labels by eye
is still required.
is still required — and note the shape one step out from a config table is just
as invisible: **a plain function returning object literals**, e.g. a
`buildRows()` that returns `[{ label: "Disk usage" }]`, is never inspected either.
**Walk the hooks your routes call, not just the components.** A closure derived
from component imports misses them, and they hold no JSX so the string count
reports them as zero. Settings → Agents shipped that way and needed a follow-up
for a thrown `Error` and a `setError` fallback in two `hooks/domains/settings/`
modules; Settings → System → Storage caught ten toast titles in
`use-storage-maintenance.ts` only because it re-derived its closure instead of
trusting the count. Install/update flows and any maintenance action are the
toast-heavy cases. A count from `pnpm run lint:i18n` is a floor on the visible
half, never a work-list.
`externalize-strings.mjs`
covers those shapes — it walks `.ts` as well as `.tsx` and handles template
@@ -391,11 +403,67 @@ 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.
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.
### …and it is weakest at an interpolated value
Plain-English-on-screen is not a complete signal. When an un-migrated value is
interpolated into a **migrated** frame, the frame renders accented and the
payload reads as part of the sentence:
```text
agents:unableToStartUpdate = "Unable to start update: {{error}}"
→ Ữńàƀĺē ţō śţàŕţ ūþďàţē: Agent installation is already in progress.
```
That scans as done. Both instances found this way survived a twelve-screen pseudo
walk of the migrated surface before anyone noticed them.
So when a message interpolates a value, ask **where the value originates**. That
distinction is the whole check, and it is a two-minute grep over the paths you
migrated:
```bash
cd apps/web
grep -rnE 't\("[^"]+", \{ (error|message)' components app hooks | grep -v '\.test\.'
```
- **An API or network error payload stays English by design.** `String(err)` from
a failed request, or a `result.error` the backend returned, is a diagnostic —
see [Backend](#what-the-backend-deliberately-does-not-translate) for why the
~1,100 server error strings are out of scope. Around 37 catalog keys already
interpolate one; none of them is a miss. Where such a call needs a fallback for
a missing payload, the *fallback* is copy and is translated
(`t("linear:testFailed", { error: result.error || t("linear:unknownError") })`)
while the payload itself is not.
- **A client-side literal reaching a translated frame is a miss.** A string
thrown, `setError`'d, or assigned in one of your own hooks or helpers is copy
you wrote, and neither the lint rule nor the pseudo-locale can see it.
**A translated toast followed by an English `throw` is correct, not a miss.**
This shape recurs across the migrated GitHub and Jira settings and should not be
churned:
```tsx
} catch {
toast({ description: t("github:failedToSaveQuickActions"), variant: "error" });
// Signals failure to the settings save coordinator, which only records the
// contributor id — this message never reaches a user, so it is not copy.
throw new Error("Failed to save quick actions");
}
```
The user already saw translated copy; the throw is control flow for
`useSettingsSaveContributor`. Translating it would change nothing on screen.
While the migration is in flight, an unmigrated screen is expected to be full of
plain English — pseudo only proves anything about screens whose components are on
the `i18nGuardFiles` allowlist.