Skip to content

finding: pickLocalized resolves a locale naming an Object.prototype member to that member, and renders its source text as the label #3907

Description

@os-zhuang

Observation-class finding, filed out of objectstack#6765 (the shared backend I18nLabelstring resolver, which is pinned to this function's rule). Not something a user hits today in the console — filed so the next reader of either implementation is not surprised by the one place they deliberately differ.

What

packages/i18n/src/pickLocalized.ts reads map entries with a bare bracket access:

const pick =
  o[lang] ??
  o[base] ??
  (regional !== undefined ? o[regional] : undefined) ??
  o.default ??
  o.en ??
  Object.values(o).find((v) => typeof v === 'string');
return pick == null ? '' : String(pick);

o[lang] / o[base] walk the prototype chain, so when language happens to name an Object.prototype member the lookup succeeds against the prototype and the function stringifies it:

pickLocalized({ en: 'Pricing' }, 'constructor');
// → 'function Object() { [native code] }'

pickLocalized({ en: 'Pricing' }, 'toString');
// → 'function toString() { [native code] }'

Same for valueOf, hasOwnProperty, isPrototypeOf, propertyIsEnumerable, toLocaleString. Verified against origin/main 50fa376 (blob 9e5d92a for the file, last touched by #3278).

Note the four limbs are inconsistent with the other two on a second axis as well: regional and the last-resort Object.values(...).find(...) both require typeof === 'string', while o[lang], o[base], o.default and o.en do not — so a non-string value on those limbs short-circuits the chain and renders as [object Object]. Inside the declared domain that is unobservable (InlineLocaleMapSchema in the framework repo is z.record(<BCP-47 tag>, z.string())), which is why this is filed as an observation rather than a defect.

Why it is observation-class here

In the console the language argument comes from the app's own localization state (useObjectTranslation().language / useLocalization()), i.e. a value the app picked, not one an outsider supplies. No BCP-47 language tag is an Object.prototype member, so no in-contract input reaches this. It is recorded rather than fixed silently because the other implementation of this rule now exists and its input can be untrusted.

Cross-repo context

objectstack#6765 adds resolveI18nLabel in packages/spec/src/ui/i18n-label-resolver.ts, mirrored limb for limb from this function and pinned by an executed parity table (a verbatim copy of this file is checked into that test as the reference). It reads own properties only and applies the string filter on every limb, because on a server the locale can arrive in an Accept-Language header. Those are the two deliberate departures documented there, and this card is the other half of that documentation: they are divergences from this implementation, both narrower than the shared rule, and neither is reachable by an input I18nLabelSchema accepts.

Suggested shape if it is ever picked up

Guard the four unguarded limbs the way the other two already are — an own-property check plus the existing typeof === 'string' filter — which collapses the difference between the two implementations to zero. No behaviour change for any language tag.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions