Migrated from objectstack-ai/objectstack#5428 under the file-at-destination ruling (objectstack-ai/objectstack#7167, maintainer 2026-08-10). Originally filed 2026-08-05T11:08:19Z. The full prior thread — including triage rulings and hold/restart conditions — remains on the source issue and MUST be read before acting on this card.
Found while re-locating objectstack#5407's item 3. That item's stated premise (dashboard chips / KPI sub-captions are "console-supplied chrome that no app bundle key reaches") does not hold — both strings come from app metadata — but the measurement turned up a real renderer defect underneath, filed here.
1. resolveLabel accepts the I18nLabel shape and then ignores it
Three private copies in packages/plugin-dashboard/src:
MetricCard.tsx line 15
MetricWidget.tsx line 118
DashboardRenderer.tsx line 92
all read string | { key?, defaultValue? } and all end with return label.defaultValue || label.key; — the key is never handed to t(). Compare packages/layout/src/NavigationRenderer.tsx line 260, whose resolveLabel takes a t and does translate.
So a dashboard author who writes the documented localized-label object gets the English defaultValue in every locale. The escape hatch is declared but inert: exactly the "declared = enforced" gap that makes AI-authored metadata quietly wrong.
2. A self-contained metric widget never sees the translated title/description at all
DashboardRenderer.tsx:
- line ~653:
const isSelfContained = widget.type === 'metric' && !datasetBound;
- lines 654-655 compute
resolvedTitle / resolvedDescription via tWidgetTitle / tWidgetDescription (the working {ns}.dashboards.{dash}.widgets.{id}.title|description convention)
- but the self-contained branch renders no Card header, so neither value is used
- meanwhile line ~525, inside the metric dispatch, builds the card's own label with the untranslated
resolveLabel(widget.title) || widgetType, and spreads ...options (carrying options.description) straight through
Net effect: a type: 'metric' KPI card shows the raw authored English while every other widget type on the same dashboard shows the translated header. That is precisely the "widget titles/descriptions translate fine, which is what makes the mismatch conspicuous" observation in objectstack#5407.
Open design question — please decide before implementing
options.description (the metric card's sub-caption) is a different authored field from widget.description (the shared card header's). Routing the former through widgetDescription(dashName, widget.id, fallback) would give it the same convention key as the latter. For a self-contained metric that collides with nothing today, because widget.description is never rendered on that path — but it does conflate two authored fields under one key. Alternatives: give the sub-caption its own convention segment, or make the self-contained metric render the shared header like everything else. Worth settling deliberately rather than in a patch.
Not in scope here
The Owner: / Lead Source: chip prefix in DashboardFilterBar.tsx (line 226, def.label || def.name) is app metadata — DashboardFilterDef.label in packages/core/src/utils/dashboard-filters.ts is a plain string, with no I18nLabel form and no convention resolution. Whether the filter bar should resolve it through useSafeFieldLabel (as DatasetWidget already does for measure headers) is a separate contract question; the filter def carries no object name to key on, so it cannot simply copy that call.
Found while re-locating objectstack#5407's item 3. That item's stated premise (dashboard chips / KPI sub-captions are "console-supplied chrome that no app bundle key reaches") does not hold — both strings come from app metadata — but the measurement turned up a real renderer defect underneath, filed here.
1.
resolveLabelaccepts theI18nLabelshape and then ignores itThree private copies in
packages/plugin-dashboard/src:MetricCard.tsxline 15MetricWidget.tsxline 118DashboardRenderer.tsxline 92all read
string | { key?, defaultValue? }and all end withreturn label.defaultValue || label.key;— thekeyis never handed tot(). Comparepackages/layout/src/NavigationRenderer.tsxline 260, whoseresolveLabeltakes atand does translate.So a dashboard author who writes the documented localized-label object gets the English
defaultValuein every locale. The escape hatch is declared but inert: exactly the "declared = enforced" gap that makes AI-authored metadata quietly wrong.2. A self-contained
metricwidget never sees the translated title/description at allDashboardRenderer.tsx:const isSelfContained = widget.type === 'metric' && !datasetBound;resolvedTitle/resolvedDescriptionviatWidgetTitle/tWidgetDescription(the working{ns}.dashboards.{dash}.widgets.{id}.title|descriptionconvention)resolveLabel(widget.title) || widgetType, and spreads...options(carryingoptions.description) straight throughNet effect: a
type: 'metric'KPI card shows the raw authored English while every other widget type on the same dashboard shows the translated header. That is precisely the "widget titles/descriptions translate fine, which is what makes the mismatch conspicuous" observation in objectstack#5407.Open design question — please decide before implementing
options.description(the metric card's sub-caption) is a different authored field fromwidget.description(the shared card header's). Routing the former throughwidgetDescription(dashName, widget.id, fallback)would give it the same convention key as the latter. For a self-contained metric that collides with nothing today, becausewidget.descriptionis never rendered on that path — but it does conflate two authored fields under one key. Alternatives: give the sub-caption its own convention segment, or make the self-contained metric render the shared header like everything else. Worth settling deliberately rather than in a patch.Not in scope here
The
Owner:/Lead Source:chip prefix inDashboardFilterBar.tsx(line 226,def.label || def.name) is app metadata —DashboardFilterDef.labelinpackages/core/src/utils/dashboard-filters.tsis a plainstring, with noI18nLabelform and no convention resolution. Whether the filter bar should resolve it throughuseSafeFieldLabel(asDatasetWidgetalready does for measure headers) is a separate contract question; the filter def carries no object name to key on, so it cannot simply copy that call.