Skip to content

Currency formatting overrides each currency's own fraction-digit convention: JPY renders ¥1,234.50, KWD renders KWD 1.50 #4361

Description

@yinlianghui

Summary

Both currency formatting paths in packages/fields hardcode a fraction-digit width and pass it to Intl.NumberFormat, which overrides the digit count ISO 4217 defines for the currency being rendered. Intl knows that JPY has 0 decimal places and KWD has 3; giving it explicit minimumFractionDigits / maximumFractionDigits throws that knowledge away.

Measured directly against the construction (node 22, full ICU):

JPY 1234.5   Intl default          ¥1,235
JPY 1234.5   as we render it       ¥1,234.50
KWD 1.5      Intl default          KWD 1.500
KWD 1.5      as we render it       KWD 1.50

A yen amount is shown with cents that the currency does not have, and a dinar amount is shown with one fewer digit than it does have.

Mechanism — two paths, same shape

  1. formatCurrency (packages/fields/src/index.tsx) picks its width from the value's wholeness, never from the currency:

    const fracDigits = isWhole ? 0 : 2;   // 2 for every currency on earth
  2. formatAmount (packages/fields/src/widgets/CurrencyField.tsx) uses the field's declared precision for both bounds, which has the same effect whenever precision disagrees with the currency (and precision defaults to 2).

The shared formatDisplayNumber in @object-ui/i18n is not implicated: it passes the bounds it is handed straight to Intl and would honour the currency default if the call sites simply omitted them.

Reachability, honestly

Needs a tenant or field whose currency is not a 2-decimal one. resolveFieldCurrency resolves from the field, currencyConfig.defaultCurrency, or the tenant default (ADR-0053), so any of the three set to JPY/KRW/CLP (0-decimal) or KWD/BHD/OMR/TND (3-decimal) reaches it. The example apps use USD, so it is not visible in dogfooding today. Filing it plainly rather than grading it — the impact section of a finding is the least reliable part of it.

Not fixed in #4332's PR, and why

Found while fixing #4332 (minimumFractionDigits was a constant 0 against a wholeness-switched maximum, so 1234.5 rendered $1,234.5). That card's scope is the min/max pairing at one policy point; which width is correct for a given currency is a different question with a different answer shape, and it is pre-existing on both sides of that change — the PR moves the second digit of an amount that was already showing decimals a yen amount should not have.

It also is not a one-line flip, which is the main reason it is separate:

  • Dropping both bounds and letting Intl decide would give JPY and KWD the right digits, but would also retire the Salesforce whole-number convention that formatCurrency's doc comment states and that 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 #4033 pinned ($1,234 keeps no .00) — Intl's own default for USD 1234 is $1,234.00.
  • Keeping the convention while respecting the currency means deriving the currency's digit count first (Intl.NumberFormat(locale, { style: 'currency', currency }).resolvedOptions().maximumFractionDigits) and switching wholeness against that instead of a literal 2.
  • CurrencyField's precision is authorable metadata, so there is a contract question underneath: does a declared precision: 2 on a JPY field win over the currency's own 0, or is that combination something publish-time validation should reject? Contract-first, that is a spec question, not a renderer question.

Suggested first step is a decision on the third bullet, since it decides whether the fix is renderer-local at all.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions