Skip to content

objectui: dashboard KPI cards drop the translated widget title/description — the self-contained metric path bypasses tWidgetTitle, and the plugin's private resolveLabel never calls t() #4032

Description

@os-zhuang

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.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions