Skip to content

fix(i18n): fail on an inline defaultValue that contradicts its en value, and align the 43 that do (#3810) - #4119

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3810-defaultvalue-en-drift-gate
Aug 10, 2026
Merged

fix(i18n): fail on an inline defaultValue that contradicts its en value, and align the 43 that do (#3810)#4119
yinlianghui merged 1 commit into
mainfrom
claude/issue-3810-defaultvalue-en-drift-gate

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

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:

Scanned 1476 files, 3492 t()/tt() call sites: 2365 pack-backed (2314/2314 literal keys resolve), ...
Inline defaults: 851 literal (804 match their en value, 4 not comparable), 57 computed (report-only).
43 inline defaultValues contradict the en value of a key that EXISTS (38 distinct keys)

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 measured console (1) and perm/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:

filesites
packages/app-shell/src/console/organizations/OrganizationsPage.tsx9
apps/console/src/pages/auth/RegisterPage.tsx5
apps/console/src/pages/auth/ForgotPasswordPage.tsx4
packages/app-shell/src/console/home/HomePage.tsx4
apps/console/src/pages/auth/{LoginPage,VerifyEmailPromptPage}.tsx2 + 2
14 further files1-2 each

All 8 rows the card tables are confirmed against current origin/main (line numbers had shifted by one or two after #3546 slice three; the en values and the call-site texts are byte-identical to the card). Both sites of the repeated auth.verifyEmail.resendFailed key are covered. RegisterPage.tsx adds 5 more auth-family sites the card did not list — same namespace, same class.

The gate (B)

scripts/check-i18n-call-site-keys.mjs gains a third failure class, default-value-drift: when a call site carries a literaldefaultValue and the key resolves to an en leaf, the two must be equal byte for byte. collectEnKeys now returns leaf values as well as paths (folding the one 'a' + 'b' concatenation in en.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:

  • A key en does not define is not reported here. That is missing-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'].
  • Not judged, counted instead: a computed default (defaultValue: label), a key that is dynamic or denotes several literals, a returnObjects subtree, and the plural families (detail.showEmptyRelated resolves through _one/_other; there is no single form to compare against). Today: 57 computed, 4 not comparable.
  • Failure output prints both strings with every non-ASCII byte escaped, because six of the 43 differed only by ... against U+2026 and are invisible otherwise.

The hint text points the fix at the call site and explicitly forbids editing en.ts to silence it.

The alignment (A)

All 43 call sites now repeat the pack value byte for byte. packages/i18n/src/locales/en.ts is untouched — verified by check:i18n-drift, which reports 0 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 I18nProvider mounted, which is every console screen — but not universally. With no provider and no initialised i18next instance, react-i18next's not-ready t returns the defaultValue:

// react-i18next 17.0.11, dist/es/useTranslation.jsconstnotReadyT=(k,optsOrDefaultValue)=>{if(isString(optsOrDefaultValue))returnoptsOrDefaultValue;if(isObject(optsOrDefaultValue)&&isString(optsOrDefaultValue.defaultValue))returnoptsOrDefaultValue.defaultValue;

So on a provider-less host the inline string was the rendered one, for the sites whose hook is useObjectTranslation directly; sites reached through a createSafeTranslation hook 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 a patch on 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.ts34 passed (9 new).
  • Reverse verification: with the rule reverted (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-drift0 en value(s) changed. node scripts/check-control-bytes.mjs — OK (3966 files).
  • turbo run type-check and lint for 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:

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.welcome passes a product argument to an en value with no {{product}} hole, so i18next drops it — a second instance of that card's class, in a different namespace.


Generated by Claude Code

…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
@vercel

vercelBot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 10, 2026 11:07am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.3 KB350 KB
Entry fileindex-BFagPtuH.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.66KB3.13KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)485.06KB107.21KB
core (index.js)3.04KB1.15KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)140.66KB36.25KB
fields (index.js)229.40KB56.93KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.87KB10.80KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)45.23KB12.45KB
plugin-charts (index.js)61.49KB17.48KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)118.50KB30.66KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)237.80KB59.48KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.58KB27.68KB
plugin-gantt (index.js)162.81KB39.67KB
plugin-grid (index.js)188.04KB49.91KB
plugin-kanban (index.js)48.60KB13.41KB
plugin-list (index.js)110.04KB26.67KB
plugin-map (index.js)17.00KB5.32KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)26.21KB7.52KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)23.71KB7.95KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.23KB0.66KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.71KB1.34KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@yinlianghui@claude