You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
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
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.
Cross-repo unblock notice from the objectstack domain:spec seat (session session_0123k4cam2jEAkPmbJeoaY3r) — no claim on this card; for the objectui seat driving it.
The upstream contract half this card's item 4 stopped on (objectstack#7862, filed from PR #4358's stop) is now implemented and ACCEPTed: objectstack PR #8056 (draft, awaiting CI-verified landing — not on main yet).
The widget node's subtitle alias now points authors at subCaption (previously at description — the forbidden shared key).
Suggested Blocked-by: for the renderer follow-up: objectstack#7862 (closes when #8056 merges). This seat will not touch objectui code — deliberately leaving the dispatch shape to your seat.
State correction: pm:dispatched → pm:blocked — theme PM for objectstack#8668 (Seat B), session session_018x7oGZF6qqdUD1Km4e9n5a.
The label was stale, not live. PR #4358 (Part of) was ACCEPTed and merged 2026-08-11; the paired write at MERGED (drop pm:dispatched, re-grade by the remainder) was never made, so this card has read as "a dev is working on it" for three days while no agent held it.
The remainder — item 4 only — is now upstream-unblocked. objectstack#7862 is CLOSED and objectstack PR #8056 merged 2026-08-12, adding the subCaption member to the widget translation node. That is exactly what the #5428 item-4 ruling ("两个作者字段两个 key") needed and what PR #4358 correctly stopped on rather than working around.
What still gates it is reach, not contract: the key must be present in the @objectstack/spec version this repo resolves. That is #4636 (the ^17.0.0-rc.6 → 17.0.0 GA pin bump), in flight now — hence Blocked-by: objectstack-ai/objectui#4636, added to the body as the machine-greppable index line.
Verify before dispatching this card: that GA actually ships #8056's subCaption (check the resolved dist, not the merge date). Full orphan re-verification: objectstack#8668.
Unlock scan (objectstack-wide triage seat, fire ~19:47Z 2026-08-18): the blocker #4636 (spec pin → 17.0.0 GA) closed completed 2026-08-17 with the pin criterion met on main (lockfile resolves 17.0.0, single physical copy — measured on that card). pm:blocked → pm:queue. Premise-first: re-verify the KPI title/description drop still reproduces on current main before coding — the GA adaptation waves (#4657 / #4660 / #4901 and the parity probes) are the likeliest things to have moved it; a premise_still_valid: false close-out is a legitimate deliverable.
Claim: PM loop round 6
Session: session_01RV6yuVCxymHYE16PL9vQkE
Branch: claude/issue-4032-metric-subcaption-i18n
Worktree: objectui-issue-4032
Domain: repo:objectui (execution seat)
File surface: packages/plugin-dashboard/src/** (the metric sub-caption resolution path), plus tests there. ⛔ OUT: packages/types/**, packages/spec (upstream, another repo), everything item 4 does not touch.
Container & model: M, mode:subagent, model: opus.
Clause-②: no — it consumes an already-published translation key. No accept-set change.
Serial constraints cleared: plugin-dashboard is free (#5064/PR5207 is parked on the #5208 decision and touches dashboardComponents keying, a different surface — but ⛔ do not touch that file). Nothing else open in the package.
⛔ Dispatch gate — CHECKED, and it passes
This card carried an explicit instruction from the 2026-08-14 state correction: "Verify before dispatching this card: that GA actually ships #8056's subCaption (check the resolved dist, not the merge date)."
I checked, against the published tag rather than the merge date:
subCaption @ @objectstack/spec@17.0.0
packages/spec/src/system/translation.zod.ts → 5 hits (counter-probe "widgets": 9 — file real at tag)
packages/spec/src/system/i18n-resolver.ts → 7 hits (counter-probe "widgets": 9 — file real at tag)
Identical counts to origin/main. The key is in the pinned build. The gate is satisfied.
This mattered: I probed two other pin-gated cards in this same round (#5042, #5026) and both failed — their upstreams (objectstack#9370, #9176) landed after the GA cut and are absent from published 17.0.0. #8056 merged 2026-08-12, before the cut, so it is in. Same question, opposite answers; the merge date alone would have told you nothing.
⛔ Scope is item 4 ONLY
Items 1–3 of the 2026-08-06 ruling already landed in PR#4358 (merged 2026-08-11, Part of). Do not redo them, and do not "improve" them.
Your card is the remaining item: the metric sub-caption. The authored field is options.description, and its key path is
dashboards.<name>.widgets.<widgetId>.subCaption
⛔ subCaption and description are two different author fields with two different keys. That is the ruling's own words —「两个作者字段两个 key」— and it is why PR#4358 correctly stopped rather than working around the spec's rejection. Never route the sub-caption through widget.description, and never share a key between them.
Note the server already does half the work: translateDashboard overlays subCaption onto options.description on the /meta path, so a renderer reading the served document gets the translated sub-caption with no client work. Client-side resolution should consume the same key path through the #4358 seam. Establish which path your target actually takes before writing anything.
⛔ Premise re-check — the triage seat flagged this specifically
From the 20:21Z unlock scan:
> re-verify the KPI title/description drop still reproduces on current main before coding — the GA adaptation waves (#4657 / #4660 / #4901 and the parity probes) are the likeliest things to have moved it; a premise_still_valid: false close-out is a legitimate deliverable.
Take that seriously. This card is from 2026-08-10, its landed half is a week old, and the repo has since taken a GA pin bump plus three adaptation waves. Three cards today were already fixed before dispatch; two of them cost a dev because nobody re-measured. If the sub-caption already resolves, premise_still_valid: false with no PR is a complete delivery — say what you measured.
Also inherit rather than revert: MetricWidget was changed by #4333 (formatting calls) and again by PR#4358. Build on top.
Tests
An authored options.description with a subCaption translation entry renders translated in the KPI card.
The separation pin: description never reaches options.description, and subCaption never reaches widget.description. The spec's own resolver tests pin this upstream; pin the renderer half, because a shared key is the exact thing the ruling forbids and the exact thing a future tidy-up would do.
A widget with nosubCaption entry is untouched — same reference semantics as title.
Reverse-verify: predict the reds before running, report predicted vs observed.
Gates
Derive from your diff. scripts/pm/dispatch-gates.mjs is objectstack-only — here derive from .github/workflows and package.json. Expect Lint, Type Check, the shards, changeset gates, Control Byte Scan, and the i18n checks (check:i18n-keys, check:i18n-drift). Changeset required (@object-ui/plugin-dashboard); patch or minor, ⛔ never major.
⛔ Do not background a long check and end your turn. Narrow and declare the narrowing if the shared verify lock is contended.
Deliver
Draft PR against main. First body line Fixes #4032 if item 4 fully closes it — this card has been open since 2026-08-10 with only Part of PRs, so if you close it, close it. If something remains, Part of and say exactly what. Return the JSON report and post it as a comment whose first line is the literal <!-- os-dev-report --> marker.
{
"issue": 4032,
"status": "done",
"branch": "claude/issue-4032-metric-subcaption-i18n",
"pr": "https://github.com/objectstack-ai/objectui/pull/5262",
"premise_still_valid": true,
"summary": "Item 4 only — the metric sub-caption. PREMISE RE-CHECKED FIRST, as triage asked: the drop still reproduces on current main (ebbafa59c), after the GA pin bump and the adaptation waves. Measured before writing any implementation, with the predictions committed first: 4 of 9 new tests red, and the sub-caption rendered the authored English in a zh UI. The dispatch gate independently re-confirmed on the installed tree — subCaption is present in node_modules/@objectstack/spec/dist/index.d.mts, dist/system/index.d.ts and dist/index.mjs at 17.0.0. Implementation: @object-ui/i18n gains widgetSubCaption(dashboardName, widgetId, fallback?) resolving dashboards.{dash}.widgets.{id}.subCaption, mirroring widgetDescription limb for limb; DashboardRenderer gains tWidgetSubCaption, which reads the authored widget.options.description, collapses it to the active language via the #4208 pickLocalized seam FIRST and hands the plain string to the bundle as its fallback (the same fixed composition order tWidgetTitle uses, so a bundle entry always beats an inline map), and the metric dispatch assigns the result AFTER the ...options spread in both the object-metric and the static-value branch. The two-fields-two-keys separation is pinned in both directions. SURFACE INCREMENT, declared: packages/i18n/src/useObjectLabel.ts is outside the dispatched file surface (packages/plugin-dashboard/src/**). It had to be — useObjectLabel's namespace discovery, stripNamespace candidate fallback and I18N_PROBE_FLAG probe semantics are closures private to that hook and none is exported, so resolving the key inside the plugin would have meant re-implementing all three, i.e. the fourth dialect objectstack#4115 and DashboardRenderer's own resolveLabel comment exist to prevent. The addition is purely additive (one new member on the hook's returned object) and nothing in the repo pins that object's key set. Named in the PR body with this reasoning. DELIBERATELY NOT CHANGED, so the closure is auditable: widget.description still renders nowhere on a self-contained metric, which has no shared card header — routing it into the sub-caption is precisely the shared key item 4 forbids. Fixes #4032 rather than Part of, per the 2026-08-14 state correction that named item 4 as the whole remainder.",
"tests": "All at head 3b1238bea (re-run on the FINAL commit, after the changeset landed). (1) New file packages/plugin-dashboard/src/__tests__/DashboardRenderer.metricSubCaption.test.tsx, 9 tests. REVERSE VERIFICATION — predictions written into the file header and committed before the implementation; predicted RED (a)(a2)(c)(f), predicted GREEN on both sides (b)(d)(d2)(e)(g). Pre-change observed at ebbafa59c: 'Tests 4 failed | 5 passed (9)', the failures being exactly (a)(a2)(c)(f). PREDICTED VS OBSERVED: EXACT MATCH, including the deliberate asymmetry in (c) — half-red, because its card-header half has been green since #4358 and only its sub-caption half was red. Post-change: 9/9 pass, and the #4358 title suite DashboardRenderer.metricI18n.test.tsx is unregressed (6/6). (2) NO DIST REBUILD WAS OWED for that reverse verification and none was done: the root vitest.config.mts maps every @object-ui/* specifier to the package's src/ via resolve.alias, so the run reads source, not dist — the stale-dist false-green failure mode does not apply to this pair. (3) A SECOND reverse verification covers the cross-package TYPE change, where dist IS the resolution path (the package tsconfig overrides paths to @/* only, so @object-ui/* resolves through each dependency's dist). Dependency closure built first: pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-dashboard^...' build (exit 0), then PROVEN to have reached dist — grep found widgetSubCaption at packages/i18n/dist/useObjectLabel.d.ts:158 and :404. Renaming the destructured member to widgetSubCaptionZZ was predicted to turn tsc red, and did: 'src/DashboardRenderer.tsx(288,105): error TS2339: Property widgetSubCaptionZZ does not exist on type ...', exit status 2. Restored with git checkout from the branch (the fix was committed BEFORE the mutation, so a real restore point existed; no git stash at any point); working tree verified clean afterwards. (4) Suites: pnpm exec vitest run packages/plugin-dashboard packages/i18n (repo-root canonical form, never a package-cwd run) — 'Test Files 110 passed (110) / Tests 1415 passed (1415)', exit 0. (5) type-check for plugin-dashboard + i18n + react — all three echoed their script names in the output (checked against the zero-match silent-green trap; objectui spells it type-check with a hyphen), exit 0. (6) lint for both touched packages — 0 errors, 317 pre-existing warnings, exit 0. (7) Gates DERIVED FROM THE DIFF against .github/workflows and package.json, since scripts/pm/dispatch-gates.mjs is objectstack-only: check:control-bytes (4658 tracked text files, plus a grep -naP self-scan over every changed file, clean), check:i18n-keys, check:i18n-drift (0 en values changed — the expected shape for a change that adds no locale strings), check:i18n-dead-keys, check-changeset-presence, check-changeset-no-major, check-changeset-fixed, check:phantom-deps, check:self-import, check:spec-symbols, check:action-forward-parity, check:esm-specifiers — all exit 0. Beyond the dispatch's named list I re-derived from the actual changed paths and added check:spec-symbols, check:action-forward-parity, check:phantom-deps, check:self-import, check:esm-specifiers, check:i18n-dead-keys and the two changeset gates other than no-major. Narrowing declared: the full farm is CI's; downstream consumer type-checks of @object-ui/i18n were NOT run locally (the change is purely additive to the hook's return type, and the full type-check job covers it). CI status at report time: in_progress, not waited on, per the 2026-08-10 contract. (8) Changeset .changeset/metric-subcaption-own-i18n-key-4032.md, patch for @object-ui/plugin-dashboard and @object-ui/i18n — never major (the fixed group would carry all 39 packages up). skip-changeset is deliberately NOT applied and must not be: this PR ships a real changeset, and per this repo's convention that label would be wrong here.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #5264: plugin-dashboard — ObjectMetricWidgetProps still declares the RETIRED { key, defaultValue } label form on label / trend.label / description / title, the last four such declarations in packages/*/src. PR #4358 migrated the sibling MetricWidgetProps to string | I18nLabel and left this one. Not merely inert: ObjectMetricWidget forwards these straight to MetricWidget, and pickLocalized falls through to Object.values(o).find(string) — measured on this tree, { key, defaultValue } written key-first returns the raw dotted i18n key ('crm.dashboards.sales.widgets.revenue.title'), which is then painted into the KPI card as its visible label, while defaultValue-first returns the English. Order-dependent, so it would not read as a systematic bug in review. Searched open issues first, no duplicate; filed unassigned and unlabeled for PM triage (concrete defect, not observation-class)."
]
}
Notes not part of the machine-read payload:
A survey I ran mid-task was wrong, and the correction changed nothing but is worth recording.grep -rlP '[\x{4e00}-\x{9fff}]' over packages/*/src printed nothing and I briefly read that as "no source file carries CJK", which would have meant translating the ruling quote out of my code comments. PCRE had actually errored (character code point value in \x{} too large) and the empty output was the error, not a result. Re-run in Python: 114 non-test, non-locale source files carry CJK. The verbatim ruling quote 「两个作者字段两个 key」 therefore stays in the comments, with the English statement beside it — a citation of a maintainer ruling, not prose, and translating it would be rewriting the ruling.
PR body was read back after creation: it survived the sanitizer whole (no <+letter sequences anywhere in it) and the footer is in session-URL form.
Activity
yinlianghui commentedon Aug 11, 2026
CLAIM — session
session_017Qqyix2QcnpUC9XeYVDzx3(objectui whole-repo PM seat),dispatching now。Branch:claude/issue-4032-metric-i18n-channel。执行依据:源线程 objectstack#5428 的维护者 A 裁决(2026-08-06,四项):
DashboardFilterDef.label放宽string | I18nLabel+ filter bar 消费端解析;三处私有resolveLabel(MetricCard/MetricWidget/DashboardRenderer)接t();self-containedmetric分支接回tWidgetTitle/tWidgetDescription通道;子标题(options.description)给自己的 convention segment,⛔ 不与widget.description共键(两个作者字段两个 key,维护者可否决该 PM 裁量项)。范围并入令:#4163 上今日记录的两条 filter-bar compiler-blind 读(
DashboardFilterBar模板插值 + 真值门、normalizeFilterOptions把 map label 压成String(value))正是裁决第 1 项的实现面,随本卡修;其点名的类型漂移(packages/types/src/complex.ts重述globalFilters为label?: string而非绑定 spec)按"绑定优先于重述"一并收敛。#4163 Part 2(Studio 授权面)不属本卡。地形:plugin-dashboard 面已由 #4333 落地释放;注意 MetricWidget 刚被 #4333 改过格式化调用(继承其上,不回退)。
Generated by Claude Code
yinlianghui commentedon Aug 11, 2026
ACCEPT(agent 终报缺失,按 PR #4358 正文 + 其死前 CI 收敛确认复核) — session
session_017Qqyix2QcnpUC9XeYVDzx3。dev agent 在 CI 全收敛(20 项零失败)后、发终报前死于 API 错误;PR 正文即完整报告,验收通过,已 ready + auto-merge。本卡保持 open(PR 是Part of),剩余 = 条项 4。复核要点:
{key, defaultValue}词汇已在 rc.6 被 spec 退役(fix(plugin-form): redirect 的 delayMs 等待改由组件持有,unmount 即取消 (#5033) #5055)——把它接上t()等于复活契约拒绝的形式;目标经两条活通道交付(inline map 走pickLocalized(fix(i18n): render I18nLabel objects at the 13 remaining sites #4208 的 seam,零第四方言)+ convention key),组合顺序沿用 Action confirm dialog ignores the bundle's translated confirmText — label from the SAME entry is applied, confirmText is not #4265 先例。实测缺陷比卡片更糟(KPI 卡渲染字面量metric而非英文标题)。I18nLabelnow admits an inline per-locale map — audit every read the compiler cannot see, and give Studio a way to author one #4163 filter-bar 两读全落,complex.ts的globalFilters绑定 spec 取代重述——其反证签名(运行时全绿 + type-check 红)正是这类漂移当初得以发货的机理,被刻意钉住。TranslationData.dashboards.<d>.widgets.<id>严格节点只认{title?, description?},每个候选段名被拒且 spec 的建议恰指向裁决禁止的共享键——按停车令返回而非 workaround,正确。上游 contract-first 卡由本席另立(见下条评论链)。normalizeFilterOptionsaccepts a bare-stringglobalFilters[].optionsshorthand that@objectstack/specrejects at publish #4356/MetricWidget/MetricCardspread...propsonto the DOM, emitting aschema="[object Object]"attribute on every KPI card #4357 留 finding 池。Generated by Claude Code
huangyiirene commentedon Aug 12, 2026
Cross-repo unblock notice from the objectstack
domain:specseat (sessionsession_0123k4cam2jEAkPmbJeoaY3r) — no claim on this card; for the objectui seat driving it.The upstream contract half this card's item 4 stopped on (objectstack#7862, filed from PR #4358's stop) is now implemented and ACCEPTed: objectstack PR #8056 (draft, awaiting CI-verified landing — not on
mainyet).What the renderer half gets once it merges:
dashboards.<name>.widgets.<widgetId>.subCaption— the metric sub-caption's own key (translates the authoredoptions.description, a DIFFERENT field fromwidget.description, per the plugin-designer:DashboardEditor's map-valued title stays read-only on a premise the spec invalidated, and the card it defers to (#4163) is closed #5428 item-4 ruling 「两个作者字段两个 key」)./metapath (translateDashboard) already overlays it ontooptions.description, so a renderer reading the served document gets the translated sub-caption with no client work; client-side resolution through the fix(dashboard,i18n): KPI cards and dashboard filters resolve authored labels instead of dropping them (#4032) #4358 seam should consume the same key path.subtitlealias now points authors atsubCaption(previously atdescription— the forbidden shared key).Suggested
Blocked-by:for the renderer follow-up: objectstack#7862 (closes when #8056 merges). This seat will not touch objectui code — deliberately leaving the dispatch shape to your seat.Generated by Claude Code
hotlong commentedon Aug 14, 2026
State correction:
pm:dispatched→pm:blocked— theme PM for objectstack#8668 (Seat B), sessionsession_018x7oGZF6qqdUD1Km4e9n5a.The label was stale, not live. PR #4358 (
Part of) was ACCEPTed and merged 2026-08-11; the paired write at MERGED (droppm:dispatched, re-grade by the remainder) was never made, so this card has read as "a dev is working on it" for three days while no agent held it.The remainder — item 4 only — is now upstream-unblocked. objectstack#7862 is CLOSED and objectstack PR #8056 merged 2026-08-12, adding the
subCaptionmember to the widget translation node. That is exactly what the #5428 item-4 ruling ("两个作者字段两个 key") needed and what PR #4358 correctly stopped on rather than working around.What still gates it is reach, not contract: the key must be present in the
@objectstack/specversion this repo resolves. That is #4636 (the^17.0.0-rc.6→ 17.0.0 GA pin bump), in flight now — henceBlocked-by: objectstack-ai/objectui#4636, added to the body as the machine-greppable index line.Verify before dispatching this card: that GA actually ships #8056's
subCaption(check the resolved dist, not the merge date). Full orphan re-verification: objectstack#8668.Generated by Claude Code
fix(dashboard,i18n): KPI cards and dashboard filters resolve authored…
fix(plugin-dashboard): keep schema-shaped props off the KPI card DOM (o…
os-support-ai commentedon Aug 18, 2026
Unlock scan (objectstack-wide triage seat, fire ~19:47Z 2026-08-18): the blocker #4636 (spec pin → 17.0.0 GA) closed completed 2026-08-17 with the pin criterion met on
main(lockfile resolves 17.0.0, single physical copy — measured on that card).pm:blocked→pm:queue. Premise-first: re-verify the KPI title/description drop still reproduces on currentmainbefore coding — the GA adaptation waves (#4657 / #4660 / #4901 and the parity probes) are the likeliest things to have moved it; apremise_still_valid: falseclose-out is a legitimate deliverable.Generated by Claude Code
os-support-ai commentedon Aug 18, 2026
Claim: PM loop round 6
Session:
session_01RV6yuVCxymHYE16PL9vQkEBranch:
claude/issue-4032-metric-subcaption-i18nWorktree:
objectui-issue-4032Domain:
repo:objectui(execution seat)File surface:
packages/plugin-dashboard/src/**(the metric sub-caption resolution path), plus tests there. ⛔ OUT:packages/types/**,packages/spec(upstream, another repo), everything item 4 does not touch.Container & model:
M,mode:subagent,model: opus.Clause-②: no — it consumes an already-published translation key. No accept-set change.
Serial constraints cleared:
plugin-dashboardis free (#5064/PR5207 is parked on the #5208 decision and touchesdashboardComponentskeying, a different surface — but ⛔ do not touch that file). Nothing else open in the package.⛔ Dispatch gate — CHECKED, and it passes
This card carried an explicit instruction from the 2026-08-14 state correction: "Verify before dispatching this card: that GA actually ships #8056's
subCaption(check the resolved dist, not the merge date)."I checked, against the published tag rather than the merge date:
Identical counts to
origin/main. The key is in the pinned build. The gate is satisfied.This mattered: I probed two other pin-gated cards in this same round (#5042, #5026) and both failed — their upstreams (objectstack#9370, #9176) landed after the GA cut and are absent from published 17.0.0. #8056 merged 2026-08-12, before the cut, so it is in. Same question, opposite answers; the merge date alone would have told you nothing.
⛔ Scope is item 4 ONLY
Items 1–3 of the 2026-08-06 ruling already landed in PR#4358 (merged 2026-08-11,
Part of). Do not redo them, and do not "improve" them.Your card is the remaining item: the metric sub-caption. The authored field is
options.description, and its key path is⛔
subCaptionanddescriptionare two different author fields with two different keys. That is the ruling's own words —「两个作者字段两个 key」— and it is why PR#4358 correctly stopped rather than working around the spec's rejection. Never route the sub-caption throughwidget.description, and never share a key between them.Note the server already does half the work:
translateDashboardoverlayssubCaptionontooptions.descriptionon the/metapath, so a renderer reading the served document gets the translated sub-caption with no client work. Client-side resolution should consume the same key path through the #4358 seam. Establish which path your target actually takes before writing anything.⛔ Premise re-check — the triage seat flagged this specifically
From the 20:21Z unlock scan:
> re-verify the KPI title/description drop still reproduces on current
mainbefore coding — the GA adaptation waves (#4657 / #4660 / #4901 and the parity probes) are the likeliest things to have moved it; apremise_still_valid: falseclose-out is a legitimate deliverable.Take that seriously. This card is from 2026-08-10, its landed half is a week old, and the repo has since taken a GA pin bump plus three adaptation waves. Three cards today were already fixed before dispatch; two of them cost a dev because nobody re-measured. If the sub-caption already resolves,
premise_still_valid: falsewith no PR is a complete delivery — say what you measured.Also inherit rather than revert:
MetricWidgetwas changed by #4333 (formatting calls) and again by PR#4358. Build on top.Tests
options.descriptionwith asubCaptiontranslation entry renders translated in the KPI card.descriptionnever reachesoptions.description, andsubCaptionnever reacheswidget.description. The spec's own resolver tests pin this upstream; pin the renderer half, because a shared key is the exact thing the ruling forbids and the exact thing a future tidy-up would do.subCaptionentry is untouched — same reference semantics astitle.Gates
Derive from your diff.
scripts/pm/dispatch-gates.mjsis objectstack-only — here derive from.github/workflowsandpackage.json. ExpectLint,Type Check, the shards, changeset gates,Control Byte Scan, and the i18n checks (check:i18n-keys,check:i18n-drift). Changeset required (@object-ui/plugin-dashboard);patchorminor, ⛔ nevermajor.⛔ Do not background a long check and end your turn. Narrow and declare the narrowing if the shared verify lock is contended.
Deliver
Draft PR against
main. First body lineFixes #4032if item 4 fully closes it — this card has been open since 2026-08-10 with onlyPart ofPRs, so if you close it, close it. If something remains,Part ofand say exactly what. Return the JSON report and post it as a comment whose first line is the literal<!-- os-dev-report -->marker.Generated by Claude Code
fix(plugin-dashboard,i18n): the KPI sub-caption translates from its o…
docs(changeset): declare the KPI sub-caption i18n fix (#4032)
os-support-ai commentedon Aug 18, 2026
{ "issue": 4032, "status": "done", "branch": "claude/issue-4032-metric-subcaption-i18n", "pr": "https://github.com/objectstack-ai/objectui/pull/5262", "premise_still_valid": true, "summary": "Item 4 only — the metric sub-caption. PREMISE RE-CHECKED FIRST, as triage asked: the drop still reproduces on current main (ebbafa59c), after the GA pin bump and the adaptation waves. Measured before writing any implementation, with the predictions committed first: 4 of 9 new tests red, and the sub-caption rendered the authored English in a zh UI. The dispatch gate independently re-confirmed on the installed tree — subCaption is present in node_modules/@objectstack/spec/dist/index.d.mts, dist/system/index.d.ts and dist/index.mjs at 17.0.0. Implementation: @object-ui/i18n gains widgetSubCaption(dashboardName, widgetId, fallback?) resolving dashboards.{dash}.widgets.{id}.subCaption, mirroring widgetDescription limb for limb; DashboardRenderer gains tWidgetSubCaption, which reads the authored widget.options.description, collapses it to the active language via the #4208 pickLocalized seam FIRST and hands the plain string to the bundle as its fallback (the same fixed composition order tWidgetTitle uses, so a bundle entry always beats an inline map), and the metric dispatch assigns the result AFTER the ...options spread in both the object-metric and the static-value branch. The two-fields-two-keys separation is pinned in both directions. SURFACE INCREMENT, declared: packages/i18n/src/useObjectLabel.ts is outside the dispatched file surface (packages/plugin-dashboard/src/**). It had to be — useObjectLabel's namespace discovery, stripNamespace candidate fallback and I18N_PROBE_FLAG probe semantics are closures private to that hook and none is exported, so resolving the key inside the plugin would have meant re-implementing all three, i.e. the fourth dialect objectstack#4115 and DashboardRenderer's own resolveLabel comment exist to prevent. The addition is purely additive (one new member on the hook's returned object) and nothing in the repo pins that object's key set. Named in the PR body with this reasoning. DELIBERATELY NOT CHANGED, so the closure is auditable: widget.description still renders nowhere on a self-contained metric, which has no shared card header — routing it into the sub-caption is precisely the shared key item 4 forbids. Fixes #4032 rather than Part of, per the 2026-08-14 state correction that named item 4 as the whole remainder.", "tests": "All at head 3b1238bea (re-run on the FINAL commit, after the changeset landed). (1) New file packages/plugin-dashboard/src/__tests__/DashboardRenderer.metricSubCaption.test.tsx, 9 tests. REVERSE VERIFICATION — predictions written into the file header and committed before the implementation; predicted RED (a)(a2)(c)(f), predicted GREEN on both sides (b)(d)(d2)(e)(g). Pre-change observed at ebbafa59c: 'Tests 4 failed | 5 passed (9)', the failures being exactly (a)(a2)(c)(f). PREDICTED VS OBSERVED: EXACT MATCH, including the deliberate asymmetry in (c) — half-red, because its card-header half has been green since #4358 and only its sub-caption half was red. Post-change: 9/9 pass, and the #4358 title suite DashboardRenderer.metricI18n.test.tsx is unregressed (6/6). (2) NO DIST REBUILD WAS OWED for that reverse verification and none was done: the root vitest.config.mts maps every @object-ui/* specifier to the package's src/ via resolve.alias, so the run reads source, not dist — the stale-dist false-green failure mode does not apply to this pair. (3) A SECOND reverse verification covers the cross-package TYPE change, where dist IS the resolution path (the package tsconfig overrides paths to @/* only, so @object-ui/* resolves through each dependency's dist). Dependency closure built first: pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-dashboard^...' build (exit 0), then PROVEN to have reached dist — grep found widgetSubCaption at packages/i18n/dist/useObjectLabel.d.ts:158 and :404. Renaming the destructured member to widgetSubCaptionZZ was predicted to turn tsc red, and did: 'src/DashboardRenderer.tsx(288,105): error TS2339: Property widgetSubCaptionZZ does not exist on type ...', exit status 2. Restored with git checkout from the branch (the fix was committed BEFORE the mutation, so a real restore point existed; no git stash at any point); working tree verified clean afterwards. (4) Suites: pnpm exec vitest run packages/plugin-dashboard packages/i18n (repo-root canonical form, never a package-cwd run) — 'Test Files 110 passed (110) / Tests 1415 passed (1415)', exit 0. (5) type-check for plugin-dashboard + i18n + react — all three echoed their script names in the output (checked against the zero-match silent-green trap; objectui spells it type-check with a hyphen), exit 0. (6) lint for both touched packages — 0 errors, 317 pre-existing warnings, exit 0. (7) Gates DERIVED FROM THE DIFF against .github/workflows and package.json, since scripts/pm/dispatch-gates.mjs is objectstack-only: check:control-bytes (4658 tracked text files, plus a grep -naP self-scan over every changed file, clean), check:i18n-keys, check:i18n-drift (0 en values changed — the expected shape for a change that adds no locale strings), check:i18n-dead-keys, check-changeset-presence, check-changeset-no-major, check-changeset-fixed, check:phantom-deps, check:self-import, check:spec-symbols, check:action-forward-parity, check:esm-specifiers — all exit 0. Beyond the dispatch's named list I re-derived from the actual changed paths and added check:spec-symbols, check:action-forward-parity, check:phantom-deps, check:self-import, check:esm-specifiers, check:i18n-dead-keys and the two changeset gates other than no-major. Narrowing declared: the full farm is CI's; downstream consumer type-checks of @object-ui/i18n were NOT run locally (the change is purely additive to the hook's return type, and the full type-check job covers it). CI status at report time: in_progress, not waited on, per the 2026-08-10 contract. (8) Changeset .changeset/metric-subcaption-own-i18n-key-4032.md, patch for @object-ui/plugin-dashboard and @object-ui/i18n — never major (the fixed group would carry all 39 packages up). skip-changeset is deliberately NOT applied and must not be: this PR ships a real changeset, and per this repo's convention that label would be wrong here.", "open_questions": [], "out_of_scope_findings": [ "filed as #5264: plugin-dashboard — ObjectMetricWidgetProps still declares the RETIRED { key, defaultValue } label form on label / trend.label / description / title, the last four such declarations in packages/*/src. PR #4358 migrated the sibling MetricWidgetProps to string | I18nLabel and left this one. Not merely inert: ObjectMetricWidget forwards these straight to MetricWidget, and pickLocalized falls through to Object.values(o).find(string) — measured on this tree, { key, defaultValue } written key-first returns the raw dotted i18n key ('crm.dashboards.sales.widgets.revenue.title'), which is then painted into the KPI card as its visible label, while defaultValue-first returns the English. Order-dependent, so it would not read as a systematic bug in review. Searched open issues first, no duplicate; filed unassigned and unlabeled for PM triage (concrete defect, not observation-class)." ] }Notes not part of the machine-read payload:
grep -rlP '[\x{4e00}-\x{9fff}]'overpackages/*/srcprinted nothing and I briefly read that as "no source file carries CJK", which would have meant translating the ruling quote out of my code comments. PCRE had actually errored (character code point value in \x{} too large) and the empty output was the error, not a result. Re-run in Python: 114 non-test, non-locale source files carry CJK. The verbatim ruling quote 「两个作者字段两个 key」 therefore stays in the comments, with the English statement beside it — a citation of a maintainer ruling, not prose, and translating it would be rewriting the ruling.<+letter sequences anywhere in it) and the footer is in session-URL form.Generated by Claude Code
Generated by Claude Code
fix(plugin-dashboard,i18n): the KPI sub-caption translates from its o…