fix(core,plugin-dashboard,plugin-charts): run resolved select-option labels through the i18n bundle on analytics surfaces (#4030) - #4324
Merged
Conversation
…labels through the i18n bundle on analytics surfaces (#4030) The analytics label net resolved a select dimension's option label and then displayed the object's authored English label, so a chart legend read `Orion Engineered Carbons` while the related list on the same page read 欧励隆. Applies the ONE existing channel — `fieldOptionLabel` (`{ns}.fieldOptions.<object>.<field>.<value>`, what list/form/kanban/record picker surfaces already translate options through) — at the net's output, on the shared option list every consumer reads: chart axis/legend, dotted table/pivot cells, that table's CSV, per-category colours and category order. - core: `localizeFieldOptions` (pure mirror of `translateOptions`), an optional translator on `buildDimensionLabelMap` (which also keys the authored English label, so a server-resolved row re-translates), and `resolveDimensionFieldMeta` — the same single relationship walk, keeping the object that OWNS the terminal field because that is what the bundle key names. - plugin-dashboard / plugin-charts: keep the fetched metadata locale-free and derive colours + label maps during render, so a language switch re-labels in place instead of waiting for a refetch. Identity is untouched: a segment clicked as 欧励隆 still drills by `orion`, and an untranslated option (or an `en` console) renders exactly as today. Refs objectstack#5076
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 11, 2026 15:17
This was referenced Aug 11, 2026
Merged
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 free
to 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.
Part of #4030 — see "What this does NOT close" at the bottom for the half that is still open.
Source thread: objectstack-ai/objectstack#5076 (read-only; its triage comment is the ruling of record).
The seam I measured
There is one existing option-label translation channel, and analytics now reuses it rather than growing a chart-side per-locale dialect:
useObjectLabel().fieldOptionLabel(object, field, value, fallback)→{ns}.fieldOptions.< object >.< field >.< value >packages/i18n/src/useObjectLabel.tsowns it (optionSuffixes→fieldOptionLabel/translateOptions), reached from plugins through the provider-safeuseSafeFieldLabel.@objectstack/specnames objectui as that convention's reader by name — "objectui reads exactly those (asfieldOptions.< obj >.< fld >.< value >) off the full-bundle route" (ResolvedFieldLabel,dist/system/index.d.ts); the spacing inside the key is this body's, added so GitHub's tag stripper leaves the segments intact.DetailSectionandObjectKanbancalltranslateOptions(...)and hand the localized options toSelectCellRenderer, which readsoption.labeland knows nothing about i18n. objectui#3336 pinned that same "one source, two faces" property for the record picker.plugin-charts' legacy aggregate path was already on it (resolveGroupByLabels'stranslateOptionargument). Its signature(value, authoredLabel) => displayis reused verbatim as the newOptionLabelTranslator, so there is one shape for this seam in the repo, not two.The application point
At the output of the analytics label net (#4053/PR #4261, #4263/PR #4273), on the shared option list every consumer reads — never per surface:
relabelDimensions(rows, dimensionLabels)dimensionLabels, viadisplayRowsdisplayRows(the objectui#4263 convention)buildOptionColorMap(localizeFieldOptions(...))buildCategoryOrder(localizeFieldOptions(...))packages/core/src/utils/chart-series.tsgains three things:localizeFieldOptions(options, translate?)— the pure mirror oftranslateOptions. Colours, values and every other option key survive; onlylabelchanges, and the input array is returned by identity when nothing translated.buildDimensionLabelMap(options, translate?)— optional translator. With one it emits a second key per option: the authored English label.resolveDimensionFieldMeta(...)— the same single relationship walkresolveDimensionFieldOptionsalready performs, now keeping{ object, field, options }.resolveDimensionFieldOptionsbecomes a thin wrapper over it, byte-identical contract. This exists because the bundle key names the object that owns the terminal field: forcrm_account.industrythat iscrm_account, not the dataset's base object, and the walk already knew it and dropped it.Both plugins now keep the fetched metadata locale-free in state and derive colours + label maps in a render memo, so switching language re-labels in place instead of waiting for a refetch.
Why the map carries two keys
The rows reach this net keyed either way — by stored value when the server did not resolve the dimension (the reason the net exists at all), by the English label when it did (ADR-0021). The reported screen is the second case: the chart read
Orion Engineered Carbons, the object's authored label byte for byte. A value-keyed map alone cannot touch those rows, so a fix that only handled the un-resolved case would have left the issue open. One key resolvesorion, the other re-translatesOrion Engineered Carbons;relabelDimensionsis value-wise and idempotent, so whichever the row carries lands on the same display.Contract stop-condition: NOT triggered
The card's ruling was to stop and report
needs_decisionif closing the loop required adding aselecttype to specDatasetDimension. It does not. The runtime knows a dimension's field is a select without that vocabulary:resolveDimensionFieldMetaloads the owning object's metadata doc through the caller's existingGET /meta/object/:namechannel and reads the terminal field'soptionsstraight off it — the same read #4261 already performs.DatasetDimension.typeis never consulted, so the card's "no authoring knob exists" concern is real at authoring time and moot at runtime. No spec change, no objectstack change.Tests
New:
packages/core/src/utils/__tests__/chart-series.i18n.test.ts(17 pure pins) andpackages/plugin-dashboard/src/__tests__/DatasetWidget.optionLabelI18n.test.tsx(8 surface pins), both on the card's exact fixture — optionorion, object labelOrion Engineered Carbons, zh-CN bundle 欧励隆.Four of the surface pins are boundaries, green in both directions, and they are deliberately not sequenced on the translated string so they survive the reverse verification:
enconsole renders exactly what it renders today, with the same bundle mounted;categories()[0], so it holds either way;Reverse verification — direction predicted first
Prediction, written into both test headers before running: removing the seam at the two consumer call sites (core helpers left in place) turns the three zh-CN chart pins and the dotted-table pin red, and leaves the four boundaries plus every pre-existing pin green. Taken out with a temp commit +
git checkout, nevergit stash.Measured, exactly that — 4 red, 32 green:
That assertion text is the card's screenshot in one line.
Restored, then the full affected suites:
Every pin from #4261 / #4273 / #4263 / #4298 / cloud#667 / funnelOrder is green untouched — no existing test file was edited.
Also green:
turbo run type-checkfor the three packages,eslinton the touched files (0 errors),check-i18n-call-site-keys,check-i18n-en-drift,check-control-bytes,check-changeset-presence.What this does NOT close
Both are measured, not assumed:
A LOCAL select dimension on a table/pivot. The server resolves that label (ADR-0021) and objectui#4263 deliberately keeps the client net off there — its pin asserts a local-only table issues no metadata read at all. Translating it needs the option list, i.e. that read. Reaching it means re-opening a landed acceptance boundary, which is a ruling, not an implementation detail. The dataset report block is the same shape:
DatasetReportRendererrenders server-resolved rows and never loads the object schema.The dashboard global filter's field label ("Sales Channel:"). The one-channel rule cannot be satisfied here today, and the blocker is contract, not code:
GlobalFilterSchemadeclares no object,DashboardSchemadeclares none either, sofields.< object >.< field >has no key to build; and the spec'sTranslationData.dashboards.< name >node is strict with onlylabel/description/actions/widgets, so afiltersslot cannot be authored without new vocabulary. Deriving an object from the bound widgets is available only forprovider: 'object'widgets (a dataset widget's object arrives with the query response), so it would be a guess on exactly the dashboards the report is about. This is the sibling of the stop-condition and is written up as an open question for the maintainer rather than guessed at.Separately, two compiler-blind
I18nLabelreads on that same filter bar were found and reported onto #4163 (whose Part 1 is precisely that audit) rather than opened as a twin issue: #4163 (comment)Generated by Claude Code