Repository navigation
[core] formatMeasure renders every dashboard measure in the MACHINE locale — the last parallel implementation outside the display-locale channel #4566
Description
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:
- A. Give
formatMeasure/formatDimensionValuean optional locale parameter and thread it from each widget (theformatDateTimeplaybook from Date formatter is half-localized: future relative forms and absolute timestamps stay en-US on a non-English locale #4272: parameter lands WITH its consumers). - B. Route these through
formatDisplayNumberin@object-ui/i18nso the grouping policy and locale handling stop being duplicated at all — larger, but it retires the parallel implementation rather than localizing it in place.
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
- Same family as Console: number cell/display renderers hardcode a grouping
Intl.NumberFormat('en-US')— an ordinalField.number(a year) always renders2,026, and no field property can turn it off #4033, Date formatter is half-localized: future relative forms and absolute timestamps stay en-US on a non-English locale #4272 / PR fix(fields): the date formatter's last three en-US channels thread the display locale (#4272) #4544, [plugin-grid] ObjectGrid's record-detail date fallback renders the MACHINE locale #4541 / PR fix(plugin-grid): the record-detail date fallback threads the display locale (#4541) #4552, plugin-timeline hardcodes 'en-US' at four date sites, so timeline headers stay English on every non-English session #4513, and [plugin-gantt] tooltip number / currency / percent rows render in the MACHINE locale, next to date rows that follow the display locale #4553 / PRs fix(plugin-gantt): tooltip numbers and currency follow the display locale (#4553) #4557 + fix(fields): formatPercent groups and follows the display locale — the last tooltip channel (#4553) #4565. - NOT a duplicate of [plugin-gantt] tooltip number / currency / percent rows render in the MACHINE locale, next to date rows that follow the display locale #4553: that card is
formatPercentand its consumers; this is a different function in a different package that [plugin-gantt] tooltip number / currency / percent rows render in the MACHINE locale, next to date rows that follow the display locale #4553's fix provably does not reach.
Generated by Claude Code
Activity
yinlianghui commentedon Aug 13, 2026
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:
useDisplayLocale()(the one channel) at the DatasetWidget / ObjectMetric / pivot call sites — parameter lands WITH its consumers, never speculatively.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
fix(core): dashboard measures follow the display locale (objectstack-…