Skip to content

Commit f157423

Browse files
hotlongclaude
andauthored
fix(studio,chatbot,i18n): workbench labels over internal ids, localized property panel, one tool-card status (#7366)
* fix(studio,chatbot,i18n): workbench labels over internal ids, localized property panel and tool cards The Studio workbench showed a Chinese author internal identifiers and whole blocks of English, and the AI tool card's header disagreed with its own body. - Interfaces breadcrumb / canvas caption / nav rail: metadata label plus a translated kind; the internal `type · name` pair moves to the tooltip. An unlabelled nav leaf falls back to its object name instead of an empty row. - `parsePackages` reads the package name from BOTH positions the packages endpoint serves it in (the server's own list handler already reads `item.manifest?.id || item.id` on both halves) — a registry-shaped entry no longer degrades to showing its reverse-domain id as its name. - Dashboard property panel: the spec's English authoring form is overlaid through the platform's own `metadataForms.<type>` convention, using `resolveMetadataFormLabels` from `@objectstack/spec/system` rather than a second key scheme — which also gives the `header` composite's sub-rows. - Tool cards: titles resolve through `chatbot.tool.<name>` (the closed `PLATFORM_TOOLS_BY_PACKAGE` registry, ten packs), the header status badge is localized through an additive `statusLabels` seam on the vendored element, and the plan count strip is a real plural family. - `resolveProposalCardState` is now the one producer of "how far has this proposal got"; the header badge reads it instead of deriving from the tool's own output alone, so a confirmed / built / published card stops saying "Awaiting Approval". Fixes #7254 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(studio): name the internal-id tooltip instead of repeating the bare pair Moving `type · name` off the breadcrumb and the canvas caption put the SAME tooltip string on three elements (the rail item already had it), which made every `findByTitle` pin in the Interfaces suite ambiguous — and left a tooltip that shows a cryptic pair without saying what it is. Prefix both with a localized "Internal id" label, and update the one pin that asserted the caption's old raw text. Part of #7254 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(metadata-admin): type the form shapes the overlay pins read `any[]` in the new pins was the one lint finding this PR actually introduced (everything else the targeted run reports is pre-existing on the touched files). Two small interfaces cover exactly what the assertions read. Part of #7254 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 7dedec6 commit f157423

25 files changed

Lines changed: 1812 additions & 54 deletions
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@object-ui/plugin-chatbot': minor
3+
'@object-ui/app-shell': minor
4+
'@object-ui/i18n': minor
5+
---
6+
7+
Studio workbench and AI tool cards speak the author's language (objectui#7254)
8+
9+
- The Interfaces breadcrumb, canvas caption and navigation rail show the
10+
metadata label plus a translated kind; the internal `type · name` pair moves
11+
to the tooltip. An unlabelled nav leaf now falls back to its object name
12+
instead of rendering an empty row.
13+
- The Studio top-bar package switcher reads the package's human name from
14+
either position the packages endpoint serves it in, instead of degrading a
15+
registry-shaped entry to its reverse-domain id.
16+
- The dashboard property panel is localized: the spec's authoring form is
17+
overlaid through the platform's own `metadataForms.<type>` convention, so
18+
section headings, field labels, hints and the `header` composite's sub-fields
19+
render in Chinese (developer vocabulary such as "Tailwind units" is replaced
20+
with something an author can act on, not transliterated).
21+
- AI tool cards: tool titles resolve through `chatbot.tool.<name>` (all thirty
22+
platform-provided tools, ten locale packs), the header status badge is
23+
localized, and the plan count strip is a real plural family instead of an
24+
English `+ "s"` concatenation.
25+
- The tool card's header badge and its body badge now come from one producer:
26+
a proposal that has been confirmed, built or published no longer keeps a
27+
header reading "Awaiting Approval".

‎packages/app-shell/src/views/metadata-admin/dashboard-schema.ts‎

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import { z } from 'zod';
2525
import { DashboardSchema, dashboardForm as specDashboardForm } from '@objectstack/spec/ui';
2626
import type { FormViewSpec } from './SchemaForm.js';
27+
import type { SupportedLocale } from './i18n.js';
28+
import { localizeMetadataForm } from './metadata-form-i18n.js';
2729

