From b41ba97d10d7e5a37233506d482596e29faaadec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Salavisa?= Date: Sun, 9 Aug 2026 00:51:14 +0200 Subject: [PATCH] docs(i18n): mark the directory-by-directory migration complete (#2453) Co-authored-by: Kandev Agent --- AGENTS.md | 8 ++++---- apps/web/AGENTS.md | 22 +++++++++++----------- apps/web/eslint.config.mjs | 9 +++++---- docs/i18n.md | 19 ++++++++++++------- 4 files changed, 32 insertions(+), 26 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 588b72585d..85f08462ea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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); diff --git a/apps/web/AGENTS.md b/apps/web/AGENTS.md index 9af1bb93d8..9c83ad8a05 100644 --- a/apps/web/AGENTS.md +++ b/apps/web/AGENTS.md @@ -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()` / `` 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()` / `` 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/.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 ` 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 ` 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, diff --git a/apps/web/eslint.config.mjs b/apps/web/eslint.config.mjs index af5b19557a..38afc0b447 100644 --- a/apps/web/eslint.config.mjs +++ b/apps/web/eslint.config.mjs @@ -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 diff --git a/docs/i18n.md b/docs/i18n.md index acca044d2e..60207209ba 100644 --- a/docs/i18n.md +++ b/docs/i18n.md @@ -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()` / `` 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