Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

[core] formatMeasure renders every dashboard measure in the MACHINE locale — the last parallel implementation outside the display-locale channel #4566

Closed

Description

@yinlianghui

Found while censusing formatPercent consumers for #4553 phase 2 (PR #4565). Filed rather than fixed — different package, different function, outside that card's ruled surface.

The gap

packages/core/src/utils/dataset-format.ts formats dashboard measure values with a bare undefined locale tag at three sites:

111:    return Number.isInteger(v) ? String(v) : v.toLocaleString(undefined, { maximumFractionDigits: 2 });
130:    const body = display.toLocaleString(undefined, { minimumFractionDigits: decimals ?? 0, maximumFractionDigits: decimals ?? 0 });
141:    return Number.isInteger(v) ? String(v) : v.toLocaleString(undefined, { maximumFractionDigits: 2 });

Line 130 is formatMeasure's main body (numbers, currency-ish and percent measures alike); 111 is its no-format-hint branch; 141 is formatDimensionValue. undefined is not "the user's locale", it is the MACHINE's — neither of the repo's two locale channels, exactly as useDisplayLocale's own doc puts it.

Why this was not covered by the #4553 family

It is easy to assume this was already fixed, and the census showed it was not. formatMeasure is a parallel implementation, not a consumer: it never calls formatPercent, formatNumber or formatDisplayNumber. It reimplements the percent scaling by sharing percentDisplayValue with @object-ui/fields and then formats independently.

So #4553's producer fix reached every formatPercent consumer and moved nothing here. DatasetWidget, ObjectMetric and the pivot matrix — the surfaces formatMeasure actually feeds — still render in the machine's locale on every session.

Worth noting it is the milder half of the same family: toLocaleString does group by default, so this is a locale defect only, not the locale-plus-grouping defect formatPercent had.

Why it can be observed

A German session on a dashboard reads a KPI as 1,234.5 where the convention is 1.234,5, next to a grid or gantt tooltip that (after #4541 / #4544 / #4557 / #4565) renders the same number as 1.234,5. Inverted separators read as a different number, not as an unstyled one.

The part that needs a decision, not just a patch

formatMeasure is a pure function in @object-ui/core, called from React components. It cannot read useDisplayLocale() itself, so the locale has to arrive as a parameter and every caller has to thread it — the same shape renderFieldValue needed in PR #4565, but with more callers and across a package boundary.

Whoever picks this up should decide deliberately between:

B is worth real consideration precisely because this function exists to keep dashboard measures agreeing with list cells (its own doc comment says so), and it currently achieves that agreement by duplicating logic that has since grown a policy layer the duplicate does not have.

Relations


Generated by Claude Code

Activity

  1. self-assigned this
    on Aug 13, 2026
  2. yinlianghui commented on Aug 13, 2026

    @yinlianghui
    ContributorAuthor

    CLAIM — session_017Qqyix2QcnpUC9XeYVDzx3, branch claude/issue-4566-measure-locale. Dispatching a dev agent now.

    PM ruling (delegated decision authority; maintainer veto window open — record objections here): the card's own fork, decided with a measured fallback:

    1. Prefer (b) — route formatMeasure through formatDisplayNumber and retire the duplicate. The function's own doc says it exists to keep dashboard measures agreeing with list cells; it achieves that today by DUPLICATING logic that has since grown a policy layer (grouping thresholds, percent style, locale) the duplicate lacks — the drift the one-resolver rule exists to end. MEASURE FIRST whether formatMeasure's behavior beyond locale (compact notation, decimal policy, any measure-specific forms at dataset-format.ts:111/:130/:141) maps losslessly onto formatDisplayNumber's options. If the mapping is clean: route through it, pin en output byte-identical (grouping already exists here, so unlike fix(fields): formatPercent groups and follows the display locale — the last tooltip channel (#4553) #4565's percent case en must NOT move — that is the discriminator between the two cards). If the mapping is LOSSY (measure-specific policies formatDisplayNumber cannot express): fall back to (a) — thread a locale parameter per the Date formatter is half-localized: future relative forms and absolute timestamps stay en-US on a non-English locale #4272 playbook — and record the surviving duplication as a comment at both ends.
    2. Widgets thread useDisplayLocale() (the one channel) at the DatasetWidget / ObjectMetric / pivot call sites — parameter lands WITH its consumers, never speculatively.
    3. Red-first on de/zh sessions per surface; en pinned byte-identical at every touched site (see the discriminator above); the flaky: DatasetWidget.dottedDimensionTable's "issues the ONE read" case asserts a fetch AFTER waiting only on the rendered cell #4487 flake lives in DatasetWidget's suite — its protocol applies (verify locally + rerun before owning a red).
    4. Changesets: '@object-ui/core' per its measured .d.ts (optional param on an entry export ⇒ minor; internal rewiring only ⇒ patch — say which); plugin-dashboard per its diff. Never major.

    Sequencing: gate worktree creation on PR #4565 reaching origin/main (armed on green CI — both cards touch plugin-dashboard, different files, but the gate is minutes and removes all doubt). Mutual exclusion: ⛔ #4548 in flight owns packages/react + consumer fixes in plugin-dashboard DashboardRenderer.tsx / DashboardGridLayout.tsx — do NOT touch those two files; DatasetWidget/ObjectMetric/recordFields are clear. #4305 (app-shell flow), #4564 (types/data-objectstack), #4549 (StudioDesignSurface) disjoint.


    Generated by Claude Code


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions