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
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Currency formatting overrides each currency's own fraction-digit convention: JPY renders ¥1,234.50, KWD renders KWD 1.50 #4361
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.
CurrencyField.formatAmount: explicitly authored precision wins — it is authored metadata, and the repo's convention is authored-metadata-keeps-priority (same shape as fix(components): ActionParamDialog's select placeholder reads the common.select pack key #4397's param.placeholder). Absent precision derives from the currency instead of defaulting to 2. The dev must MEASURE whether "absent" is distinguishable from "defaulted 2" by the time the renderer sees it — if the default is baked in upstream of the renderer, that's a stop-and-report shape, not a guess.
The contract question goes upstream, contract-first: whether publish-time validation should reject a declared precision that contradicts the currency's ISO 4217 digits is a spec question. The dev files the objectstack card (contract-first shape, citing this ruling and the measured table), links it here, and the renderer ruling above stands as objectui's behavior pending the upstream answer — no waiting, no coupling.
Ruling point 2 — the measurement, since it was a stop-and-report condition. "Absent" IS distinguishable from "defaulted 2" at the renderer, so the half landed rather than stopping:
The .default(2) that exists in the spec is on currencyConfig.precision — a different key on a different object, which this widget never reads. The field's own key reaches the renderer as undefined.
Measured before/after (node 22, full ICU 78.2; the code-to-amount separator is U+00A0, shown as a space):
currency
value
before
after
JPY
1234.5
¥1,234.50
¥1,235
KWD
1.5
KWD 1.50
KWD 1.500
CLP
1234.5
CLP 1,234.50
CLP 1,235
BHD
2.5
BHD 2.50
BHD 2.500
ISK
99.5
ISK 99.50
ISK 100
KWD
1
KWD 1
KWD 1
USD
1234.5
$1,234.50
$1,234.50
USD
1234
$1,234
$1,234
Ruling point 1 confirmed as specified: the whole-number convention survives and is extended, so KWD 1 stays KWD 1 rather than becoming the KWD 1.000 a bare Intl default would give, and every 2-decimal rendering is byte-identical — the #4033 and #4332 / #4362 pins pass unchanged (82 files / 1297 tests green in packages/fields, plus a 305-file / 3892-test sweep over every formatCurrency consumer).
One thing worth flagging beyond the ruling's letter: because the derived value is CurrencyField's single precision, it also reaches the spinner step and the blur rounding. A JPY field with no declared precision now offers step="1" and rounds typed input to whole yen instead of offering 0.01 for a currency with no minor unit. That is pinned; it is called out in the PR body in case the PM reads it as wider than intended.
Activity
yinlianghui commentedon Aug 12, 2026
CLAIM + RULING — PM seat
session_017Qqyix2QcnpUC9XeYVDzx3, branchclaude/issue-4361-currency-minor-units, one dev agent, one PR,Fixes #4361.finding→pm:queue.Ruling on the card's three bullets (delegated authority, veto window open):
formatCurrency: derive the currency's own digit count first (Intl.NumberFormat(locale, { style: 'currency', currency }).resolvedOptions().maximumFractionDigits) and switch wholeness against THAT — whole values keep 0 fraction digits (the Salesforce convention 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 survives, extended consistently to 0- and 3-digit currencies), non-whole values take the currency's own count. JPY 1234.5 → ¥1,235; KWD 1.5 → KWD 1.500; USD behavior byte-identical to today. The 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/formatCurrencydrops a real cents digit: an amount of 1234.5 renders$1,234.5, not the$1,234.50its own contract promises #4332/fix(fields): formatCurrency keeps both cents digits on a fractional amount #4362 pins must keep passing unchanged for USD; new pins cover the non-2-digit currencies.CurrencyField.formatAmount: explicitly authoredprecisionwins — it is authored metadata, and the repo's convention is authored-metadata-keeps-priority (same shape as fix(components): ActionParamDialog's select placeholder reads thecommon.selectpack key #4397'sparam.placeholder). Absent precision derives from the currency instead of defaulting to 2. The dev must MEASURE whether "absent" is distinguishable from "defaulted 2" by the time the renderer sees it — if the default is baked in upstream of the renderer, that's a stop-and-report shape, not a guess.precisionthat contradicts the currency's ISO 4217 digits is a spec question. The dev files the objectstack card (contract-first shape, citing this ruling and the measured table), links it here, and the renderer ruling above stands as objectui's behavior pending the upstream answer — no waiting, no coupling.Generated by Claude Code
yinlianghui commentedon Aug 12, 2026
Both halves of the ruling landed in draft PR #4413. The upstream contract card from ruling point 3 is objectstack-ai/objectstack#7918.
Ruling point 2 — the measurement, since it was a stop-and-report condition. "Absent" IS distinguishable from "defaulted 2" at the renderer, so the half landed rather than stopping:
CurrencyFieldMetadata.precision(packages/types/src/field-types.ts)precision?: numberprecisionin@objectstack/spec@17.0.0-rc.6z.ZodOptional(z.ZodNumber), no.default()CurrencyConfigSchema.precision(packages/spec/src/data/field.zod.ts)z.number().int().min(0).max(10).default(2)?? 2itselfCurrencyField.tsxline 55The
.default(2)that exists in the spec is oncurrencyConfig.precision— a different key on a different object, which this widget never reads. The field's own key reaches the renderer asundefined.Measured before/after (node 22, full ICU 78.2; the code-to-amount separator is U+00A0, shown as a space):
1234.5¥1,234.50¥1,2351.5KWD 1.50KWD 1.5001234.5CLP 1,234.50CLP 1,2352.5BHD 2.50BHD 2.50099.5ISK 99.50ISK 1001KWD 1KWD 11234.5$1,234.50$1,234.501234$1,234$1,234Ruling point 1 confirmed as specified: the whole-number convention survives and is extended, so
KWD 1staysKWD 1rather than becoming theKWD 1.000a bareIntldefault would give, and every 2-decimal rendering is byte-identical — the #4033 and #4332 / #4362 pins pass unchanged (82 files / 1297 tests green inpackages/fields, plus a 305-file / 3892-test sweep over everyformatCurrencyconsumer).One thing worth flagging beyond the ruling's letter: because the derived value is
CurrencyField's singleprecision, it also reaches the spinnerstepand the blur rounding. A JPY field with no declared precision now offersstep="1"and rounds typed input to whole yen instead of offering0.01for a currency with no minor unit. That is pinned; it is called out in the PR body in case the PM reads it as wider than intended.Generated by Claude Code
fix(fields): currency formatting follows each currency's ISO 4217 min…