Uh oh!
There was an error while loading. Please reload this page.
fix(i18n): delete the dead t(key) || 'English' fallback and gate the spelling (#4117) - #4147
Merged
Merged
Conversation
…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.
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 14:03
Uh oh!
There was an error while loading. Please reload this page.
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#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.mjsused to read.
t('marketplace.detail.moreOptions') || 'More options'carries no optionsobject at all, so the
default-value-driftrule sees nodefaultValueto compare and theinterpolation-parityrule sees an empty — and correct — argument set.node.parentiswhat 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:
defaultValue.treturns the KEY — truthy — so||skipsthe fallback there too and the user reads a raw
console.objectView.delete. Verified insource:
createSafeTranslation's provider-less path returnsdefaults[key] || key, alsoalways truthy.
defaultValueat 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, governedby
default-value-drift).What changed
The gate — a fifth class,
dead-sibling-fallback, hard from day one like classes 3 and4. Also widens the file pre-filter to
t?.(:ContextSelectors.tsxspells every call thatway and holds no
t(at all, so the whole file used to fall out of all five classessilently — 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) andActionResultDialog.tsx(1).Where this diverges from the card:
ContextSelectors.tsxThe card listed its two sites among the 24 and asked for
:409to be counted-not-judged onthe 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), sot?.(key)evaluates toundefinedwhenever the prop is absent and the
??fallback is live, not dead. Its ownContextSelectors.persist.test.tsx:128renders the hook with not. Deleting those two —including
:411, which the card listed as one of the five divergent rows — would havereplaced a rendered placeholder with
undefined. They stay, counted asoptionalCallFallbacks, 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 havebeen hiding an unfilled hole — a provider-mounted user reading raw
{{count}}. Each waschecked against what the call actually passes; all were already wired, so nothing renders
differently and no params needed adding:
enrendersMarketplacePackagePage.tsx:429/:487purgeSuccessRemoved { {count} } sample record(s).{ count: removed }Removed 3 sample record(s).→ sameMarketplacePackagePage.tsx:453reseedLocalSuccessRe-seeded sample data: { {inserted} } inserted, { {updated} } updated.{ inserted, updated }MarketplacePackagePage.tsx:459reseedPartialErrors(a sixth, not on the card's list)({ {count} } record(s) failed to write){ count: errored }ObjectView.tsx:939deleteViewConfirm… delete the view "{ {name} }"? …{ name: viewLabel }MarketplacePackagePage.tsx:680moreOptionsMore install optionsaria-labelalready read the pack value → sameContextSelectors.tsx:411selectPlaceholderSelect { {label} }{ label, defaultValue }#4136's
interpolation-parityclass agrees independently: it judges all 22 of these sitesand 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
t(key) || label) — renders a runtime value, not a secondcopy of the sentence. Same treatment as a computed
defaultValuein [finding] 8 处 auth 调用点的内联 defaultValue 与 en 包值不一致 —— key 存在故兜底是死代码,三道 i18n 门禁按设计都看不见这一类 #3810.envalue — the one way a non-optionalt()is falsy, so the class's premisefails.
enhas no such leaf today; the abstention stops the first one being a wrong red.returnObjects— the same key preconditions asclasses 3 and 4.
someValue || t(key)is the healthy, opposite shape, andmaincarries 94 of those against these 24. A rule reading "appears in a||" ratherthan "is the left operand of one" would have condemned four times more than it fixed.
Verification
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.ActionResultDialog.tsx): therepo-wide run names exactly it —
ActionResultDialog.tsx:239:43 [dead-sibling-fallback] actions.resultDialog.copyAll, counters moving2 → 3sites and0 → 1judged.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.pnpm type-check:scriptsand app-shelltype-check: clean.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