You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
formatCurrency (packages/fields/src/index.tsx) picks its width from the value's wholeness, never from the currency:
constfracDigits=isWhole ? 0 : 2;// 2 for every currency on earth
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.
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:
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.
Summary
Both currency formatting paths in
packages/fieldshardcode a fraction-digit width and pass it toIntl.NumberFormat, which overrides the digit count ISO 4217 defines for the currency being rendered.Intlknows that JPY has 0 decimal places and KWD has 3; giving it explicitminimumFractionDigits/maximumFractionDigitsthrows that knowledge away.Measured directly against the construction (node 22, full ICU):
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
formatCurrency(packages/fields/src/index.tsx) picks its width from the value's wholeness, never from the currency:formatAmount(packages/fields/src/widgets/CurrencyField.tsx) uses the field's declaredprecisionfor both bounds, which has the same effect wheneverprecisiondisagrees with the currency (andprecisiondefaults to 2).The shared
formatDisplayNumberin@object-ui/i18nis not implicated: it passes the bounds it is handed straight toIntland 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.
resolveFieldCurrencyresolves 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 (
minimumFractionDigitswas a constant0against a wholeness-switched maximum, so1234.5rendered$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:
Intldecide would give JPY and KWD the right digits, but would also retire the Salesforce whole-number convention thatformatCurrency's doc comment states and that Console: number cell/display renderers hardcode a groupingIntl.NumberFormat('en-US')— an ordinalField.number(a year) always renders2,026, and no field property can turn it off #4033 pinned ($1,234keeps no.00) —Intl's own default for USD1234is$1,234.00.Intl.NumberFormat(locale, { style: 'currency', currency }).resolvedOptions().maximumFractionDigits) and switching wholeness against that instead of a literal 2.CurrencyField'sprecisionis authorable metadata, so there is a contract question underneath: does a declaredprecision: 2on 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.