Skip to content

fix(plugin-report): report and preview measures follow the display locale (#4575) - #4582

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4575-report-preview-measure-locale
Aug 13, 2026
Merged

fix(plugin-report): report and preview measures follow the display locale (#4575)#4582
yinlianghui merged 1 commit into
mainfrom
claude/issue-4575-report-preview-measure-locale

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4575

objectui#4566 (PR #4577) gave formatMeasure / formatDimensionValue in @object-ui/core an optional trailing locale and threaded useDisplayLocale() through the dashboard's DatasetWidget. The parameter is OPTIONAL by design, so the producer could land without dragging every consumer with it — which left the consumers it could not reach still formatting in the MACHINE's locale. A German session read a report measure as 1,234.5 directly beside a dashboard measure that, after #4566, rendered 1.234,5.

What changed

All thirteen censused call sites thread useDisplayLocale(). Line numbers were re-located by content and matched the issue's census exactly:

file sites
packages/plugin-report/src/DatasetReportRenderer.tsx 10 — grouped table measure / dimension / grand-total cells (487, 488, 502), single-value chart metric (737), bucketLabel (792), cross-tab down-axis cell (980), measure cell (994), row total (1005), column total (1019), grand total (1029)
packages/app-shell/src/views/metadata-admin/previews/DatasetPreview.tsx 2 — measure cell (228), dimension cell (229)
packages/app-shell/src/views/metadata-admin/inspectors/DatasetDefaultInspector.tsx 1 — the measure format-hint sample (181)

Two findings shaped the implementation:

The cross-tab formats inside a useMemo. bucketLabel is called from within the pivot memo, so it takes the locale as a parameter and the locale joins that memo's dependency array. Threading the call alone would leave the across-axis headers frozen in whatever locale they were first built with.

The metadata designer's locale prop is deliberately NOT the channel. That prop comes from useMetadataLocale(), which resolves to exactly en-US or zh-CN — it is the designer's chrome language, so a German session gets en-US from it. The preview's numbers must match what the report and dashboard render for the same dataset, which is useDisplayLocale(). A dedicated case pins the discriminator: chrome locale held at en-US while the session is German still renders 1.234,5.

Red-first, with the directions predicted in writing

Runner machine locale measured as en-US. Every case pins the same value in de AND en, so at least one half must fail on ANY runner — before the change both render in the machine's locale, which is what makes the machine locale stop being a test input.

Predicted, then measured — 11 red / 12 green, exactly as written:

AssertionError: expected '1,234.5' to be '1.234,5'
AssertionError: expected '12.3%'   to be '12,3%'
AssertionError: expected '€1,234.50' to be '1.234,50 €'
TestingLibraryElementError: Unable to find an element with the text: 1.234,5

The 12 green-on-both-sides cases are the en byte-identity pins plus the labelled guards. English output does not move — these sites already grouped through Intl, so the only change is whose locale is used. (Contrast objectui#4553, where formatPercent had never grouped and moving en WAS the fix.)

Reverse verification of the dependency array

Removing only the dep-array entry, leaving the locale threaded into the call, turns exactly one case red and leaves the other nine green:

× re-labels the across header when only the locale changes (memo dependency)
  TestingLibraryElementError: Unable to find role="columnheader" and name "2.468,5"
Tests  1 failed | 9 passed (10)

That isolation is only possible because the harness re-uses one report element across the rerender, so React bails out on the unchanged child: the parent does not re-render, the readNames(...) array props keep their identity, and useDatasetRows does not refetch (its effect is keyed on a signature the locale is not part of). Restored and verified byte-identical by sha256. Not reasoning-guarded — genuinely isolated.

Malformed-tag safety is not re-implemented; it belongs to the producer's formatNumberInLocale retry. One guard per package pins only that a bad tag does not take the surface down, asserted against the producer's own no-locale output so it stays machine-locale-independent.

Verification

  • New suites: 23 passed (3 files).
  • Regression sweep: packages/plugin-report, metadata-admin previews + inspectors, plus fix(core): dashboard measures follow the display locale (#4566) #4577's producer pins (dataset-format.locale, DatasetWidget.measureLocale) — 99 files, 1145 passed, 1 skipped, 0 failed. Existing suites green and untouched.
  • Type-check: both packages, both tsc passes (tsc --noEmit && tsc -p tsconfig.test.json) — clean.
  • ESLint vs origin/main baseline — NET ZERO: plugin-report 72 → 72, app-shell 2373 → 2373 (0 errors both sides).
  • .d.ts both ways: with dist/ and tsconfig.tsbuildinfo cleared between builds, plugin-report (10 files) and app-shell (416 files) declarations are byte-identical. Falsified with a positive control — a temporary probe export made the comparison differ, and reverting returned it to identical — so the check is not blind. Hence patch + patch, never minor: the only signature that gained a parameter is the file-local, non-exported bucketLabel.
  • Control bytes: check:control-bytes OK; NBSP/NNBSP written as escapes throughout, with trim-only normalizers where Testing Library's default would collapse them.
  • check:phantom-deps: OK — both packages already declared @object-ui/i18n.

Note

These surfaces are now deterministic where they previously followed whatever locale the machine ran in: useDisplayLocale ends at a concrete 'en' rather than the undefined that hands Intl the machine's locale. The inspector case that mounts with no providers at all documents that, and is why the sibling suite stays green untouched.

Surface respected: ReportViewer.tsx (#4548), packages/react, SettingsView (#4570), apps/console entry (#4563), types + ObjectGrid exportOptions (#4535) and content/docs/releases/ were not touched.


Generated by Claude Code

…cale (#4575)

objectui#4566 gave `formatMeasure` / `formatDimensionValue` an optional trailing
`locale` and threaded `useDisplayLocale()` from the dashboard's `DatasetWidget`.
The parameter is optional by design, so the consumers that card could not reach
kept formatting in the MACHINE's locale — a German session read a report measure
as `1,234.5` beside a dashboard measure rendering `1.234,5`.

The remaining thirteen sites thread the display locale: DatasetReportRenderer
(ten — grouped table cells and total, the single-value chart metric, and the
cross-tab's header, down-axis cell, cell and three totals), DatasetPreview (two)
and DatasetDefaultInspector's format-hint sample (one).

English output does not move: these sites already grouped through `Intl`, so the
only change is whose locale is used. Every new case pins de AND en, so at least
one half must fail on any runner.

The cross-tab builds its header labels inside a `useMemo`, so the locale joins
that dependency array — measured: removing only that entry turns exactly the
dep-isolation case red and leaves the other nine green.

The metadata designer's `locale` prop is deliberately not used — it is the
designer's chrome language (`useMetadataLocale()`: exactly en-US or zh-CN), not
a number-formatting locale.

Both packages are patch: their built `.d.ts` are byte-identical (dist cleared
between builds; verified falsifiable with a probe export).

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 1:31pm

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-BrUYGs1L.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.33KB 108.47KB
core (index.js) 3.37KB 1.34KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 163.56KB 44.83KB
fields (index.js) 230.37KB 57.17KB
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) 121.04KB 31.57KB
plugin-designer (index.js) 212.58KB 42.83KB
plugin-detail (index.js) 239.93KB 60.01KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 164.30KB 40.02KB
plugin-grid (index.js) 189.37KB 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.38KB 11.09KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.09KB 20.56KB
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) 27.64KB 9.44KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.26KB 0.67KB
react (schema-input.js) 1.45KB 0.83KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 13, 2026 13:42
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 13, 2026
Merged via the queue into main with commit cb315f2 Aug 13, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4575-report-preview-measure-locale branch August 13, 2026 13:42
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.

[plugin-report][app-shell] Dataset measures still render in the MACHINE locale — the consumers #4566 could not reach

2 participants