fix(dashboard,i18n): KPI cards and dashboard filters resolve authored labels instead of dropping them (#4032) - #4358
Merged
Conversation
|
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 21:15
This was referenced Aug 11, 2026
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 #4032.
Part of, notFixes: ruling item 4 (the sub-caption's own convention segment) is blocked by a hard spec constraint and is returned as a decision rather than implemented — see "Item 4" below. Items 1-3 and the merged #4163 filter-bar scope are complete.The ruling this implements
From objectstack#5428, maintainer decision of 2026-08-06, quoted verbatim:
The merged scope comes from the #4163 part-1 audit comment of 2026-08-11, which recorded two compiler-blind filter-bar reads as this ruling's implementation surface.
Premise check: the ruling's item 2 mechanism has expired, its goal has not
Verified against
origin/mainbefore implementing, and this changes how item 2 lands.Item 2 says the three private
resolveLabelcopies should hand theirkeytot(). Those copies read{ key, defaultValue }— and that vocabulary was retired from@objectstack/specafter the ruling was written (objectstack#5055, in 17.0.0-rc.6). Measured:So there is no
keyleft to hand tot(): an author who writes that form today has their dashboard rejected at authoring, not rendered in English. Wiring it tot()would have resurrected a form the contract now refuses — the opposite of contract-first.The ruling's goal is untouched and is what this PR delivers, through the two channels that are actually live:
{ en: 'Revenue', 'zh-CN': '收入' }), resolved bypickLocalized;{ns}.dashboards.{dash}.widgets.{id}.title), which ist()and needs no annotation on the authored value.They compose in the order
useActionTextLocalizer(#4265) already established: collapse the authored value to the active language first, hand the resulting plain string to the bundle as its fallback. A bundle entry always wins; a per-locale literal and a bundle entry can never disagree about what "the authored title" is.The live defect is also worse than the card states. The card predicts "the English
defaultValuein every locale". Measured, a spec-valid map title maderesolveLabelreturnundefined, so the caller's|| widgetTypefallback took over and the KPI card rendered the literal stringmetric— the widget type, a plausible-looking word substituted for the title with no diagnostic.Resolver seam: reused, not re-invented
pickLocalizedfrom@object-ui/i18n— the seam #4208 landed for this exact vocabulary on the sibling surface (DashboardGridLayout.tsx, sameDashboardWidget.titlefield), pinned limb-for-limb against the spec's ownresolveI18nLabelover 96/96 spec-valid inputs. No fourth dialect was added; the three private copies are deleted rather than fixed in place.What changed
DashboardFilterDef.label→string | I18nLabel;options[].labellikewise.@object-ui/corestays locale-free and carries the vocabulary through; the filter bar resolves.DashboardFilterBarresolves before rendering — trigger text,aria-label,TextFilterplaceholder/aria-label, date-rangearia-label. Thedef.label || def.namegate now tests the resolved string (an object is always truthy, so it never reached the fallback).normalizeFilterOptionsno longer coerces a map label toString(value). That line discarded the authored text in every locale, English included:{ value: 'domestic', label: { en: 'Domestic', … } }displayed asdomestic.DashboardComponentSchema.globalFiltersbound to the spec'sGlobalFilterinstead of restated ("绑定优先于重述").resolveLabelcopies deleted;DashboardRenderer,MetricWidget,MetricCardall resolve throughpickLocalized.metricbranch takes its label fromtWidgetTitle(widget)— the same convention channel every other widget header uses.MetricWidgetkeeps two distinct locale channels on purpose:useDisplayLocalefor numbers (#4333's threading, untouched) andlanguagefor label text.Item 4 — the sub-caption segment is blocked at the spec, not chosen
The ruling gives the sub-caption its own convention segment. The spec does not admit one.
TranslationDataSchema.dashboards.{dash}.widgets.{id}isz.core.$strictover exactly{ title?, description? }:Every candidate name is refused, and the spec's own suggestion points the sub-caption at
description— i.e. at precisely the shared key the ruling forbids. The two constraints are in direct conflict,objectstackis read-only for this task, and the dispatch brief names this case a stop rather than a workaround. Details and options are in the report.Consequence for reviewers:
widget.descriptionon a self-contained metric is still not rendered by this PR. Giving it the only available slot would have put it in the sub-caption, which is the conflation item 4 exists to prevent.Verification
Red-first, with the direction of every case written down before it was run. All predictions held, including the two that were not the obvious guess: the map-title case fails as the literal
metricrather than[object Object], and the option-label case is red inentoo (data loss, not a translation gap).Reverse verification by limb (
git checkout origin/main -- < file >, re-run, restore — nevergit stash):DashboardRenderer.tsxDashboardFilterBar.tsxdashboard-filters.ts(core, alone)MetricWidget.tsx+MetricCard.tsxcomplex.ts(the binding)type-checkREDThat last row is the point of
dashboard-global-filters-spec-binding.test.ts. The binding is the one part of this change no runtime test can falsify — a type that is too narrow does not fail, it makes the read sites that would have failed invisible totsc, which is exactly how these reads shipped. Reverting it reproduces that signature deliberately.Against the merge with current
main:Downstream direction stated explicitly:
app-shellandplugin-designerare the consumers of the two changed contracts (@object-ui/core'sDashboardFilterDef,@object-ui/types'globalFilters), reached by consumer-direction filter, not the upstream dependency set.One file outside the nominal scope
packages/app-shell/.../DashboardWidgetInspector.tsx, two lines. This is the widening's forced blast radius, not scope creep: wideninglabelturns two previously compiler-blind reads intotscerrors (TS2322at 369 and 389), soTypeScript Type Checkcannot go green without them. Both are repaired with the file's own existing resolver (resolveInlineI18nLabel, already imported there forwidget.title), and resolving before the||fixes the same truthiness gate. Flagging it because that file is held by a continuation task — it does not overlap that work, but the PM should know it was touched.Out of scope, filed
normalizeFilterOptionsaccepts a bare-stringglobalFilters[].optionsshorthand that@objectstack/specrejects at publish #4356 —normalizeFilterOptionsaccepts a bare-string option shorthand the spec rejects at publish. Surfaced by the binding: the restatement was not only too narrow onlabel, it was too wide here. The type half is fixed and pinned in this PR; the runtime tolerance is deliberately left for stored documents.MetricWidget/MetricCardspread...propsonto the DOM, emitting aschema="[object Object]"attribute on every KPI card #4357 —MetricWidget/MetricCardspread...propsonto the DOM, emittingschema="[object Object]"on every KPI card. Observation-class. It is the reason this PR's pins assert on the card heading rather thancontainer.innerHTML.Generated by Claude Code