Skip to content

fix(i18n): delete the dead t(key) || 'English' fallback and gate the spelling (#4117) - #4147

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4117-dead-or-fallbacks
Aug 10, 2026
Merged

fix(i18n): delete the dead t(key) || 'English' fallback and gate the spelling (#4117)#4147
yinlianghui merged 1 commit into
mainfrom
claude/issue-4117-dead-or-fallbacks

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#4117

The other spelling of #3810's dead i18n fallback: not an argument of the call but its
sibling, one operator to the right of everything scripts/check-i18n-call-site-keys.mjs
used to read. t('marketplace.detail.moreOptions') || 'More options' carries no options
object at all, so the default-value-drift rule sees no defaultValue to compare and the
interpolation-parity rule sees an empty — and correct — argument set. node.parent is
what closes it.

The ruling this implements

Delegated on the card, veto window open: existence-is-red demanding deletion, not
RHS-equality. The reasoning is that this fallback is dead on every path, which is one
step stronger than #3810's case:

  • With a provider, i18next serves the pack value. Same as defaultValue.
  • Without one, react-i18next's not-ready t returns the KEY — truthy — so || skips
    the fallback there too and the user reads a raw console.objectView.delete. Verified in
    source: createSafeTranslation's provider-less path returns defaults[key] || key, also
    always truthy. defaultValue at least renders English there.

So the two spellings are not two ways to write one thing. Aligning these strings would have
blessed a second fallback spelling when the point is to keep one (defaultValue, governed
by default-value-drift).

What changed

The gate — a fifth class, dead-sibling-fallback, hard from day one like classes 3 and
4. Also widens the file pre-filter to t?.(: ContextSelectors.tsx spells every call that
way and holds no t( at all, so the whole file used to fall out of all five classes
silently — a blind spot inside the thing being extended.

The stock — 24 sites measured on this tip (the card's count reproduces exactly), 22
judged and deleted across MarketplacePackagePage.tsx (16), ObjectView.tsx (5) and
ActionResultDialog.tsx (1).

Where this diverges from the card: ContextSelectors.tsx

The card listed its two sites among the 24 and asked for :409 to be counted-not-judged on
the grounds that its right operand is a variable. Measuring the call shape takes both of
them out for a stronger reason: they are optional calls on an optional prop
(t?: (key: string, options?: any) => string), so t?.(key) evaluates to undefined
whenever the prop is absent and the ?? fallback is live, not dead. Its own
ContextSelectors.persist.test.tsx:128 renders the hook with no t. Deleting those two —
including :411, which the card listed as one of the five divergent rows — would have
replaced a rendered placeholder with undefined. They stay, counted as
optionalCallFallbacks, and the self-test pins the abstention.

The five divergent rows: no rendering bug, verified per row

The card flagged these because a divergent fallback that also carried {{holes}} could have
been hiding an unfilled hole — a provider-mounted user reading raw {{count}}. Each was
checked against what the call actually passes; all were already wired, so nothing renders
differently
and no params needed adding:

siteen renderscall passesbefore → after
MarketplacePackagePage.tsx:429/:487purgeSuccessRemoved { {count} } sample record(s).{ count: removed }Removed 3 sample record(s). → same
MarketplacePackagePage.tsx:453reseedLocalSuccessRe-seeded sample data: { {inserted} } inserted, { {updated} } updated.{ inserted, updated }both filled → same
MarketplacePackagePage.tsx:459reseedPartialErrors (a sixth, not on the card's list)({ {count} } record(s) failed to write){ count: errored }filled → same
ObjectView.tsx:939deleteViewConfirm… delete the view "{ {name} }"? …{ name: viewLabel }filled → same
MarketplacePackagePage.tsx:680moreOptionsMore install options— (no holes)the aria-label already read the pack value → same
ContextSelectors.tsx:411selectPlaceholderSelect { {label} }{ label, defaultValue }not deleted — optional call

#4136's interpolation-parity class agrees independently: it judges all 22 of these sites
and reports nothing in either direction, which is the mechanical proof that no hole is
unfilled and no argument inert.

(Placeholder braces are spaced apart in this table only so GitHub's body sanitizer does not
eat them; the source uses the normal doubled form.)

The abstentions, each a decision

  • Optional call — above. Not a parser limitation; a live path.
  • Non-literal right operand (t(key) || label) — renders a runtime value, not a second
    copy of the sentence. Same treatment as a computed defaultValue in [finding] 8 处 auth 调用点的内联 defaultValue 与 en 包值不一致 —— key 存在故兜底是死代码,三道 i18n 门禁按设计都看不见这一类 #3810.
  • Empty en value — the one way a non-optional t() is falsy, so the class's premise
    fails. en has no such leaf today; the abstention stops the first one being a wrong red.
  • Plural families / dynamic keys / returnObjects — the same key preconditions as
    classes 3 and 4.
  • Right-operand positionsomeValue || t(key) is the healthy, opposite shape, and
    main carries 94 of those against these 24. A rule reading "appears in a ||" rather
    than "is the left operand of one" would have condemned four times more than it fixed.

Verification

  • Script self-test: 64 passed (13 new).
  • Reverse verification, rule removed: predicted the reds before running — the 5 RED
    cases plus the 5 counter-based abstention cases go red, 10 of 13, and the 3 that stay
    green do so by construction (a source-level stock pin, the missing-key territory case, and
    the repo-wide "main carries none"). Measured: exactly 10 failed | 54 passed.
  • Reverse verification, one stock site restored (ActionResultDialog.tsx): the
    repo-wide run names exactly it — ActionResultDialog.tsx:239:43 [dead-sibling-fallback] actions.resultDialog.copyAll, counters moving 2 → 3 sites and 0 → 1 judged.
  • Repo-wide gate: green. Sibling fallbacks: 2 call site(s) sit left of a ||/?? — 0 judged, 0 with a non-literal right operand, 2 an optional call (fallback is live), 0 with no single comparable en value.
  • app-shell suite (repo root): 318 files, 2969 passed, 1 skipped.
  • pnpm type-check:scripts and app-shell type-check: clean.
  • ESLint: 0 errors; warning counts on the three edited files identical to origin/main
    (12 / 144 / 0).

Changeset

Empty frontmatter — declared as releasing nothing, with the per-row argument above. Removing
an operand that never evaluated leaves both the provider and provider-less paths
byte-identical.


Generated by Claude Code

…e spelling (#4117)
The other spelling of objectui#3810's dead fallback: not an argument of the
call but its SIBLING, one operator to the right of everything the call-site
gate used to read. `scripts/check-i18n-call-site-keys.mjs` gains a fifth class,
`dead-sibling-fallback`, decided by `node.parent`.
Existence is the defect, so the verdict is deletion rather than alignment.
With a provider, i18next serves the pack value. WITHOUT one, react-i18next's
not-ready `t` returns the key — truthy — so `||` skips the fallback there too
and the user reads a raw `console.objectView.delete`. That makes this spelling
strictly worse than an inline `defaultValue`, which at least renders English on
a provider-less host, and it is why aligning these strings would have blessed a
second fallback spelling instead of leaving one (`defaultValue`).
24 sites measured on this tip; 22 judged and deleted here. The remaining two
are `ContextSelectors.tsx`, and they are the reason the rule reads the call
SHAPE and not just the operator: both are optional calls (`t?.(key) ?? '…'`)
on an optional prop, so the call really can be `undefined` and the fallback
really does render — its own persist test mounts the hook with no `t`. They
are counted, not judged.
Two more abstentions carry the rule's accuracy: a non-literal right operand
renders a runtime value rather than a second copy of the sentence, and an `en`
value that is the empty string is the one way a non-optional `t()` can be
falsy. Only the LEFT operand position is judged — `someValue || t(key)` is the
healthy shape and `main` carries 94 of those against these 24.
Also widens the file pre-filter to `t?.(`. `ContextSelectors.tsx` spells every
call that way and holds no `t(` at all, so the whole file used to fall out of
all five classes silently.
No rendered output changes: every one of the five divergent rows already passes
the params its `en` holes need, which objectui#3845's interpolation-parity class
independently confirms across all 22 sites.
@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 1:55pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.3 KB350 KB
Entry fileindex-Zlo7juFM.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.52KB17.49KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)118.52KB30.68KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 10, 2026 14:03
@yinlianghui
yinlianghui added this pull request to the merge queueAug 10, 2026
Merged via the queue into main with commit 36ae270Aug 10, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4117-dead-or-fallbacks branch August 10, 2026 14:03
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] The same dead i18n fallback spelled t(key) || 'English' — 24 sites, all on keys en defines, and #3810's new rule cannot see any of them

2 participants

@yinlianghui@claude