docs(i18n): mark the directory-by-directory migration complete (#2453)
Co-authored-by: Kandev Agent <agent@kandev.dev>
This commit is contained in:
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user