2830
type JsonSchema = Record<string, any>;
2931

@@ -54,16 +56,28 @@ export function getDashboardSchema(): JsonSchema | undefined {
5456
return _dashboardDocSchema;
5557
}
5658

57-
let _dashboardForm: FormViewSpec | undefined;
59+
/**
60+
* Per-locale, because the form now carries LOCALIZED copy (objectui#7254) and
61+
* a single slot would have served whichever locale asked first to everyone
62+
* after it — a cache that is right until someone switches language.
63+
*/
64+
const _dashboardForm = new Map<string, FormViewSpec | undefined>();
5865

5966
/**
6067
* The canonical authoring FormView, with the fields the curated inspector
6168
* owns directly (widgets / label / description / name) pruned from every
6269
* section so they are not double-rendered. Everything else — layout,
6370
* filters, performance — flows through verbatim from the spec.
71+
*
72+
* The spec authors these strings in English. `localizeMetadataForm` overlays
73+
* the active locale on top (objectui#7254) through the platform's own
74+
* `metadataForms.<type>` convention, so the panel speaks the same language as
75+
* the Studio around it. A locale the overlay does not carry gets the spec's
76+
* English, exactly as before.
6477
*/
65-
export function getDashboardForm(): FormViewSpec | undefined {
66-
if (_dashboardForm) return _dashboardForm;
78+
export function getDashboardForm(locale?: SupportedLocale | string): FormViewSpec | undefined {
79+
const cacheKey = String(locale ?? '');
80+
if (_dashboardForm.has(cacheKey)) return _dashboardForm.get(cacheKey);
6781
if (!specDashboardForm || typeof specDashboardForm !== 'object') return undefined;
6882
try {
6983
const clone = JSON.parse(JSON.stringify(specDashboardForm)) as FormViewSpec;
@@ -77,12 +91,19 @@ export function getDashboardForm(): FormViewSpec | undefined {
7791
clone.sections = (clone.sections ?? []).filter(
7892
(s: any) => (s.fields ?? []).length > 0,
7993
);
80-
_dashboardForm = clone;
94+
// Localize AFTER pruning: the overlay only has to reach what renders, and
95+
// pruning cannot drop a section the overlay just renamed.
96+
const localized = localizeMetadataForm(
97+
clone as unknown as Record<string, unknown>,
98+
'dashboard',
99+
locale,
100+
) as unknown as FormViewSpec;
101+
_dashboardForm.set(cacheKey, localized);
102+
return localized;
81103
} catch (err) {
82104
if (typeof console !== 'undefined') {
83105
console.warn('[dashboard-schema] failed to prepare dashboardForm from spec', err);
84106
}
85107
return undefined;
86108
}
87-
return _dashboardForm;
88109
}

‎packages/app-shell/src/views/metadata-admin/i18n.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1737,6 +1737,13 @@ const ENGINE_STRINGS_EN: Record<string, string> = {
17371737
'engine.studio.nav.noObjects': 'This package has no objects yet — create one in the Data pillar first.',
17381738
// Interfaces pillar
17391739
'engine.studio.if.pickLeft': 'Select a menu item on the left',
1740+
// objectui#7254 — the tooltip that keeps the DEVELOPER identity reachable
1741+
// after the breadcrumb/caption stopped printing it beside a localized
1742+
// label. Prefixed rather than bare: `dashboard · customer_dashboard` on
1743+
// its own does not say what it is, and it is also what the rail item's
1744+
// own tooltip carries — three elements sharing one tooltip string is a
1745+
// page nothing (a person or a test) can address unambiguously.
1746+
'engine.studio.if.internalId': 'Internal id',
17401747
'engine.studio.if.navHeading': '{app} · Navigation',
17411748
'engine.studio.if.editNavTitle': 'Edit navigation (drag to reorder / rename / add-remove)',
17421749
'engine.studio.if.doneEditTitle': 'Done editing',
@@ -3623,6 +3630,7 @@ const ENGINE_STRINGS_ZH: Record<string, string> = {
36233630
'engine.studio.nav.noObjects': '这个软件包还没有对象 — 先到 Data 支柱创建。',
36243631
// Interfaces pillar
36253632
'engine.studio.if.pickLeft': '从左侧选择一个菜单项',
3633+
'engine.studio.if.internalId': '内部标识',
36263634
'engine.studio.if.navHeading': '{app} · 导航',
36273635
'engine.studio.if.editNavTitle': '编辑导航(拖拽排序 / 重命名 / 增删)',
36283636
'engine.studio.if.doneEditTitle': '完成编辑',

‎packages/app-shell/src/views/metadata-admin/inspectors/DashboardDefaultInspector.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,9 @@ export function DashboardDefaultInspector({
130130
() =>
131131
mergeServerFields({
132132
bundledSchema: getDashboardSchema(),
133-
bundledForm: getDashboardForm(),
133+
// objectui#7254 — the spec form is English; the locale overlay lives
134+
// inside `getDashboardForm` so every consumer of it gets the same copy.
135+
bundledForm: getDashboardForm(locale),
134136
serverSchema,
135137
excludeFields: DASHBOARD_CURATED_FIELDS,
136138
sectionTitle: t('engine.inspector.moreFields', locale),
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* objectui#7254 — the dashboard property panel was an all-English island
5+
* inside a Chinese Studio.
6+
*
7+
* The panel is spec-driven on purpose (`dashboardForm` from
8+
* `@objectstack/spec/ui` fed straight into SchemaForm), and the spec authors
9+
* its copy in English, so every section heading, field label and hint reached
10+
* the author untranslated — including "Grid gap (Tailwind units)", a unit only
11+
* a developer can act on.
12+
*
13+
* These pins assert the OVERLAY, and specifically the two things a hand-rolled
14+
* walker would have got wrong and a shape-only test would not have noticed:
15+
* the section key is the SLUGGED label (not the label text), and a `composite`
16+
* field's sub-rows are synthesized from the bundle — completely, or the ones
17+
* left out vanish from the form.
18+
*/
19+
import { describe, expect, it } from 'vitest';
20+
import { dashboardForm } from '@objectstack/spec/ui';
21+
import { localizeMetadataForm, hasMetadataFormOverlay } from './metadata-form-i18n';
22+
import { getDashboardForm } from './dashboard-schema';
23+
24+
/** The two form shapes these assertions read. `unknown` everywhere else. */
25+
interface FormField {
26+
field?: string;
27+
type?: string;
28+
label?: string;
29+
helpText?: string;
30+
fields?: FormField[];
31+
}
32+
interface Section {
33+
label?: string;
34+
description?: string;
35+
fields?: FormField[];
36+
}
37+
38+
function sections(form: unknown): Section[] {
39+
return ((form as { sections?: Section[] } | undefined)?.sections ?? []) as Section[];
40+
}
41+
42+
function fieldByName(form: unknown, name: string): FormField | undefined {
43+
for (const s of sections(form)) {
44+
for (const f of s.fields ?? []) {
45+
if (f?.field === name) return f;
46+
}
47+
}
48+
return undefined;
49+
}
50+
51+
describe('localizeMetadataForm — the spec form, in the author’s language', () => {
52+
it('has an overlay for `dashboard` at zh and none at en', () => {
53+
expect(hasMetadataFormOverlay('dashboard', 'zh-CN')).toBe(true);
54+
expect(hasMetadataFormOverlay('dashboard', 'en-US')).toBe(false);
55+
});
56+
57+
it('leaves the form untouched (same object) outside zh', () => {
58+
const form = dashboardForm as unknown as Record<string, unknown>;
59+
expect(localizeMetadataForm(form, 'dashboard', 'en-US')).toBe(form);
60+
});
61+
62+
it('leaves a type the overlay does not carry untouched', () => {
63+
const form = dashboardForm as unknown as Record<string, unknown>;
64+
expect(localizeMetadataForm(form, 'report', 'zh-CN')).toBe(form);
65+
});
66+
67+
it('translates section headings — addressed by the SLUGGED label, not the label text', () => {
68+
const zh = localizeMetadataForm(
69+
dashboardForm as unknown as Record<string, unknown>,
70+
'dashboard',
71+
'zh-CN',
72+
);
73+
const labels = sections(zh).map((s) => s.label);
74+
expect(labels).toContain('布局'); // 'Layout' → section key `layout`
75+
expect(labels).toContain('筛选'); // 'Filters' → `filters`
76+
expect(labels).not.toContain('Layout');
77+
const layout = sections(zh).find((s) => s.label === '布局');
78+
expect(layout?.description).toBe('栅格尺寸与刷新频率。');
79+
expect(layout?.description).not.toMatch(/refresh cadence/);
80+
});
81+
82+
it('translates the layout field labels and hints the card named', () => {
83+
const zh = localizeMetadataForm(
84+
dashboardForm as unknown as Record<string, unknown>,
85+
'dashboard',
86+
'zh-CN',
87+
);
88+
expect(fieldByName(zh, 'columns')).toMatchObject({ label: '列数', helpText: '栅格列数(默认 12)' });
89+
expect(fieldByName(zh, 'refreshInterval')?.label).toBe('自动刷新');
90+
expect(fieldByName(zh, 'header')?.label).toBe('页眉');
91+
});
92+
93+
it('drops the "Tailwind units" developer vocabulary rather than transliterating it', () => {
94+
const zh = localizeMetadataForm(
95+
dashboardForm as unknown as Record<string, unknown>,
96+
'dashboard',
97+
'zh-CN',
98+
);
99+
const gap = fieldByName(zh, 'gap');
100+
expect(gap?.label).toBe('间距');
101+
expect(gap?.helpText).not.toMatch(/Tailwind/i);
102+
// The English source still says it — that copy is the spec's to fix, and
103+
// this overlay deliberately does not rewrite the producer's own text.
104+
expect(fieldByName(dashboardForm, 'gap')?.helpText).toMatch(/Tailwind/i);
105+
});
106+
107+
it('synthesizes ALL of the `header` composite’s sub-rows — a partial list would hide the rest', () => {
108+
const zh = localizeMetadataForm(
109+
dashboardForm as unknown as Record<string, unknown>,
110+
'dashboard',
111+
'zh-CN',
112+
);
113+
const header = fieldByName(zh, 'header');
114+
expect(header?.type).toBe('composite');
115+
const children = (header?.fields ?? []).map((f) => f.field);
116+
// Exactly `DashboardHeaderSchema`'s three properties. SchemaForm prefers a
117+
// declared `fields` array over the schema-derived one, so anything absent
118+
// here would disappear from the panel.
119+
expect(children.sort()).toEqual(['actions', 'showDescription', 'showTitle']);
120+
const byName = new Map((header!.fields ?? []).map((f) => [f.field, f]));
121+
expect(byName.get('showTitle')?.label).toBe('显示标题');
122+
expect(byName.get('showTitle')?.helpText).toBe('在页眉中显示仪表板名称');
123+
expect(byName.get('showDescription')?.label).toBe('显示描述');
124+
expect(byName.get('actions')?.label).toBe('操作按钮');
125+
});
126+
});
127+
128+
describe('getDashboardForm — the overlay reaches the panel, and caches per locale', () => {
129+
it('serves the spec’s English by default and Chinese to a zh console', () => {
130+
const en = getDashboardForm('en-US');
131+
const zh = getDashboardForm('zh-CN');
132+
expect(sections(en).map((s) => s.label)).toContain('Layout');
133+
expect(sections(zh).map((s) => s.label)).toContain('布局');
134+
});
135+
136+
it('does not serve one locale’s copy to another (the single-slot cache bug)', () => {
137+
// Order matters: ask for zh first, then en. A single memo slot would hand
138+
// the Chinese form to the English console.
139+
expect(sections(getDashboardForm('zh-CN')).map((s) => s.label)).toContain('布局');
140+
expect(sections(getDashboardForm('en-US')).map((s) => s.label)).toContain('Layout');
141+
expect(sections(getDashboardForm('en-US')).map((s) => s.label)).not.toContain('布局');
142+
});
143+
144+
it('is stable per locale', () => {
145+
expect(getDashboardForm('zh-CN')).toBe(getDashboardForm('zh-CN'));
146+
});
147+
148+
it('still prunes the fields the curated inspector owns', () => {
149+
const zh = getDashboardForm('zh-CN');
150+
for (const owned of ['name', 'label', 'description', 'widgets']) {
151+
expect(fieldByName(zh, owned)).toBeUndefined();
152+
}
153+
});
154+
});

0 commit comments

Comments
 (0)