Uh oh!
There was an error while loading. Please reload this page.
fix(dashboard,report): localize a LOCAL select dimension on table/pivot and the dataset report block (#4330) - #4388
Merged
Conversation
…ot and the dataset report (#4330) A dashboard table grouped by a select field rendered `Domestic` on a zh-CN console while the related list beside it rendered 国内. The label was not missing — the server resolves it (ADR-0021) and sends the object's AUTHORED English label — and the locale bundle is keyed by the option's stored VALUE, so translating one needs the option LIST. objectui#4263's landed pin asserted that this path issues no metadata read at all, so there was nothing on the client to translate against; PR #4324 measured that and left the half open. Table, pivot and `DatasetReportRenderer` now take that one read and feed it to the SAME seam #4324 landed (`resolveDimensionFieldMeta` → `localizeFieldOptions` / `buildDimensionLabelMap` → `relabelDimensions`). One channel, no second dialect. Applied at the shared map every consumer reads: cells, both pivot axes, the server's marginal totals (relabeled so their bucket lookup still meets the headers), the CSV export, and a report's embedded chart. The read is deliberately NOT gated on "a select dimension is present": `DatasetDimension.type` has no `select` member, every select dimension in the live example apps declares `type: 'string'`, and a select column arrives on the wire typed `'string'`. The gate lands on the read's OUTPUT instead, where it is exact — `resolveDimensionFieldMeta` yields an entry only for a terminal field that carries `options`. Identity keys are untouched: drills still filter by the values the server sent (`drillRawRows` / `groupKey` / `objectFilter`), and measures still export as bare numbers. Untranslated apps are unchanged by construction — no bundle entry means no map key, so the rows come back by identity. #4263's no-metadata-read pin, and #4324's restatement of it, are rewritten in place in this commit to assert the new boundary and to say why it moved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
#4330) The reverse verification measured that pin RED on its read-count assertion, not green in both directions as the header predicted: after this change the read IS issued for a dimension whose field owns no options, and resolves nothing. Its rendered half is green either way. Recording the measurement rather than the prediction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
commented
Aug 12, 2026
CollaboratorAuthor
ACCEPT — PM 复核 (session
Flipping ready + arming auto-merge. Generated by Claude Code |
yinlianghui
marked this pull request as ready for review
August 12, 2026 01:07
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#4330
Residual of #4030 / PR #4324, dispatched with the PM boundary-amendment ruling recorded on the card at queue time.
The gap, and why #4324 could not reach it
A dashboard table grouped by a local select dimension rendered
Domesticon a zh-CN console while the related list on the same screen rendered 国内. The label was never missing: the server resolves it (ADR-0021) and sends the object's authored English label. The locale bundle is keyed by the option's stored value (fieldOptions.< obj >.< fld >.< value >), so translating one needs the option list — and #4263's landed pin asserted that this path issues no metadata read at all. There was nothing on the client to translate against.DatasetReportRendererwas the same shape, which is why plugin-report carried no change in #4324.What this does
Table, pivot and the dataset report block take the one read that gives the bundle an option list, and feed it to the same seam#4324 landed —
resolveDimensionFieldMeta→localizeFieldOptions/buildDimensionLabelMap→relabelDimensions, translatoruseSafeFieldLabel().fieldOptionLabel. One channel, no second dialect.On the dashboard side the change is the deletion of the table-only branch:
dottedOnly = isTableand itsresolveDimsfilter are gone, so table/pivot resolve every dimension exactly as charts already did. Chart-only wiring (per-category colours, declared category order) still staysnullon that path — #4330 widened which dimensions get a label map, never what a table consumes.plugin-report had no such machinery at all, so it gains
useDatasetDimensionLabels— fetch-and-memo glue only, calling the same@object-ui/corehelpers. Its natural home is core, beside those helpers; it is report-local becausepackages/coreis held by #4040 tranche 5. Applied at the shared map every consumer reads: tabular cells, both matrix axes, the server's marginal totals (relabeled, so their bucket lookup still meets the headers it belongs to), and the report's embedded chart — leaving that last one out would have put 国内 in the table andDomesticon the axis of the very same report.The amended pin: old boundary → new boundary
The ruling was explicit that #4263's pin is amended deliberately, in this PR, not worked around. #4263 ruled it for label resolution — "the label already exists, do not produce it twice" — and it held exactly as long as resolution was the read's only consumer. #4030 added a second one. Three pins were rewritten in place:
DatasetWidget.dottedDimensionTable.test.tsx·renders a LOCAL-only table …expect(requested).toEqual([])— no read at alltoEqual(['crm_opportunity']); the rendered string is still byte-identical (no bundle entry ⇒ no map key ⇒ rows returned by identity). That identity, not the absence of a fetch, is what "no double resolution" means noweducation, CSVeducation,Education,10Education, CSVEducation,Education,10DatasetWidget.optionLabelI18n.test.tsx·a LOCAL-only table still resolves nothing and fetches nothingOrion Engineered Carbonsunder a mounted zh bundle — this test's own green was the bug's pin, and #4324's summary said so out loud欧励隆, plus the one readEach carries the reasoning and cites #4330 at its site; the file headers carry the boundary change.
DatasetWidget.localSelectI18n.test.tsxandDatasetReportRenderer.localSelectI18n.test.tsxare the new behaviour's own suites. Untouched and still green: the dotted walk, the multi-hop walk, and the METRIC branch's silence (it renders no dimension value, so it still resolves nothing and reads nothing).Read gating: measured, and neither of the two the card sketched
The card asked for locale-gated or unconditional-when-select, with the measurement. The measurement disqualified the premise of the second and the architecture of the first, so the shipped rule is unconditional when the widget has dimensions, with the select gate on the read's OUTPUT.
Select-ness is not observable before the read. Three independent confirmations:
@objectstack/spec'sDatasetDimension.typeisstring | number | date | boolean | lookup— there is noselectmember, so no authored dataset can declare one (PR fix(core,plugin-dashboard,plugin-charts): run resolved select-option labels through the i18n bundle on analytics surfaces (#4030) #4324 hit the same wall from the other side and recorded it as "real at authoring time and moot at runtime");type: 'string'—showcase_task.status/.priority,showcase_invoice.status,showcase_account.industry/.sales_regionare allField.selectobjects behind atype: 'string'dimension;AnalyticsResult.fields[].typefor a select column is'string'too (the analytics cube registry'sfieldTypeToDimensionTypemaps everything that is not number/boolean/date to'string').So a pre-read gate spelled
type === 'select'would pass hand-written fixtures and never fire in production. The gate lands on the output instead, where it is exact rather than heuristic:resolveDimensionFieldMetareturns an entry only for a terminal field that actually carriesoptions, so a text / number / date / lookup dimension yields no map andrelabelDimensionshands back the caller's rows by identity. Pinned both ways —pins the read is issued ONCE …andBOUNDARY — a dimension whose field carries no options renders untouched.The locale gate is not worth the branch, on three measured costs against one saved request:
useSafeFieldLabelexposes no language anduseObjectTranslationis not provider-safe — i.e. a new public export in@object-ui/i18n, a package with three tasks in flight;enconsoles whose server left a local select unresolved, turning an i18n gate into a behaviour gate.Cost of the choice, stated plainly: +1
GET /meta/object/:nameper dataset table / pivot / report that has at least one dimension and a server-suppliedobject— including one that resolves nothing. Measured as exactly 1 per surface, not one per dimension:resolveDimensionFieldMetamemoizes per call, so the two-dimension table and the two-axis pivot each issue one. In the live corpus (5 dataset table/pivot widgets across app-showcase and platform-objects) 2 carry a select dimension and would resolve, 2 are lookup-only and would not, 1 groups byaction.Pre-fix red evidence (reverse verification, per limb)
Direction predicted in both file headers before running. Committed first, then reverted with
git checkout origin/main -- < path >(nevergit stash), re-run, restored.Limb 1 —
DatasetWidget.tsxreverted (predicted: the zh-CN cells / headers / totals / CSV and every read-count pin red,enand no-bundle-entry green, plugin-report untouched):expected 'Domestic' to be '国内'is the card's screenshot in one line. All three failing files are plugin-dashboard's; plugin-report stayed green, which is what makes it a limb.Limb 2 —
DatasetReportRenderer.tsx+ the new hook reverted (predicted: the six new report pins red,engreen, dashboard untouched):One prediction was wrong and is corrected rather than re-fitted. The headers first called the no-options pin green in both directions; it measured red on its read-count half (green on its rendered half). That is the pin's whole point after this change — the read is issued for a dimension that owns no options and resolves nothing — so the header now records the measurement, in its own commit.
Two drill pins are also in the red set for a sequencing reason rather than an assertion one, and say so at their site: they wait for the translated cell so the click lands after the metadata read settles, and that wait is what fails without the seam. Clicking as soon as the rows exist raced the read (it produced the one genuine failure during development) and would have proved nothing about a drill after the relabel. Their assertions are direction-independent.
Identity keys stay untranslated
Display translates; identity does not (#4263 / #4273's convention).
a drilled row still filters by the STORED value, AFTER the relabel: the row reads 国内 / 欧励隆 and the filter is{ sales_channel: 'domestic', competitor_name: 'orion' }, read offdrillRawRows, which no relabel touches.groupKeyandobjectFilterare built fromstate.rows[i], the row as the server sent it, never the displayed one. In the matrix the split is structural: a header'skey(drill identity) comes from the raw row while itsdisplaycomes from the relabeled one — two different objects, pinned byexpect(drills[0].groupKey).toEqual({ sales_channel: 'Domestic', … })firing while the cell beside it reads 国内.downloadCsvalready prepends is what makes Excel read it. Measures stay bare numbers:expect(body).toBe('国内,欧励隆,7').['5','7'], grand12) on both surfaces.Verification
Also green: build closure for both packages' dependencies;
tsc --noEmitfor plugin-dashboard;tsc --noEmit+tsc -p tsconfig.test.jsonfor plugin-report;eslinton the seven touched files (0 errors, warnings only — the one newset-state-in-effectwarning is the same shapeDatasetWidget's two existing effects anduseDatasetRowsalready have);check-control-bytes,check-changeset-presence,check-changeset-no-major. Changeset:patchfor both packages. No locale-pack key is added — these labels live in object metadata, not the i18n packs, socheck:i18n-keys/check:i18n-drifthave nothing new to see.Surfaces
packages/plugin-dashboard/**andpackages/plugin-report/**only, plus one changeset.packages/coreis untouched — the shared helpers already export everything this needed — so #4040 tranche 5, #4363 and the i18n trio are not crossed.Generated by Claude Code