Repository navigation
Description
Summary
Every numeric field the console renders goes through an Intl.NumberFormat that is constructed with grouping on and the locale hardcoded to en-US, and the only field property it consults is scale. There is no authorable way to say "this number is an ordinal — do not group it".
The visible consequence: a four-digit year stored as Field.number renders as 2,026 everywhere — grids, related lists, record headers — in every locale.
Evidence
Measured on a fresh database (@objectstack/console 17.0.0-rc.1, locale zh-CN), the three related lists on one customer record:
{ yearCol: "年度", sample: ["2,026", "2,026", "2,026"] } // crm_competitor_supply
{ yearCol: "年度", sample: ["2,023", "2,025", "2,024"] } // crm_supply_history
{ yearCol: "年度", sample: ["2,026", "2,026", "2,026"] } // crm_supply_item
The field definition that produces this carries everything the spec offers for a discrete integer:
year: Field.number({
label: 'Year',
required: true,
storage: { notNull: true },
scale: 0, // ← the only property the renderer reads
min: 1900,
})Mechanism
The number display renderer in ui-components reduces to:
new Intl.NumberFormat('en-US', {
minimumFractionDigits: scale ?? 0,
maximumFractionDigits: scale ?? 20,
}).format(value)Two independent problems in one line:
useGroupingis never set, and it defaults totrue.scale: 0controls fraction digits only; nothing controls the separator.- The locale is hardcoded to
en-USrather than taken from the active i18n locale, so azh-CNorde-DEconsole still groups by the US convention.
The same construction appears in the grid cell renderer and in the compact formatNumber helper, so changing the surface does not change the answer.
Why "just store it as text" is not a fix, only a dodge
That is what we were forced to do (twice — see below), and it costs real capability:
- The column loses numeric comparison at the driver, so range filters (
year >= 2023) stop working and have to be re-expressed as string comparisons that only happen to work for zero-free four-digit years. - Dataset dimensions must be re-typed
string, which changes howservice-analyticssorts and buckets them. - Every writer (seed, REST client, test) has to be normalized by hand through a lifecycle hook, because the engine will happily store
2026and"2026"as two different values — and if the field is part of an upsert external id, that divergence forks the key.
None of that is conceptually necessary. The value is a number; only its presentation is wrong.
Suggested fix
Any of these closes it; the first is the smallest:
- Honour an authorable presentation hint on the field — e.g.
useGrouping: false, or aformat/displayFormatproperty — and pass it toIntl.NumberFormat. - Suppress grouping automatically when
scale === 0and the field has nocurrency, or when the field is flagged as an identifier/ordinal. - Independently of the above: stop hardcoding
'en-US'and use the active locale, so grouping and decimal marks follow the user's language.
Downstream impact in our app
We have now paid for this three times in the same codebase:
crm_customer_rating.rating_yearwas converted fromField.numbertoField.textfor exactly this reason (yinlianghui/hotcrm-heimao#35, Hide blog menu from top navigation #40).crm_supply_item.year,crm_competitor_supply.yearandcrm_supply_history.yearare being converted now (yinlianghui/hotcrm-heimao#59) — which additionally forces three new normalization hooks, three dataset dimension re-types, and a re-check of the cross-object "common comparison year" logic that decides a customer's supply share.
Each conversion is a permanent loss of numeric semantics traded for a display detail, and it will keep happening to every app that stores a year, a fiscal period, or any other ordinal integer.
Activity
os-zhuang commentedon Aug 5, 2026
分诊:入队
pm:queue(已挂repo:objectui)。packages/fields/src/index.tsx:401/420/430/451/621、widgets/CurrencyField.tsx、plugin-dashboard/MetricWidget.tsx:89多处Intl.NumberFormat('en-US')仍在,前提成立。scale === 0抑制分组 + 用活跃 i18n locale 取代硬编码 'en-US')纯 objectui 可闭环,建议先落;修法 1(给字段新增 authorable 展示属性,如useGrouping/displayFormat)触packages/spec契约面,若走该路线须回报分诊座位做 contract-first 拆分(spec 子单先行),不得在本单顺手改 spec。本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-zhuang commentedon Aug 10, 2026
Migrated to objectstack-ai/objectui#4033 under #7167 (file-at-destination ruling, maintainer 2026-08-10). Native GitHub issue transfer is not available to this session's credential, so the card was recreated at the destination; this thread stays as the authoritative history and is linked from the new card. Closing as not planned here — moved, not rejected.
Generated by Claude Code