fix(i18n): pickLocalized reads own properties only and takes only string values on every limb (#3907) - #4359
Merged
Conversation
…ing values on every limb (#3907) Four of the six limbs — exact tag, base language, `default`, `en` — used bare bracket access, so a locale naming an `Object.prototype` member resolved to that member and rendered its source text as the label, and a non-string value short-circuited the chain into `[object Object]`. Both guards now apply uniformly; a guarded limb misses rather than aborting, so the chain continues. Zero change for any real BCP-47 tag, which collapses the two departures the backend twin `resolveI18nLabel` (objectstack#6765) recorded against this function to zero. The parity table now pins that convergence. 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
|
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 free
to 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 #3907
packages/i18n/src/pickLocalized.tsread four of its six limbs — the exact tag, the base language,defaultanden— with a bare bracket access, and skipped thetypeof === 'string'filter that the regional and last-resort limbs already applied. Both guards now apply uniformly on every limb.What was wrong
Bare access walks the prototype chain, so a locale that happened to name an
Object.prototypemember resolved to that member and the function stringified it into the label. Measured onorigin/main@78b6c4a88before the change, all seven members the card lists reproduce:The same four limbs also let a non-string value short-circuit the chain and render as the stringified object:
The shape of the fix
A guard makes its limb miss; it does not abort the resolution. An unusable entry now falls through to the next limb exactly as an absent one does.
That distinction is the one correction this PR makes to the card's stated acceptance criterion. The dispatch note expected
pickLocalized({ en: 'Pricing' }, 'constructor')to become''; the correct converged answer is'Pricing', because the prototype limb misses and the chain continues to limb 5 (en).''is right only when no limb hits at all, e.g.pickLocalized({}, 'constructor'). Both are pinned. The already-hardened backend twin answers the same way, which is what the expectation was checked against:A test demanding
''for the first case would have pinned a fallthrough bug rather than this fix.Why this is behaviour-neutral
No BCP-47 tag is an
Object.prototypemember, and the inline locale map is declared as a record whose values arez.string(), so no in-contract input can reach either guard. 20 control vectors covering every limb — exact tag, base language, regional upgrade,default,en, last resort, trimming, case asymmetry, empty map, empty-string values — resolve byte-identically to before, and were green on both sides of the change.Cross-repo convergence
This collapses to zero the two departures that
resolveI18nLabel(objectstack#6765,packages/spec/src/ui/i18n-label-resolver.ts) shipped with as documented, deliberate narrowings of this function — it reads own properties only and filtersstringon every limb, because on a server the locale can arrive in anAccept-Languageheader. The only remaining difference is how each side spells a miss:''here for a text node,undefinedthere for a producer's?? namechain.packages/plugin-list/src/__tests__/i18nLabel-resolver-parity.test.ts(the six-limb parity pin from the #4208 train) is updated to say exactly that. Its header no longer claims one rule difference; a newCONVERGEDtable carries the 19 vectors that used to tell the two implementations apart — the only inputs that ever could — and asserts both sides now answer them identically, plus a one-pass check that the union of both tables has no disagreement left.Upstream note (no objectstack change in this PR — that repo was read-only here).
packages/spec/src/ui/i18n-label-resolver.test.tscarries a verbatim copy of this file as its reference fixture, pinned toorigin/main 50fa3766/ blob9e5d92a. Because the copy is local rather than imported, that suite stays green while silently going stale — it will not alert anyone. Two of its assertions encode this file's old behaviour and will need to flip when the copy is synced, and the resolver's own module doc still describes both departures as live. Reported to the PM for upstream relay rather than actioned here.Verification
pickLocalizedresolves a locale naming anObject.prototypemember to that member, and renders its source text as the label #3907 pins (22 in the i18n suite, 18 in the parity suite); the 50 greens are the pre-existing suite plus every control.git stash), both directions predicted before running:pickLocalized.tsfromorigin/mainwhile keeping the new tests reproduced the red-first numbers exactly: 40 failed / 50 passed.typeof === 'string'filter turned red only the value-filter pins and left every prototype-member pin green: 15 failed / 75 passed, 0 of the reds being prototype pins. The two guards are independently load-bearing; neither masks the other.type-checkacross i18n, plugin-list and all six consumer packages: 36/36 tasks successful (app-shell runs bothtsc --noEmitandtsc -p tsconfig.typetests.json).packages/i18n/,packages/plugin-list/): 71 files, 1240 passed.pickLocalized.components,plugin-detail,plugin-dashboard,plugin-designer,react: 275 files, 2691 passed;app-shell: 349 files, 3331 passed / 1 skipped.linton both changed packages: 0 errors.check:control-bytes,check:i18n-keys,check:i18n-drift,check-changeset-presence,check-changeset-no-major: all green.Generated by Claude Code