Uh oh!
There was an error while loading. Please reload this page.
fix(i18n): fail on an inline defaultValue that contradicts its en value, and align the 43 that do (#3810) - #4119
Merged
Conversation
…ue (#3810) `t(key, { defaultValue: 'English' })` renders that text only when i18next misses the key. Where `en` defines the key the pack value always wins, so the inline string is dead code — and 43 of them said something else than the sentence users read, which misleads every later reader and becomes the visible copy the day the key is renamed. `scripts/check-i18n-call-site-keys.mjs` (#3530) already parsed both halves; it now compares them and fails on any byte of difference. Hard rule, no baseline: the repo-wide census found 43 sites in 19 files out of 851 literal inline defaults, and all 43 are aligned to the pack in this commit, so there is no debt for a ratchet to hold. A defaultValue on a key `en` does NOT define stays legal and stays the `missing-key` rule's business alone — the two classes are disjoint by construction and pinned that way. Every alignment moved the call site, never `en.ts`: pack values are what users read, and editing one would oblige the same edit in nine other packs (check-i18n-en-drift.mjs, #3650). Six of the 43 differed only in an ellipsis (`...` vs U+2026). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 10, 2026 11:19
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#3810
Option B + A per the delegated ruling on the card, measurement first. Option C (banning inline defaults on existing keys) is not taken.
The measurement, which decided the shape
The extended check, run over the whole repo before anything was fixed:
43 sites in 19 files. Small enough to fix in this PR, so the rule lands hard, with no baseline — no ratchet file, no debt, a new divergence is red on the day it is written. That is the branch the ruling asked for, and the census is what chose it: the card measured
auth(8) and later comments measuredconsole(1) andperm/home(4); the remaining ~1700 call sites had never been scanned, and they held 30 more.Distribution — the card's guess that divergence clusters rather than spreads holds:
packages/app-shell/src/console/organizations/OrganizationsPage.tsxapps/console/src/pages/auth/RegisterPage.tsxapps/console/src/pages/auth/ForgotPasswordPage.tsxpackages/app-shell/src/console/home/HomePage.tsxapps/console/src/pages/auth/{LoginPage,VerifyEmailPromptPage}.tsxAll 8 rows the card tables are confirmed against current
origin/main(line numbers had shifted by one or two after #3546 slice three; theenvalues and the call-site texts are byte-identical to the card). Both sites of the repeatedauth.verifyEmail.resendFailedkey are covered.RegisterPage.tsxadds 5 more auth-family sites the card did not list — same namespace, same class.The gate (B)
scripts/check-i18n-call-site-keys.mjsgains a third failure class,default-value-drift: when a call site carries a literaldefaultValueand the key resolves to anenleaf, the two must be equal byte for byte.collectEnKeysnow returns leaf values as well as paths (folding the one'a' + 'b'concatenation inen.ts), which is the only new machinery — the call-site half was already parsed.Kept deliberately narrow, and each abstention is counted rather than silently dropped:
endoes not define is not reported here. That ismissing-key's territory, and the two classes stay disjoint so either report reads on its own — pinned by a test asserting the finding list for such a call site is exactly['missing-key'].defaultValue: label), a key that is dynamic or denotes several literals, areturnObjectssubtree, and the plural families (detail.showEmptyRelatedresolves through_one/_other; there is no single form to compare against). Today: 57 computed, 4 not comparable....against U+2026 and are invisible otherwise.The hint text points the fix at the call site and explicitly forbids editing
en.tsto silence it.The alignment (A)
All 43 call sites now repeat the pack value byte for byte.
packages/i18n/src/locales/en.tsis untouched — verified bycheck:i18n-drift, which reports0 en value(s) changed. Rows 3 and 6 of the card's table (the ellipsis-only pair) align to the pack's..., per the card-specific clause; the pack's own typography mixing is not this card's business.One correction to the card's premise
The card says the dead defaults are invisible to users. That is exact with an
I18nProvidermounted, which is every console screen — but not universally. With no provider and no initialised i18next instance, react-i18next's not-readytreturns thedefaultValue:So on a provider-less host the inline string was the rendered one, for the sites whose hook is
useObjectTranslationdirectly; sites reached through acreateSafeTranslationhook take a third path that ignores the inline default altogether and renders the raw key (#3865). Either way this PR converges the two hosts on the sentence the pack serves, and the changeset is apatchon that basis rather than "no user-visible change at all". It does not weaken the finding — it sharpens the second cost the card names, since two hosts of the same component were rendering two different sentences.Verification
pnpm exec vitest run scripts/__tests__/check-i18n-call-site-keys.test.ts— 34 passed (9 new).git checkout --of the script, patch re-applied after), 7 of the 9 new tests go red. The two that stay green are the ones that should: the disjointness test (it guards against the rule OVER-firing, so removing the rule cannot make it fail) and the "main carries no drift" stock assertion. Before the alignment the gate itself exited 1 with the 43 findings — the census output above is that red run.pnpm exec vitest run packages/app-shell/— 318 files, 2964 passed, 1 skipped.pnpm exec vitest run apps/console/— 29 files, 300 passed.pnpm exec vitest run packages/plugin-list/ packages/plugin-detail/— 91 files, 948 passed.pnpm exec vitest run scripts/ packages/i18n/— 62 files, 1139 passed.pnpm check:i18n-keys— exit 0,Every in-scope call-site key resolves against the en pack (2921 keys), and every literal inline defaultValue matches the value the pack serves.pnpm check:i18n-drift—0 en value(s) changed.node scripts/check-control-bytes.mjs— OK (3966 files).turbo run type-checkandlintfor app-shell / console / plugin-list / plugin-detail — 38 type-check tasks successful, lint 0 errors.check-changeset-presence/-fixed/-no-major— all green.Out of scope, filed separately
Two classes this census surfaced that the ruling did not take. Both are filed rather than smuggled in here:
t(key) || 'English'— 24 sites, all on keysendefines, and #3810's new rule cannot see any of them #4117 — the same dead fallback spelledt(key) || 'English'/?? 'English'. Measured first-hand: 24 sites, 18 distinct keys, 4 files, all 24 on keysendefines, and this PR's rule cannot see one of them (they carry no options argument). Five diverge in text; the||spelling is also strictly worse thandefaultValueon a provider-less host, where it renders a raw key.ReportConfigPanelasks forreport.editor.title, which is the Title field's label in the report editor form, for the panel's heading andaria-label: the panel is titled "Title" and the landmark is announced as "Title". Six more of the same shape.Aligning both groups' call sites here is right regardless (the pack is what renders); whether the pack holds the right copy for each slot is a separate judgement, which is what #4118 is for.
A third, narrower one is recorded as a comment on the existing #3845 rather than a new card:
home.welcomepasses aproductargument to anenvalue with no{{product}}hole, so i18next drops it — a second instance of that card's class, in a different namespace.Generated by Claude Code