Skip to content

fix(plugin-gantt): tooltip numbers and currency follow the display locale (#4553) - #4557

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4553-gantt-tooltip-number-locale
Aug 13, 2026
Merged

fix(plugin-gantt): tooltip numbers and currency follow the display locale (#4553)#4557
yinlianghui merged 1 commit into
mainfrom
claude/issue-4553-gantt-tooltip-number-locale

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Part of #4553 — two of the card's three call sites. The third is a premise
inversion that needs a ruling before it can be touched; details below, and this
PR deliberately does not close the card.

What changed

formatFieldValue (the tooltip value formatter inside ObjectGantt's tasks
memo) had its four TEMPORAL call sites threaded with useDisplayLocale() by
#4272 / PR #4544. The numeric cases beside them passed no locale, so they
reached new Intl.NumberFormat(undefined, ...) — the MACHINE's locale, which
is neither of the repo's two locale channels.

One tooltip, two conventions: a German session read 5. Jan. 2024 on the date
row and 1,234.50 on the amount row directly below it, where German groups with
. and marks the decimal with ,. Inverted separators do not read as an
unstyled number — they read as a different number.

number / integer / float / decimal and currency now pass the
displayLocale already read at component level (L435, added by PR #4544), using
each formatter's existing locale parameter. No formatter signature changed and
no memo dependency changed — displayLocale has been in that array since #4272.

The card's premise held for two of three formatters

The ruling required measuring each formatter's signature in packages/fields
first, and treating a missing locale parameter as a premise inversion. Measured:

formatter signature locale?
formatNumber (value, decimals = 2, locale?) yes, param 3
formatCurrency (value, currency?, locale?) yes, param 3
formatPercent (value, precision = 0) no

The issue states all three end up at formatDisplayNumber with an undefined
locale. formatPercent does not: its whole body is
`${percentDisplayValue(value).toFixed(precision)}%`. It builds no
Intl.NumberFormat and never reaches formatDisplayNumber, so its output is
not the machine's locale but no locale — an ASCII decimal mark, never
grouped, byte-identical on every machine (1235% where German wants
1.235 %).

So the percent row is a different defect class from the other two, and threading
is not available to it: closing it means adding a parameter to a
@object-ui/fields export, which this card's ruled surface excludes
(packages/fields is read-only here). Per the ruling I stopped rather than
edited fields. The percent row is pinned as-is by a test in this PR, labelled
as evidence of the inversion rather than an endorsement, so the gap cannot drift
unnoticed and will go red the day formatPercent grows a locale.

Verification

Red-first, directions predicted in writing before the run and recorded in the
test file header. Runner: node v22.22.2 / ICU 78.2 / machine locale en-US.

Before the fix — both call sites independently red (assertion order is
deliberate: a failing expect aborts its test, so with the number row asserted
first everywhere the currency site would never report its own red):

Expected: "Qty=1.234,50"          Received: "Qty=1,234.50"
Expected: "Amount=1.234,50 EUR"   Received: "Amount=EUR1,234.50"
Tests  3 failed | 2 passed (5)

After: Tests 5 passed (5). Full package suite: Test Files 44 passed (44),
Tests 383 passed (383) — PR #4544's date threading and PR #4554's currency-dep
and locale-dep cases green untouched, field-explicit currency precedence
unchanged.

Reverse verification via git diff > fix.patch + git checkout -- (never
git stash): removing the fix restored exactly those 3 failures with the 2 pins
still green; restore confirmed byte-identical by sha256.

en is pinned byte-identical at every touched site. Those en cases are PINS,
green on both sides
en and the runner's en-US coincide here, so they
assert unchanged English output and are not evidence the fix works; the de
cases carry that.

Other gates: both tsc passes green (tsc --noEmit && tsc -p tsconfig.test.json);
eslint 0 errors, and the new test file produces the same 5 warnings as its two
sibling locale test files already on main (identical stub idiom, no drift);
check:control-bytes, check:phantom-deps, changeset:check all green.

Why patch

Consumer-side threading only, module-local: the package's 9 dist/*.d.ts files
are byte-identical before and after (verified by sha256 in both directions).

Surface

packages/plugin-gantt/src/ObjectGantt.tsx, one new test file, one changeset —
nothing else. packages/fields, plugin-grid, app-shell/studio, plugin-kanban and
the CelPredicateField family are untouched.


Generated by Claude Code

…cale (#4553)

The tooltip value formatter in ObjectGantt's `tasks` memo had its four
temporal call sites threaded with `useDisplayLocale()` by objectui#4272.
The numeric cases beside them passed no locale, so they reached
`new Intl.NumberFormat(undefined, ...)` — the machine's locale, which is
neither of the repo's two locale channels.

One tooltip rendered two conventions: a German session read `5. Jan. 2024`
on the date row and `1,234.50` on the amount row below it, where German
groups with `.` and marks the decimal with `,`. Inverted separators read as
a different number, not an unstyled one.

`number`/`integer`/`float`/`decimal` and `currency` now pass the
`displayLocale` already read at component level, via each formatter's
existing locale parameter. No formatter signature and no memo dependency
changed; the package's .d.ts files are byte-identical.

`percent` is deliberately NOT threaded: `formatPercent(value, precision)`
takes no locale parameter at all, so closing that half needs a
`@object-ui/fields` signature change, which is outside this card's ruled
surface. It is pinned by a test and escalated on #4553.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 13, 2026 8:51am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-B9y-QmUC.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 25.13KB 5.40KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 38.46KB 10.17KB
auth (createAuthenticatedFetch.js) 6.34KB 2.43KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.02KB 0.88KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 489.32KB 108.45KB
core (index.js) 3.37KB 1.34KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 158.47KB 43.13KB
fields (index.js) 230.18KB 57.13KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.84KB 1.45KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.98KB 10.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.86KB 12.91KB
plugin-charts (index.js) 62.10KB 17.67KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 120.95KB 31.53KB
plugin-designer (index.js) 212.58KB 42.83KB
plugin-detail (index.js) 239.88KB 59.99KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 164.28KB 40.02KB
plugin-grid (index.js) 189.36KB 50.33KB
plugin-kanban (index.js) 52.74KB 14.53KB
plugin-list (index.js) 111.13KB 27.12KB
plugin-map (index.js) 18.16KB 5.81KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.16KB 10.96KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.08KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 23.73KB 7.96KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

PM step-7 复核 — ACCEPT(本 PR 为 #4553 的两站点半,卡保持开放候 percent 裁决执行)(session_017Qqyix2QcnpUC9XeYVDzx3)

  • The number and currency threading is complete and correct, with a detail worth naming as the house standard: the tests are ARRANGED so both sites red independently (one test asserts currency first), because a failing expect aborts its test and the currency site would otherwise have ridden on the number site's evidence. De inverts both separators AND moves the symbol — un-fakeable.
  • The percent inversion is a genuine measured premise correction: formatPercent(value, precision=0) has no locale parameter and never touches Intl — its output is NO locale (ASCII, ungrouped, 1235% on every machine including en-US), not the machine locale the card claimed. STOPPED exactly per the ruling's fields-read-only clause, current behavior pinned AS EVIDENCE with the pin expected to red the day formatPercent grows a locale.
  • The deliberate title deviation is accepted and commended: the ruled title would have asserted on main's permanent history something the measurement disproved. Part of #4553 keeps the card open. The NBSP self-catch and the merge-base clarification on the misleading diff --stat are both the right reflexes.
  • dts byte-identical → patch; fix(fields): the date formatter's last three en-US channels thread the display locale (#4272) #4544/fix(plugin-gantt): tooltip currency re-formats when the tenant currency resolves (#4542) #4554 pins green; eslint drift zero (the 5 new warnings are the sibling files' exact idiom); CI 20/20 on per-job conclusions.

Percent ruling: option A — posted on #4553; the same dev continues. Auto-merge armed (squash) — landing verified per the merge-queue discipline.


Generated by Claude Code


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 13, 2026 09:05
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 13, 2026
Merged via the queue into main with commit a908882 Aug 13, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4553-gantt-tooltip-number-locale branch August 13, 2026 09:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants