docs(i18n): mark the directory-by-directory migration complete (#2453)

Co-authored-by: Kandev Agent <agent@kandev.dev>
This commit is contained in:
João Salavisa
2026-08-09 00:51:14 +02:00
committed by GitHub
parent 46021e7d23
commit b41ba97d10
4 changed files with 32 additions and 26 deletions
+4 -4
View File
@@ -89,11 +89,11 @@ This is enforced, not just requested. Two ratchets, both of which only tighten:
hardcoded string in a file you added, or on a line you changed, regardless of
directory. Untouched lines are never judged, so you are never asked to migrate
code you did not write.
- **Migrated paths, whole file** — `i18next/no-literal-string` is a lint error for
- **Listed paths, whole file** — `i18next/no-literal-string` is a lint error for
the paths in `i18nGuardFiles` (`apps/web/eslint.i18n.options.mjs`). The
migration of existing strings proceeds **one directory per PR**: when you
migrate one, append it to that list in the same PR. Never remove an entry to
make a build pass — a check rejects that.
migration of existing strings is **complete**, and the list covers every area it
touched; append a path in the same PR that adds it or externalizes one still
off the list. Never remove an entry to make a build pass — a check rejects that.
Three rules that cause silent, hard-to-find bugs when broken: never translate a
string compared with `===` (type-to-confirm tokens become impossible to type);
+11 -11
View File
@@ -188,10 +188,10 @@ surface.
## Internationalization (i18n)
**The migration is in progress, one directory per PR.** The runtime, the gates,
and Settings → General → Appearance are done; most of the app still holds English
literals. New user-facing copy must go through `t()` / `<Trans>` wherever you
write it, even in a directory that has not been migrated yet.
**Externalization is complete.** #2367 localized `app/office`, the last
un-migrated area. A hardcoded user-facing literal is now a regression, not
leftover migration work. New copy must go through `t()` / `<Trans>` wherever you
write it.
User-facing strings are localized with i18next + react-i18next, keyed as
`namespace:key`. Add the English text to `src/locales/en/<namespace>.json`, then
@@ -222,13 +222,13 @@ at render, or make the value a component. `check-module-scope-t.mjs` enforces it
`pnpm lint` fails on hardcoded UI strings (`i18next/no-literal-string` is an
**error**), but **only on the `i18nGuardFiles` allowlist** in
`eslint.i18n.options.mjs` — paths already migrated. That scoping is deliberate:
a repo-wide error breaks every unrelated PR that adds a label, which is what made
the first attempt at this migration unmergeable. **When you migrate a directory,
append it to `i18nGuardFiles` in the same PR** — that is the step that stops it
drifting back. Never delete an entry to make a build pass. Use
`pnpm run lint:i18n <path>` to preview the guard on a path that is not on the
list yet.
`eslint.i18n.options.mjs`, which covers every area the migration touched. It
stays an allowlist because a repo-wide error breaks every unrelated PR that adds
a label — what made the first attempt unmergeable. **Append a path in the same PR
that adds it or externalizes it** (`lib/sidebar` is one still off the list).
Never delete an entry to make a build pass; `check-guard-allowlist.mjs` rejects
that unless the file is gone. Use `pnpm run lint:i18n <path>` to preview the
guard on a path not yet on the list.
Separately, `pnpm run i18n:ratchet` (pre-commit + CI) guards **new code
everywhere**, regardless of the allowlist: a file you added must be clean outright,
+5 -4
View File
@@ -52,10 +52,11 @@ const eslintConfig = defineConfig([
},
},
// Hardcoded user-facing strings. An ERROR, but only on the allowlist in
// eslint.i18n.options.mjs — paths already migrated, which is where a
// regression is real. A repo-wide error would break every unrelated PR that
// lands a literal; a warning would let migrated paths drift back. Each
// migration PR externalizes one path and appends it to `i18nGuardFiles`.
// eslint.i18n.options.mjs — the paths that render user-facing copy, which is
// where a regression is real. A repo-wide error would break every unrelated PR
// that lands a literal; a warning would let listed paths drift back. A PR
// appends to `i18nGuardFiles` when it adds such a path or externalizes one
// still off the list — `lib/sidebar` is one the screen sweep did not cover.
{
files: i18nGuardFiles,
// Test files build fixtures out of literal strings on purpose; guarding them
+12 -7
View File
@@ -17,11 +17,11 @@ Two enforcement layers, and it matters which is which:
- **New code is guarded everywhere.** Copy you add — in a new file, or on a line
you changed — must go through `t()` / `<Trans>` whatever directory it is in.
See [the new-code ratchet](#but-new-code-is-guarded-everywhere).
- **Whole files are guarded only once migrated.** The eslint literal rule errors
- **Whole files are guarded only once listed.** The eslint literal rule errors
across an entire file only for the paths in `i18nGuardFiles`, so you are never
asked to migrate lines you did not touch. See
[The guard is scoped](#the-guard-is-scoped-on-purpose) and
[Migrating a directory](#migrating-a-directory).
[Externalizing a path](#externalizing-a-path).
## TL;DR for adding a string
@@ -336,8 +336,8 @@ all three.
### …but new code is guarded everywhere
Scoping the whole-file guard leaves a hole: a brand-new component under an
un-migrated directory would be unchecked. `check-new-code-i18n.mjs` closes it by
Scoping the whole-file guard leaves a hole: a brand-new component under a path
that is not on the list would be unchecked. `check-new-code-i18n.mjs` closes it by
judging the **change** rather than the file:
| | Judged on |
@@ -352,12 +352,17 @@ this can be repo-wide without recreating the treadmill. It is the same contract
So there are two independent ratchets, and both only tighten:
- **`i18nGuardFiles`** — whole files, migrated paths. Grows one directory per PR.
- **`i18nGuardFiles`** — whole files, listed paths. Grew one directory per PR
through the migration; now grows when a PR adds a path that renders copy.
- **the new-code ratchet** — new lines, everywhere. Always on.
## Migrating a directory
## Externalizing a path
One directory per PR. The whole loop:
The directory-by-directory sweep of the screens is **finished** — #2367 localized
`app/office`, the last un-migrated area. This loop is what you still run for a
path you are adding, for a helper directory the sweep did not cover (`lib/sidebar`
is one), or to remediate a blind-spot find like the SCREAMING_CASE tables in PR
`#2409`:
```bash
cd apps/web