test(i18n): guard against locale keys English has dropped - #71233
test(i18n): guard against locale keys English has dropped#71233joelbrilliant wants to merge 1 commit into
Conversation
defineLocale() deep-merges a locale's overrides onto en and makes every key optional, so a partially translated locale falls back to English. That is deliberate. What it hides is the opposite case: a locale key English NO LONGER HAS. The stale override survives the merge and wins over English. TypeScript cannot catch it. Blocks like composer.commandDescs are typed Record<string, string>, so any key satisfies the type - verified by re-adding a removed key and watching tsc --noEmit still exit 0. That is not cosmetic. NousResearch#66646 removed terminal-only commands (/clear, /details, /copy, /quit) from the desktop quick-help drawer because the desktop refuses them, but a locale still defining those keys kept advertising commands that do not work - the very bug the change fixed, staying live in one language while every other locale was corrected. The guard fails on exactly that: a key a locale defines that English does not have. Missing keys are NOT flagged, since falling back is the design. Two exemptions, both documented, because the guard cannot protect them: - blocks English leaves deliberately empty (messaging.platformIntro is {}) - runtime contribution registries (sidebar.nav, SIDEBAR_NAV_AREA in app/routes.ts), where plugins register the ids Running it on current main found two genuinely dead keys in ar.ts, both removed here after checking the app for any remaining reference: - keybinds.actions 'view.closePreviewTab' - no such action exists - onboarding.flowSubtitles 'loopback' - desktop handles pkce, device_code and external only 39 passed in src/i18n, tsc clean, lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
c5ca399 to
316007f
Compare
|
Thanks for adding a behavioral invariant rather than a locale snapshot. Current Automated hermes-sweeper review. |
|
Thanks for checking the stale-key premise and the behavioural guard against current main. The current head is |
The gap
defineLocale()deep-merges a locale's overrides ontoenand makes every key optional, so a partially translated locale falls back to English. That's deliberate and good.What it hides is the opposite case: a locale key English no longer has. The stale override survives the merge and wins over the English value.
TypeScript can't catch it. Blocks like
composer.commandDescsare typedRecord<string, string>, so any key satisfies the type. Verified on this repo — re-adding a removed key to a locale leavestsc --noEmitexiting 0.Why it's worth a guard
This isn't hypothetical. #66646 removed terminal-only commands (
/clear,/details,/copy,/quit) from the desktop quick-help drawer because the desktop refuses them. A locale that still defined those keys kept advertising commands that don't work — the exact bug that change fixed, staying live in one language while every other locale was corrected. Nothing failed.Any long-lived branch touching i18n has this landmine, because a locale added after the branch was cut inherits English silently.
What it checks
Fails when a locale defines a key English doesn't have. Missing keys are not flagged — falling back is the design.
Two exemptions, both documented in the file, because the guard genuinely can't protect them:
messaging.platformIntrois{}, an extension point locales populate.sidebar.navisSIDEBAR_NAV_AREAinapp/routes.ts; plugins register the ids, so extra locale labels there aren't evidence of a removal.I kept that list to two and named the reason for each, since every entry is somewhere the guard stops working.
What it found
Running it on current main surfaced two genuinely dead keys in
ar.ts, both removed here after checking the app for any remaining reference:keybinds.actions→'view.closePreviewTab'onboarding.flowSubtitles→'loopback'pkce,device_code,externalonlyBoth would render to Arabic users as descriptions of things that don't exist.
Tests
The walker itself is unit-tested alongside the parity check: reports extra keys, reports nested keys with a dotted trail, does not report missing keys, skips empty blocks, skips registry paths, and ignores functions/arrays.
39 passed in
src/i18n, tsc clean, lint clean.