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

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions