I18n changes
Skill andymai/gridfinity-layout-tool/.claude/skills/i18n-changes
Adding/renaming/removing translation keys in src/i18n/locales/en.ts and the 14 non-English locale JSONs, fixing the four i18n check failures ("i18n key mismatch detected", "Untranslated values detected", "interpolation mismatch detected", "Unused i18n keys detected"), the missing en.json TS2307 error, the values allowlist, plurals without ICU, and adding a new locale.From its SKILL.md
npx -y skills add andymai/gridfinity-layout-tool --skill i18n-changesAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
SKILL.md
10.0 KB, as published. Nobody here has run it
i18n Changes
When to use
- Adding, renaming, or removing any user-facing string (nearly every feature PR).
- A pre-commit or CI quality-job step starting with
check:i18nfailed. pnpm run typecheckfails withTS2307: Cannot find module './locales/en.json'.- UI shows a raw key (
feature.thing), a literal{count}, or an ICU block.
Mental model
src/i18n/locales/en.tsis the hand-edited source of truth (~2500 flat dotted keys).src/i18n/locales/en.jsonis a gitignored build artifact regenerated bypnpm run build:en-locale(also runs automatically insidedevandbuild). Never hand-edit en.json; on a fresh clone, generate it before typecheck.- The translated locale JSONs (cs, de, es, fr, it, ja, ko, nb, nl, pl, pt-BR, sv, uk, zh-CN — 14 today) must each have exactly the same key set as en.ts. Never trust a memorized count: both check scripts
readdirSyncthe locales dir and skipen.json, so a new locale is picked up the moment its JSON lands.SUPPORTED_LOCALESinsrc/i18n/types.ts(15 incl. en) is the matching runtime list. - All four check scripts parse en.ts with regex, never import it. Keep en.ts regex-parseable: single-quoted keys, plain quoted string values, no template literals, no concatenation, no escaped quotes (use double-quoted values for apostrophes). A value the regex can't match silently disappears from every check.
- Runtime interpolation (
interpolate()insrc/i18n/context.tsx) is literal{name}replaceAll. ICU is never evaluated even though the interpolation check tolerates its syntax. Plurals = two keys + a ternary in code. t()fallback istranslations[key] || _loadedEn?.[key] || key— empty string counts as missing; you cannot intentionally blank a string per-locale.- Gridfinity domain terms stay in English in all locales: bin, drawer, layer, staging/stash, grid unit, height unit, print bed, Gridfinity (see the en.ts header comment).
Recipe: add a string
- Add
'feature.context.element': 'Value',tosrc/i18n/locales/en.tsunder the matching//section comment. - Add the same key, translated, to every non-English locale JSON. If a locale's value is legitimately identical to English (brand name, format string, domain term), add the key to the
"keys"array inscripts/i18n-values-allowlist.json. Identical values ≤3 chars without spaces (OK,3MF) or matching avaluePatternsregex (placeholder-only strings) auto-pass. - In code:
const t = useTranslation();thent('feature.context.element')ort('key', { count })— the param name must equal the{count}token exactly. Outside React (workers, ErrorBoundary) usegetStaticTranslationfrom@/i18n. - Plurals: two keys, e.g.
'baseplate.bedLoads.one': 'Prints in 1 build-plate load'and'baseplate.bedLoads.other': 'Prints in {count} build-plate loads', selected by a ternary at the call site (seesrc/features/baseplate/components/BaseplatePage/BaseplatePage.tsx~line 326). Never{count, plural, ...}. - Verify (see below).
Recipe: rename or remove a key
rg "old\.key" src/— catch data-structure refs (labelKey: 'x.y') and ternary keys, not justt()calls.- Change en.ts, every non-English JSON, and every code ref in the same commit. Touching only en.ts produces "Missing" (add) or "Extra ... (not in en.ts)" (remove) per locale.
- Merging duplicates:
pnpm exec tsx scripts/consolidate-i18n-key.ts --from old.key --to keep.key --dry-run— refuses unless both keys are identical in every locale; drop--dry-runto apply, then update the code refs it lists. - Because you touched
src/i18n/, pre-commit WILL run the unused check and may surface unrelated pre-existing debt — fix it, don't bypass.
Recipe: add a locale
- Create
src/i18n/locales/<code>.jsonwith every en.ts key translated (use en.json as template). src/i18n/types.ts: extend theLocaleunion and append toSUPPORTED_LOCALES(renders in the language selector).src/i18n/context.tsx: add entries tolocaleLoadersandOG_LOCALE_MAP(underscore OG format, e.g.sv_SE) — typecheck errors on bothRecord<Locale, ...>until you do.src/i18n/detection.ts: addLANGUAGE_MAPentries for the base code and regional variants.pnpm run typecheck && pnpm run check:i18n && pnpm run check:i18n:values.
Verification
pnpm run build:en-locale
pnpm run check:i18n && pnpm run check:i18n:values && \
pnpm run check:i18n:interpolation && pnpm run check:i18n:unused
Each failure prints a distinct ❌ line naming exact keys/files/lines. Pre-commit runs checks 1–3 always and the unused check only when src/i18n/ files are staged (.husky/pre-commit); the CI quality job runs each with continue-on-error and aggregates, so read the whole log — there may be several failures at once. After changing context.tsx/detection.ts: pnpm run test:run src/i18n.
For the other pre-commit gates (boundaries, design-system, exhaustiveness, missing-tests), see the quality-gates skill. For where strings live inside a feature slice, see the feature-slices skill.
Traps
| Symptom | Cause | Fix |
|---|---|---|
TS2307: Cannot find module './locales/en.json' at context.tsx | en.json is gitignored; fresh clone has none | pnpm run build:en-locale |
❌ i18n key mismatch detected | Key set differs between en.ts and a locale JSON (typos show as Missing + Extra) | Apply the exact per-file Missing/Extra list |
❌ Untranslated values detected | Locale value copied from English, misses allowlist heuristics | Translate it, or allowlist the key if identity is intentional |
❌ i18n interpolation mismatch detected | {name} token in en.ts ≠ param name at the t() call (output gives file:line) | Make token and param identical — fix whichever side is wrong |
❌ Unused i18n keys detected blocks a commit that only added one key | An earlier PR deleted the last ref; the gated check fires on the next i18n toucher | Delete the listed definitely-unused keys from en.ts + all JSONs; ⚠️ possibly-unused (dynamic-prefix) lines are warn-only |
UI renders literal {count} or a raw ICU block | Runtime is plain replaceAll; the interpolation check only sees literal t('key', ...) calls — ternary/dynamic keys are invisible to it | Pass the vars object; replace ICU with .one/.other keys + ternary. 'collab.participantCount' in en.ts is a live latent bug — do not copy it |
| UI shows raw dotted key, sometimes only briefly or only non-English | Key missing in active locale before the en chunk lazy-loads; pre-load resolves only the hardcoded fallback map in context.tsx (errorBoundary., seo., common.loading) | Run check:i18n for parity; keys needed outside React/before load must also go in that fallback map |
| Unrelated commit blocked by an i18n check | Checks scan whole files, not staged hunks | Fix or stash existing i18n debt first |
| Component test asserts translated text without a provider and passes | src/test/setup.ts globally mocks @/i18n with real en.ts strings | Expected; locale-switching/fallback behavior is only tested in src/i18n/context.test.tsx |
| Missing loading spinner on first page load after a locale change | Initial mount deliberately renders children with the fallback map to avoid CLS; spinner appears only on explicit setLocale() | Not a bug — don't "fix" it |