Skip to content

finding(plugin-charts): normalizeChartSchema's label() picks the FIRST entry of an inline locale map, so a chart heading follows key order instead of the user's language #8943

Description

@claude

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:

  1. Thread the active language into normalizeChartSchema as a parameter and delegate label() to
    pickLocalized, at every call site that has one.
  2. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions