feat(i18n): localize Settings → Workspace (#2212)
Co-authored-by: Kandev Agent <agent@kandev.dev>
This commit is contained in:
@@ -159,6 +159,7 @@ pnpm run i18n:check # the four repo-wide gates (runs in CI: Frontend Tests)
|
||||
pnpm run i18n:pseudo # regenerate the pseudo catalog from en
|
||||
pnpm run lint:i18n <path> # preview the guard on a path that is NOT yet migrated
|
||||
pnpm run i18n:ratchet # new-code guard + allowlist-only-grows (pre-commit + CI)
|
||||
pnpm run i18n:removed # did this change rewrite any English? (diff-based, advisory)
|
||||
|
||||
# Codemods — always inspect the diff; each declines what it cannot prove safe.
|
||||
node scripts/externalize-strings.mjs --write components app hooks lib
|
||||
@@ -643,6 +644,60 @@ the guard: a table of every route's title and description as English, asserted
|
||||
byte-for-byte. It converts "I diffed them carefully" into something that stays
|
||||
true after you leave.
|
||||
|
||||
`pnpm run i18n:removed` is the **mechanical form of that diff**, and it needs no
|
||||
foresight about which strings to pin. It compares the strings present before and
|
||||
after your change and reports any that left the source without reappearing as a
|
||||
catalog value — so it covers every file in the change, not the one that happened
|
||||
to have a spec. On the case above it reports the live sentence immediately,
|
||||
because `Create a diagnostic ZIP…` is nowhere in any catalog.
|
||||
|
||||
It parses with Babel rather than matching quoted strings, because **JSX text
|
||||
carries no quotes** — `<Link>Back to Workspaces</Link>` is most migrated copy and
|
||||
a regex cannot see it. Three acceptance rules keep it quiet on a clean
|
||||
migration: a value extracted into `{{interpolation}}`, either half of a `<Trans>`
|
||||
sentence split around an element, and an `_one`/`_other` pair.
|
||||
|
||||
**The hazard has two shapes, and only one of them is above.** The case in this
|
||||
section is an *attribute* — `description="…"` on a route shell — which is
|
||||
quoted, so even a crude scan finds it. The other is *element text*, which is
|
||||
not quoted and is where most migrated copy lives; a reused key that rewrites a
|
||||
button or a link label leaves nothing for a quoted-string search to find. Do not
|
||||
read this section as "route descriptions drift". Any copy rendered from two
|
||||
places can.
|
||||
|
||||
It is **advisory and deliberately not in CI**: it is diff-based, so it needs a
|
||||
base ref (`i18n:check` is repo-wide on purpose), and a finding is a prompt to
|
||||
*classify* rather than proof of a bug — each is a value extraction, a literal
|
||||
hoisted to a constant, or the real thing. It narrows the diff for a human; it
|
||||
does not decide. `--base <ref>` overrides the `origin/main` fork point, and
|
||||
`--all` widens past prose to identifier-shaped strings.
|
||||
|
||||
**Its coverage is partial, and a clean run is evidence rather than proof.** Rule
|
||||
2 is only as tight as the literal text around a message's placeholders, so a
|
||||
message with too little of it earns no pattern at all — it needs two words of
|
||||
prose. That threshold is not arbitrary. One short word clears any "is there a
|
||||
word?" bar while leaving the pattern almost unbounded: `"Delete {{name}}"`
|
||||
compiled to `/^Delete [\s\S]+?$/` and accepted *every* sentence beginning
|
||||
"Delete ", which left 19 of 43 strings unprotected in the first file this check
|
||||
was pointed at.
|
||||
|
||||
Measured by simulating a rewrite of every removed string, requiring two words
|
||||
took detection from 79 to **89 of 96** on one migration while adding **zero**
|
||||
findings to either of two real diffs. The seven that remain are messages whose
|
||||
prose is genuinely too thin to distinguish a rewrite from anything else, so
|
||||
treat a clean run as evidence, not proof — that is why the mutation probe above
|
||||
is a separate step, and why "the check was clean" is not on its own an answer to
|
||||
"did the English change?".
|
||||
|
||||
One limit is structural rather than a tuning choice, and is worth knowing before
|
||||
anyone tries to close it: `"{{count}} of {{total}}"` (a legitimate value
|
||||
extraction) and `"{{a}} of {{b}}"` (far too permissive to anchor anything) both
|
||||
reduce to the residue `"of"` and compile to the *same* regex. Nothing computed
|
||||
from the message can accept one and reject the other. Both are rejected, so a
|
||||
string like `"3 of 9"` is reported and classified by hand — the right direction
|
||||
for an advisory check, since a false positive costs someone ten seconds and a
|
||||
false negative is the failure the check exists to prevent.
|
||||
|
||||
The underlying defect is the duplication itself. Unreferenced pages that
|
||||
re-render live copy will drift again, and the next migration inherits the same
|
||||
trap — which is why deleting a dead twin is a correctness change, not tidiness.
|
||||
|
||||
Reference in New Issue
Block a user