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.
[core][fields] A percent renders as 1.234,5 % in a list cell and 1.234,5% as a dashboard measure — two percent conventions for one number #4576
Found and measured while implementing objectui#4566. Filed rather than fixed there: changing it moves output in a direction that card's ruling explicitly excluded, so it needs its own decision.
The divergence
The two formatters build the percent sign differently:
formatMeasure (@object-ui/core, utils/dataset-format.ts) appends a LITERAL % to a decimal-formatted body, so it gets no space in any locale.
Measured, de-DE, the same stored value:
surface
output
list cell (formatPercent)
1.234,5 % (U+00A0 before the sign)
dashboard measure (formatMeasure)
1.234,5%
English is identical on both paths (1,234.5%), so this is invisible in an en session. Affects every locale whose percent convention has a space: de, fr, es, ar were all measured to differ; en and zh do not.
This directly contradicts percentDisplayValue's own doc comment, which says the shared scaling exists so that "a percent renders identically as a row value and as an aggregated metric — the two surfaces can never drift". The SCALING no longer drifts; the CONVENTION now does.
Which convention is the correct one for a dashboard measure, given both surfaces show the same underlying number:
A. Route formatMeasure's percent branch through Intlstyle: 'percent' so both surfaces agree on the locale's convention. Costs the precision artefact above at extreme magnitudes (arguably irrelevant for a percentage, which is not normally 1e21).
C. Accept the divergence and record it at both ends.
A is the direction the one-resolver rule points; B contradicts a ruling already made on #4553; C is only defensible if the two surfaces are judged never to be read side by side, which the dataset preview and the report renderer make doubtful.
Worth deciding together with objectui#4575, which threads the remaining formatMeasure consumers — if A wins it should land before those surfaces multiply the inconsistency.
CLAIM — session_017Qqyix2QcnpUC9XeYVDzx3, branch claude/issue-4576-one-number-home. Dispatching a dev agent now. This card is ruled TOGETHER with PR #4577's open question 1 (the formatDisplayNumber down-move) — the split this card reports is the live symptom of the two-homes drift that question describes, so they are one fix, not two.
PM ruling (delegated decision authority; maintainer veto window open — record objections here):
The down-move lands first, in this PR (PR fix(core): dashboard measures follow the display locale (#4566) #4577's option B, adopted): formatDisplayNumber, shouldGroupDisplayNumber and DisplayNumberFormatOptions MOVE from @object-ui/i18n into @object-ui/core (core is the lower, React-free layer — the function is pure and has no business behind a React/i18next package); @object-ui/i18n RE-EXPORTS them under the same names so every existing import path (fields, components, plugin-dashboard) keeps compiling byte-compatible. i18n gains a dependency on core — measure that this creates no cycle (core must not import i18n anywhere; fix(core): dashboard measures follow the display locale (#4566) #4577 already verified core's dependency list).
Then the percent split closes on the moved home: formatMeasure's percent path routes through the SAME percent policy formatPercent uses (Intl style:'percent' with its locale-correct NBSP), replacing the literal '%' suffix — restoring percentDisplayValue's doc promise that a percent 'can never drift' between a list cell and a measure. The measured cost is DECLARED, not hidden: Intl percent moves en at absurd magnitudes (fix(core): dashboard measures follow the display locale (#4566) #4577 measured 24/32,760 combinations, all ≥ ~1e20%). That en move is the fix's declared behavior change (the fix(fields): formatPercent groups and follows the display locale — the last tooltip channel (#4553) #4565 precedent — consistency wins; pin the new forms red-first, note the old forms in the changeset). Ordinary-magnitude en percent must stay byte-identical — pin the threshold.
Changesets: '@object-ui/core' MINOR (entry gains the moved exports + the percent behavior move); '@object-ui/i18n' per its measured .d.ts (a re-export swap — if declaration-identical, patch; if the declaration moved, minor with the fix(types): the DataSource contract carries deleteView's per-home outcomes (#4564) #4569 precedent's reasoning); consumers per their diffs. Never major.
STOP conditions: a dependency cycle; any consumer that imports the moved symbols by a path the re-export cannot preserve; the percent unification measurably breaking an ordinary-magnitude en form.
Mutual exclusion: ⛔ in flight: #4563 (apps/console), #4535 (types+ObjectGrid), #4550 (packages/react navigation). packages/core, packages/i18n, packages/fields are claimed by nobody — verify with a fresh fetch. #4577/#4565/#4582 are landed context; their test files move only under authorized pin moves.
PM step-7 复核: ACCEPT — and the stage-2 mechanism substitution is CONFIRMED (option A, style: 'percentPoints' as implemented in PR #4589).
复核 checked: the 15-file diff matches the declared surface exactly (the only fields/app-shell touches are test files, both in the authorized pin-move class); changeset '@object-ui/core': minor + '@object-ui/i18n': minor follows the #4544 (entry-visible growth) and #4569 (declaration-moved) precedents; packages/i18n/src/utils/number-display.ts is an honest re-export with a reference-identity pin so a second copy cannot silently return; CI converged green on head 2e1b87ccc (18 success + 2 skipped, 0 failures); #4590 filed with measurements and acceptance criteria.
Why A and not the ruling's named mechanism (B): the ruling's stop conditions outrank its named mechanism. The dev measured first — as instructed — and the measurement disproved the ruling's premise: the style: 'percent' divergence is rounding-tie-keyed, not magnitude-keyed (smallest mover 0.175; 27,581 of 1,200,013 ordinary-magnitude en forms), which is STOP condition 3 verbatim. Implementing B was therefore forbidden by the ruling itself. A delivers the ruling's stated goal exactly — byte-identical percent affix to style: 'percent' across all 171 locale tags tested — while moving zero numerals at any magnitude, so even the pre-authorized extreme-magnitude en cost is not incurred. And 'percent' (value is a fraction) vs 'percentPoints' (value is already in points) names the exact trap this family of cards keeps rediscovering, at the call site, in the type system. Not C: the defect was filed and its blast radius was growing.
The remaining tie-level gap with formatPercent is deliberately NOT closed here: the measure is the faithful surface and the cell is the artefact, so the fix belongs at the artefact's home — that is #4590, actionable the moment this merges.
Veto window open as with every delegated ruling: reverting to the literal mechanism (B) is a two-line swap plus the tie/extreme pins per the report, at the cost of re-incurring the en movement the original ruling forbade.
Flipping PR #4589 ready and arming auto-merge now.
Activity
yinlianghui commentedon Aug 13, 2026
CLAIM — session_017Qqyix2QcnpUC9XeYVDzx3, branch
claude/issue-4576-one-number-home. Dispatching a dev agent now. This card is ruled TOGETHER with PR #4577's open question 1 (the formatDisplayNumber down-move) — the split this card reports is the live symptom of the two-homes drift that question describes, so they are one fix, not two.PM ruling (delegated decision authority; maintainer veto window open — record objections here):
formatDisplayNumber,shouldGroupDisplayNumberandDisplayNumberFormatOptionsMOVE from @object-ui/i18n into @object-ui/core (core is the lower, React-free layer — the function is pure and has no business behind a React/i18next package); @object-ui/i18n RE-EXPORTS them under the same names so every existing import path (fields, components, plugin-dashboard) keeps compiling byte-compatible. i18n gains a dependency on core — measure that this creates no cycle (core must not import i18n anywhere; fix(core): dashboard measures follow the display locale (#4566) #4577 already verified core's dependency list).Mutual exclusion: ⛔ in flight: #4563 (apps/console), #4535 (types+ObjectGrid), #4550 (packages/react navigation). packages/core, packages/i18n, packages/fields are claimed by nobody — verify with a fresh fetch. #4577/#4565/#4582 are landed context; their test files move only under authorized pin moves.
Generated by Claude Code
Generated by Claude Code
fix(app-shell): move the #4575 German percent sample pin onto the loc…
yinlianghui commentedon Aug 13, 2026
PM step-7 复核: ACCEPT — and the stage-2 mechanism substitution is CONFIRMED (option A,
style: 'percentPoints'as implemented in PR #4589).复核 checked: the 15-file diff matches the declared surface exactly (the only fields/app-shell touches are test files, both in the authorized pin-move class); changeset
'@object-ui/core': minor+'@object-ui/i18n': minorfollows the #4544 (entry-visible growth) and #4569 (declaration-moved) precedents;packages/i18n/src/utils/number-display.tsis an honest re-export with a reference-identity pin so a second copy cannot silently return; CI converged green on head2e1b87ccc(18 success + 2 skipped, 0 failures); #4590 filed with measurements and acceptance criteria.Why A and not the ruling's named mechanism (B): the ruling's stop conditions outrank its named mechanism. The dev measured first — as instructed — and the measurement disproved the ruling's premise: the
style: 'percent'divergence is rounding-tie-keyed, not magnitude-keyed (smallest mover 0.175; 27,581 of 1,200,013 ordinary-magnitude en forms), which is STOP condition 3 verbatim. Implementing B was therefore forbidden by the ruling itself. A delivers the ruling's stated goal exactly — byte-identical percent affix tostyle: 'percent'across all 171 locale tags tested — while moving zero numerals at any magnitude, so even the pre-authorized extreme-magnitude en cost is not incurred. And'percent'(value is a fraction) vs'percentPoints'(value is already in points) names the exact trap this family of cards keeps rediscovering, at the call site, in the type system. Not C: the defect was filed and its blast radius was growing.The remaining tie-level gap with
formatPercentis deliberately NOT closed here: the measure is the faithful surface and the cell is the artefact, so the fix belongs at the artefact's home — that is #4590, actionable the moment this merges.Veto window open as with every delegated ruling: reverting to the literal mechanism (B) is a two-line swap plus the tie/extreme pins per the report, at the cost of re-incurring the en movement the original ruling forbade.
Flipping PR #4589 ready and arming auto-merge now.
Generated by Claude Code
Generated by Claude Code
fix(core): one home for the number-display policy — and percent stops…
fix(fields): formatPercent renders points directly — ties round half-…
fix(core,fields): render a dataset measure over a date field as a date
fix(core,fields): render a dataset measure over a date field as a date (