Skip to content

Console: number cell/display renderers hardcode a grouping Intl.NumberFormat('en-US') — an ordinal Field.number (a year) always renders 2,026, and no field property can turn it off #5067

Closed as not planned

Description

@yinlianghui

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:

  1. useGrouping is never set, and it defaults to true. scale: 0 controls fraction digits only; nothing controls the separator.
  2. The locale is hardcoded to en-US rather than taken from the active i18n locale, so a zh-CN or de-DE console 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 how service-analytics sorts 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 2026 and "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:

  1. Honour an authorable presentation hint on the field — e.g. useGrouping: false, or a format / displayFormat property — and pass it to Intl.NumberFormat.
  2. Suppress grouping automatically when scale === 0 and the field has no currency, or when the field is flagged as an identifier/ordinal.
  3. 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_year was converted from Field.number to Field.text for exactly this reason (yinlianghui/hotcrm-heimao#35, Hide blog menu from top navigation #40).
  • crm_supply_item.year, crm_competitor_supply.year and crm_supply_history.year are 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

  1. added
    bugSomething isn't working
    repo:objectuiSeam card: cross-repo ordering with objectui is the substance (pure objectui fixes live in objectui)
    on Aug 4, 2026
  2. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    分诊:入队 pm:queue(已挂 repo:objectui)。

    • 过时前提检查(objectui origin/main):packages/fields/src/index.tsx:401/420/430/451/621、widgets/CurrencyField.tsx、plugin-dashboard/MetricWidget.tsx:89 多处 Intl.NumberFormat('en-US') 仍在,前提成立。
    • 路线提示:正文修法 2/3(scale === 0 抑制分组 + 用活跃 i18n locale 取代硬编码 'en-US')纯 objectui 可闭环,建议先落;修法 1(给字段新增 authorable 展示属性,如 useGrouping/displayFormat)触 packages/spec 契约面,若走该路线须回报分诊座位做 contract-first 拆分(spec 子单先行),不得在本单顺手改 spec。
    • 查重:三仓无影子单。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    Contributor

    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

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

    bugSomething isn't workingpm:queueReady for the PM dispatch looprepo:objectuiSeam card: cross-repo ordering with objectui is the substance (pure objectui fixes live in objectui)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions