Uh oh!
There was an error while loading. Please reload this page.
fix(fields,i18n,dashboard): number renderers follow the active locale and stop grouping ordinals - #4333
Merged
Merged
Conversation
… and stop grouping ordinals
Every numeric field the console renders went through an Intl.NumberFormat
constructed with the locale hardcoded to 'en-US' and grouping left at the
Intl default, so a four-digit year stored as Field.number({ scale: 0 })
rendered as '2,026' in every locale with no field property able to turn it
off (objectui#4033, source thread objectstack#5067).
The same construction had been copied into five places, so fixing any one
surface never changed the answer. Converge them onto one formatter in
@object-ui/i18n — formatDisplayNumber — which owns both the locale and the
grouping policy, and one locale resolver, useDisplayLocale.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3Reverse verification caught the 'emits an explicit false only when suppressing' pin passing for an empty reason: es-ES renders 1234 ungrouped under Intl's 'auto' as well, so the assertion was green whether or not the suppression existed. Re-anchored on en-US, which separates the two. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 11, 2026 17:18
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#4033 — source thread objectstack-ai/objectstack#5067 (read-only history; its triage comment is the route of record).
What was wrong
Every numeric field the console rendered went through an
Intl.NumberFormatconstructed with the locale hardcoded toen-USanduseGroupingnever set. Two independent defects rode in that one construction:zh-CN/de-DEconsole still grouped and pointed decimals the US way; andField.number({ scale: 0 })rendered as2,026— in every locale, with no field property able to turn it off.The construction had been copied into five places, so fixing any one surface never changed the answer.
What changed
Both policies now live in one function,
formatDisplayNumberin@object-ui/i18n, plus one locale resolver,useDisplayLocale. Call sites bring the value and the display width; they do not bring a locale default and they do not decide grouping.Converged call sites —
packages/fields/src/index.tsx(formatCurrency,formatCompactCurrency,formatNumber,NumberCellRenderer,CurrencyCellRenderer),packages/fields/src/widgets/CurrencyField.tsx,packages/components/src/renderers/basic/elements.tsx,packages/plugin-dashboard/src/MetricWidget.tsx.The locale channel is composed, not invented
The card asked for "the active i18n locale". Measurement found two existing channels, and
useDisplayLocalecomposes them rather than adding a third:useLocalization().locale— the tenant's resolved regional default (ADR-0053). Frequentlyundefined; the endpoint is cosmetic and non-blocking.useObjectTranslation().language— the ACTIVE UI language from the switcher.'en'— a concrete last resort rather thanundefined, which would handIntlthe machine's locale and be non-deterministic in CI.Step 2 is what covers the state #4033 was measured in: a fresh database, where the tenant endpoint has no locale to give, so a language switch alone has to drive formatting.
Grouping policy, and its interim status
scale === 0and no currency ⇒useGrouping: false. Documented at the definition as an interim default with the accepted cost named (a large scale-0 count loses its separators too), and marked as overridden by the future spec-level presentation hint. No spec / objectstack change here — that stays contract-first and separate.scaleis treated as a policy input, not a display width: only a surface with a real field declaration behind it passes it. That is what makes the exceptions structural rather than three separately-patched call sites.Measured grouping exception, reported honestly
MetricWidgetkeeps its separators. Itsdecimalsis parsed from a numeral.js format pattern ('0,0.00'gives 2), not from a field's declared scale, and it is0for the commonest KPI patterns ('0,0', or no format at all). Grouping there is load-bearing by the widget's own contract:The
en-UShardcode is fixed on that surface; only the grouping default is not applied to it.element:number(an aggregate renderer) and every currency path are excluded on the same structural grounds. An undeclaredscalealso keeps grouping — absent means "decimals unknown", not "integer".Reverse verification
Predictions were written down before running; commit-then-revert, never
git stash.en-USMetricWidgetmade to obey the ordinal defaultLimb B's green half is the load-bearing one:
2026ungrouped is identical in every locale, so a locale regression is invisible to the ordinal tests. The two fixes are therefore independently pinned rather than entangled.Limb C exists so the exception is evidence rather than assertion — had nothing gone red, "grouping is load-bearing here" would be an unverified claim.
Reverse verification also caught one of my own pins passing for an empty reason:
emits an explicit false only when suppressingwas anchored on es-ES, which renders1234ungrouped under Intl'sautoas well — green whether or not the suppression existed. Re-anchored on en-US, which separates the two (1,234vs1234). Second commit.Two Intl traps the implementation is written around
useGrouping: trueis not the same as omitting it.truemeans "always"; omitting means "auto" (the locale's own preference). Measured for1234: es-ES auto1234vs always1.234; pl-PL auto1234vs always1 234. So the code setsuseGroupingonly to suppress — writingtruewould silently override those locales' conventions in the name of preserving en-US output. Pinned as a test.localearrives from a server response;new Intl.NumberFormat('not a locale')throwsRangeError. Handled inside the formatter by retrying without the locale, while a genuinely badcurrencycode still propagates to the call sites' existing fallbacks. Both directions pinned.Tests
New:
packages/fields/src/__tests__/NumberCellRenderer.grouping.test.tsx(17),packages/i18n/src/__tests__/number-display.test.ts(16),packages/plugin-dashboard/src/__tests__/MetricWidget.numberFormat.test.tsx(7). Red-first on the card's exact fixture.i18n,fields,components,plugin-dashboard).plugin-grid,plugin-gantt,plugin-list,plugin-detail,react).--filter '...pkg'= consumers, not dependencies): 35 of 46 workspace projects, all clean, after a fullpnpm build(43/43) so no staledist/*.d.tswas read.check-control-bytes: OK.API surface
Additive only — no narrowing.
formatCurrency,formatCompactCurrencyandformatNumbereach gain an optional trailinglocale; existing calls are unaffected, though omitting it now follows the runtime default rather than forcing US conventions.@object-ui/i18ngains three exports.Scope notes
objectstacktreated as read-only throughout.packages/components/src/renderers/action/(held by Two of the four action renderers still absorb unknown keys silently — an object spread disables the excess-property check that #4046 just installed #4281) andsrc/ui/untouched.MetricWidgetedit is confined to the formatter call and its locale threading, disjoint from Analytics surfaces resolve select-option labels but never apply the i18n bundle — reports and dashboards show English while lists and forms show the translation #4030's in-flightchartConfigPresentation/DatasetWidgetregion in the same package.Out-of-scope findings
formatCurrencydrops a real cents digit: an amount of 1234.5 renders$1,234.5, not the$1,234.50its own contract promises #4332:formatCurrencydrops a real cents digit (1234.5renders$1,234.5, not the$1,234.50its own doc comment promises) —minimumFractionDigitsis a constant0whilemaximumFractionDigitscorrectly switches on wholeness. Pre-existing, neither a locale nor a grouping defect. This PR pins the current behaviour in a control test with a comment pointing at the issue, so its fix flips a watched test rather than a silent surface.DateCellRendererreads only the tenant channel, so dates will not follow a language switch once numbers do.useDisplayLocaleis the ready-made resolver for it, but re-pointing a date renderer is that card's call.Generated by Claude Code