docs(i18n): consumer sweeps, destructuring defaults, and unowned shared copy (#2204)

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 16:04:02 +02:00
committed by GitHub
parent 03d5f98cb6
commit d9ea08af82
+61
View File
@@ -296,6 +296,23 @@ 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.
**Default values in a destructuring pattern are a third invisible shape, and the
fix is not the obvious one.** `ActionButtonContent` carried three:
```tsx
export function ActionButtonContent({
pendingLabel = "Running...", // NOT flagged — not a literal in JSX
successLabel = "Done",
errorLabel = "Failed",
}: Props) {
```
These are evaluated in the parameter list, which runs *before* the component
body, so a `const { t } = useTranslation()` in the body is not in scope for them —
you cannot fix this in place. Resolve the fallback inside the body instead
(`const pending = pendingLabel ?? t("system:jobRunning")`). Prop defaults on a
shared presentational component are a common home for this.
`externalize-strings.mjs`
covers those shapes — it walks `.ts` as well as `.tsx` and handles template
literals and dialog/toast arguments — but **a zero from `pnpm lint` does not by
@@ -490,6 +507,50 @@ 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.
### Walk it by consumer, not by route
Which screens you walk matters as much as what you look for. A component's copy
is verified by walking **every route that mounts it** — not the routes your
migration owns.
The natural method fails here. Walking the import closure *from your own pages*
traverses the graph in the wrong direction: it finds what your routes render, and
by construction never finds a route that renders *you*. One migration hit it in
both directions — the cards it converted mount only on a sibling migration's
route, and a card that sibling owned mounts on its own. Neither could have been
verified from its own pages alone.
`job-progress-indicator.tsx` is the worked example. It reported **0** findings,
because its `Queued` / `Running` / `Done` / `Failed` come out of a plain `switch`
in a non-JSX function, and it has four consumers spanning two directories
(`database-stats-card`, `disk-usage-card`, `backups-table`, and Storage's
`storage-quarantine-card`). Three separate migrations rendered it, and each left
it — a component every one of them touched, and none of them owned. The System
migration finally took it and walked Storage's already-merged route to confirm.
Per component you touch:
```bash
grep -rn '<JobProgressIndicator' apps/web/app apps/web/components
```
then walk each of those routes under pseudo, whoever owns them. If a consumer
belongs to an unmigrated directory, note it in the guard comment rather than
silently leaving it — the next migration inherits the answer instead of
rediscovering the question.
### Copy that belongs to no directory
A file shared by *every* area has no natural owner and so gets skipped by all of
them. `src/settings-routes.tsx` holds the live title and description for every
settings route in one `SETTINGS_ROUTES` map — a SCREAMING_CASE identifier the
guard skips entirely, in a 650-line file no single migration's closure claims.
The rule: a migration converts the entries it owns but must **not** add such a
file to `i18nGuardFiles`, because allowlisting it claims a completeness nobody
has. The last area to migrate adds it. Say so in your guard comment, naming which
entries are still English and who owns them.
## Adding a real language
1. Add the locale to `SUPPORTED_LOCALES` + `LOCALE_LABELS` in