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() #5428

Description

@yinlianghui

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.

Activity

  1. yinlianghui commented on Aug 5, 2026

    @yinlianghui
    CollaboratorAuthor

    决策位登记(objectui 分片 PM session_01GTRjn8xBqp75dk7kFupVRt,分析出自 #5407 dev 的测量,PM 复核采纳):

    背景:dashboard filter chip 前缀(Owner:)与 KPI 副标题不翻译的真因是 app 元数据层无本地化通道 + 本单记录的渲染器缺陷(三份私有 resolveLabel 接受 I18nLabel 却从不调 t();type:'metric' self-contained 分支绕过 tWidgetTitle)。渲染器缺陷本身无争议可直接修;待拍板的是 DashboardFilterDef.label 的契约形状:

    • A(推荐):放宽为 string | I18nLabel,与本单渲染器修复打包落地。① 平台长远:dashboard 的 widget title/description 已有「约定 key + I18nLabel」成熟机制,filter label 是同层同类,补齐是收敛不是新方言;② 防 AI 犯错:「声明即强制」—— 作者写 {key, defaultValue} 就必须被解析,而今天 I18nLabel 被接受后静默丢弃,正是 AI 元数据最难发现的「接受但失效」形状。
    • B:消费端走 useSafeFieldLabel 约定解析 —— 被否决候选:DashboardFilterDef 不携带 object 名(仅 lookup 型有 optionsFrom.object),注定「部分 chip 翻了部分没翻」,且属 AGENTS.md #0.1 禁止的消费端猜测。
    • C:app 侧自行写本地化文案(hotcrm#682 下游) —— 最小动作,但渲染器的 I18nLabel 惰性留在原地,下一个 app 再踩。

    维护者拍板 A/C 即可;A 拍板后本分片打包派发(渲染器三处 + 类型放宽 + 测试)。在此之前本单不派。


    Generated by Claude Code

  2. xuyushun441-sys commented on Aug 6, 2026

    @xuyushun441-sys
    Collaborator

    维护者裁决(2026-08-06,经 PM 会话 session_01MN3ao9kJEtD7PNAKwFVp96 转达):A —— DashboardFilterDef.label 放宽为 string | I18nLabel,与渲染器修复打包一单落地:

    1. 类型放宽:packages/core/src/utils/dashboard-filters.ts 的 DashboardFilterDef.label,filter bar 消费端解析 I18nLabel;
    2. 渲染器三处私有 resolveLabel(MetricCard.tsx:15 / MetricWidget.tsx:118 / DashboardRenderer.tsx:92)接 t(),不再静默丢 key;
    3. self-contained metric 分支接回 tWidgetTitle / tWidgetDescription 通道,KPI 卡与其它 widget 同源翻译;
    4. 正文遗留设计点(options.description 子标题的 key 归属)PM 裁量:子标题给自己的 convention segment,⛔ 不与 widget.description 共用一个 key —— 两个作者字段两个 key;维护者可否决。

    摘 needs-user-decision,回 pm:queue,按分片此前计划打包派发(渲染器三处 + 类型放宽 + 测试)。


    Generated by Claude Code

  3. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    Contributor

    Migrated to objectstack-ai/objectui#4032 under #7167 (file-at-destination ruling, maintainer 2026-08-10). Native GitHub issue transfer is not available to this session's credential, so the card was recreated at the destination; this thread stays as the authoritative history and is linked from the new card. Closing as not planned here — moved, not rejected.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions