Found while reworking PR objectui#8884 (card objectui#7946). Filed rather than fixed there: it is a
different read site, in a different function, and widening that PR past its ruled key set is a second
contract decision.
The defect
packages/plugin-charts/src/normalizeChartSchema.ts resolves a chart's own heading through a
module-local label():
function label(v: unknown): string | undefined {
const s = str(v);
if (s) return s;
if (isRec(v)) {
const first = Object.values(v).find((x) => typeof x === 'string' && x);
return first as string | undefined;
}
return undefined;
}
The map arm picks the first string value in the object. It is not locale-aware: it never reads the
active language, never prefers default, and never prefers en. So for a title the protocol
explicitly admits —
{ "title": { "zh-CN": "定价", "en": "Pricing" } }
— the heading is decided by key insertion order, not by who is looking at it. A zh user gets
定价 only if the author happened to write that key first; reorder the JSON and the same user gets
English, with no other change.
@objectstack/spec types this slot as I18nLabel (a plain string OR an inline locale map), and
ChartConfigSchema.title carries exactly that union, so the map arm is authored surface rather than
an accident.
Why it is worth a card rather than a note
The repository already publishes the right answer, and it is now used in the same component, on the
sibling read site. PR objectui#8884's rework round repaired the drill drawer's heading fallback to
go through pickLocalized (@object-ui/i18n) — the locale-aware resolver pinned as the twin of the
spec's own resolveI18nLabel. That leaves ObjectChart with two answers for one union on one node:
the drill heading follows the user's language, the chart heading beside it follows key order.
That asymmetry PRE-DATES objectui#8884 and was recorded there deliberately rather than widened into
it — this card is where it gets closed.
Suggested shape (not a ruling)
normalizeChartSchema is pure and takes no React context, so it cannot call a hook. The two obvious
routes, both needing a decision rather than a guess:
- Thread the active language into
normalizeChartSchema as a parameter and delegate label() to
pickLocalized, at every call site that has one.
- Leave normalisation locale-free and resolve the heading at the render site, the way the drill
heading now does.
Route 2 keeps the function pure; route 1 keeps one answer for every label() caller in the file
(axis titles use it too — see normalizeAxis). Whichever is taken, the acceptance is that
pickLocalized is the single resolver, not a second local one.
Acceptance
- Authoring the locale-map arm of
title renders the entry for the ACTIVE language, with the
default / en fallbacks pickLocalized documents, and does not depend on key order.
- Every
label() caller in normalizeChartSchema.ts gets the same treatment, or the ones left
behind are ledgered by name with a reason.
- A runtime pin: same map, two languages, two headings.
Generated by Claude Code
Found while reworking PR objectui#8884 (card objectui#7946). Filed rather than fixed there: it is a
different read site, in a different function, and widening that PR past its ruled key set is a second
contract decision.
The defect
packages/plugin-charts/src/normalizeChartSchema.tsresolves a chart's own heading through amodule-local
label():The map arm picks the first string value in the object. It is not locale-aware: it never reads the
active language, never prefers
default, and never prefersen. So for a title the protocolexplicitly admits —
{ "title": { "zh-CN": "定价", "en": "Pricing" } }— the heading is decided by key insertion order, not by who is looking at it. A
zhuser gets定价only if the author happened to write that key first; reorder the JSON and the same user getsEnglish, with no other change.
@objectstack/spectypes this slot asI18nLabel(a plain string OR an inline locale map), andChartConfigSchema.titlecarries exactly that union, so the map arm is authored surface rather thanan accident.
Why it is worth a card rather than a note
The repository already publishes the right answer, and it is now used in the same component, on the
sibling read site. PR objectui#8884's rework round repaired the drill drawer's heading fallback to
go through
pickLocalized(@object-ui/i18n) — the locale-aware resolver pinned as the twin of thespec's own
resolveI18nLabel. That leavesObjectChartwith two answers for one union on one node:the drill heading follows the user's language, the chart heading beside it follows key order.
That asymmetry PRE-DATES objectui#8884 and was recorded there deliberately rather than widened into
it — this card is where it gets closed.
Suggested shape (not a ruling)
normalizeChartSchemais pure and takes no React context, so it cannot call a hook. The two obviousroutes, both needing a decision rather than a guess:
normalizeChartSchemaas a parameter and delegatelabel()topickLocalized, at every call site that has one.heading now does.
Route 2 keeps the function pure; route 1 keeps one answer for every
label()caller in the file(axis titles use it too — see
normalizeAxis). Whichever is taken, the acceptance is thatpickLocalizedis the single resolver, not a second local one.Acceptance
titlerenders the entry for the ACTIVE language, with thedefault/enfallbackspickLocalizeddocuments, and does not depend on key order.label()caller innormalizeChartSchema.tsgets the same treatment, or the ones leftbehind are ledgered by name with a reason.
Generated by Claude Code