Skip to content

fix(studio,chatbot,i18n): workbench labels over internal ids, localized property panel, one tool-card status - #7366

Merged
hotlong merged 3 commits into
mainfrom
claude/issue-7254-studio-jargon-and-i18n
Sep 2, 2026
Merged

fix(studio,chatbot,i18n): workbench labels over internal ids, localized property panel, one tool-card status#7366
hotlong merged 3 commits into
mainfrom
claude/issue-7254-studio-jargon-and-i18n

Conversation

@hotlong

@hotlong hotlong commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #7254

A Chinese customer opening the Studio workbench met three separate failures of the
same kind: internal identifiers printed where a name belongs, whole English blocks
inside an otherwise Chinese screen, and a tool card whose header contradicted its
own body.

1. Workbench labels, internal names on the tooltip

  • Breadcrumb and canvas caption printed dashboard · customer_dashboard beside a
    Chinese label — the same strip speaking two vocabularies. They now read the metadata
    label plus the translated kind (客户仪表盘 · 仪表板), with the internal pair
    on the tooltip, prefixed so it says what it is (内部标识: dashboard · …) rather than
    repeating a bare cryptic pair. The prefix is also what keeps three elements from
    sharing one addressable tooltip string.
  • Nav rail: the kind chip was the raw English metadata type in an otherwise
    localized rail, and a leaf the author never labelled rendered an empty row. It now
    falls back to the object name — a poor label, but an honest one.
  • Top-bar package switcher: parsePackages read the package's id from BOTH
    positions the packages endpoint serves it in, and its name from only one. So every
    registry-shaped row (protocol.getMetaItems({type:'package'}) hands back the package
    METADATA DOCUMENT, fields top-level) lost its human name and the top bar showed
    app.b2r4. Closing that asymmetry inside the one reader that already declares both
    positions — the server's own list handler reads item.manifest?.id || item.id on both
    halves, so this is not a new tolerant dialect. scope is deliberately left
    manifest-only (it decides what the switcher HIDES, a different question).

2. Dashboard property panel

The panel is spec-driven on purpose: dashboardForm from @objectstack/spec/ui fed
straight into SchemaForm, so a new spec prop shows up with zero code changes. The spec
authors that copy in English, and nothing on this side translated it.

Rather than invent a parallel key scheme, the new metadata-form-i18n.ts supplies a
translation bundle to the platform's own resolver — resolveMetadataFormLabels
from @objectstack/spec/system, the same function the framework's /meta/types handler
calls to localize these very forms. That buys the producer's section-name slugging, its
dot-path field addressing, and its composite sub-field synthesis (which is what produces
the header.showTitle / showDescription / actions rows the card listed). When an
environment's own bundle reaches this surface it drops in as a higher-precedence source
with no call-site change.

3. Tool card status — one producer

cloud#787's shape, mirrored. The header badge derived "Awaiting Approval" from
isProposalResult(tool.result) alone — a fact about the tool's own output, which never
changes once it has returned — while the body collapsed to 已搭建 / 已生效 / 未生效 from
four other memos the header never read. Fixing it in the header's own branch would have
made a FIFTH producer, so resolveProposalCardState is now the single one both sides
read, with a table mapping its four states onto the badge.

i18n coverage report

No coverage script exists in this repo for these families, so it was measured directly:
a throwaway vitest module imported the ten locale packs plus
PLATFORM_TOOLS_BY_PACKAGE from @objectstack/spec, counted defined keys per pack, and
ran the real overlay over dashboardForm to dump every string the panel renders. Run at
2d332f868; the module was deleted afterwards (nothing in this diff).

AI tool cards, @object-ui/i18n packs

key family keys per-pack coverage
chatbot.tool.* 30 en:30/30 · zh:30/30 · ja:30/30 · ko:30/30 · es:30/30 · fr:30/30 · de:30/30 · pt:30/30 · ru:30/30 · ar:30/30
chatbot.toolState.* 9 en:9/9 · zh:9/9 · ja:9/9 · ko:9/9 · es:9/9 · fr:9/9 · de:9/9 · pt:9/9 · ru:9/9 · ar:9/9
chatbot.plan.* 7 en:7/7 · zh:7/7 · ja:7/7 · ko:7/7 · es:7/7 · fr:7/7 · de:7/7 · pt:7/7 · ru:7/7 · ar:7/7

Platform tool names in PLATFORM_TOOLS_BY_PACKAGE: 30; keyed here: 30; unkeyed:
none. The scope is that closed registry rather than the six names the card happened
to list — the maintenance contract for adding a tool already lives there, and partial
coverage is exactly the dormancy this fixes. en values equal what the existing English
title-caser produced, so adding the keys cannot silently reword the English UI (the one
deliberate exception: the badge's "Awaiting Approval" becomes "Awaiting approval", which
is what the activity chips beside it already said).

Dashboard property panel, every string it renders after the overlay

基本信息 / 仪表板的名称与描述。 / 名称 / snake_case 唯一标识 / 显示名称 /
展示给使用者的名称 / 描述 / 布局 / 栅格尺寸与刷新频率。 / 列数 / 栅格列数(默认 12) /
间距 / 组件之间的间距,数值越大越松 / 自动刷新 / 自动刷新间隔(秒),0 表示不自动刷新 /
页眉 / 页眉设置:标题、描述与操作按钮 / 显示标题 / 在页眉中显示仪表板名称 / 显示描述 /
在页眉中显示仪表板描述 / 操作按钮 / 显示在页眉里的操作按钮 / 组件 /
放在栅格上的卡片与图表。 / 组件 / 仪表板组件,含位置与尺寸 / 筛选 /
应用到全部组件的默认筛选与全局筛选。 / 日期范围 / 默认的日期范围选择器 / 全局筛选 /
应用到全部组件的筛选条件 / 高级 / 无障碍与性能调优。

35 strings rendered; 1 still carries a 4+ letter Latin word — snake_case 唯一标识,
which is the identifier convention's own name and is deliberately not translated. All
seventeen strings the card enumerated are covered.

Declared deviation from the dispatch ruling

The ruling said the property panel's strings go through the @object-ui/i18n
dictionary. They go through the engine.* carve-out beside them instead
(views/metadata-admin/, en+zh), and this is flagged rather than done quietly:

  • every other string on that same panel already comes from there
    (engine.inspector.dashboard.*, engine.studio.*) — splitting one panel across two
    dictionaries is worse to maintain than either alone;
  • the carve-out is documented on both ends (metadata-admin/i18n.ts header,
    packages/i18n/README.md "Scope — the engine.* carve-out") with a deliberately
    narrow reopening condition;
  • @object-ui/i18n enforces full key parity across ten packs, so the alternative was
    inventing technical form copy in eight more languages nobody asked for.

The user-visible outcome the ruling demanded — full Chinese coverage of the listed
strings — is met either way; the tool-card half is in @object-ui/i18n as directed.

Producer-side findings, reported not guessed

Contract-first: two English strings on that panel are the spec's own copy and are NOT
rewritten here.

  1. dashboardForm.gap.helpText is Grid gap (Tailwind units) — a unit only a developer
    can act on. The zh overlay says what an author can decide; the English source is
    packages/spec/src/ui/dashboard.form.ts's to fix.
  2. objectstack's generated zh-CN.metadata-forms.generated.ts carries a stale
    dashboard.sections.layout.description (栅格与响应式) against a source that now says
    "Grid sizing and refresh cadence." — the extractor's merge only fills gaps, so a
    present-but-stale leaf is never corrected. Worth a card on that side.
  3. PackageSwitcher's package-list fetch swallows any failure (.catch(() => {})),
    which leaves the top bar showing the raw package id forever with no signal. Out of
    scope here (it is the fetch's error posture, not the naming) — reported for triage.

Tests

Run at 2d332f868 (final commit; git rev-parse --short HEAD), after
pnpm install + a dependency-closure build on the rebased tree.

  • pnpm exec vitest run packages/plugin-chatbot/ packages/i18n/ packages/app-shell/src/views/studio-design/136 files, 1592 tests, 0 failed
  • pnpm exec vitest run packages/app-shell/src/views/metadata-admin/ packages/app-shell/src/preview/ packages/app-shell/src/console/ai/254 files, 2533 passed, 1 skipped
  • pnpm --filter @object-ui/{i18n,plugin-chatbot,app-shell} type-check — all exit 0. Each
    runs tsc --noEmit && tsc -p tsconfig.test.json, so the new test files are covered
    rather than excluded.
  • pnpm --filter @object-ui/{i18n,plugin-chatbot,app-shell} build — all Done.
  • Gates, verdict lines quoted from the gate itself, exit code captured before any pipe:
    check-i18n-call-site-keys "Every in-scope call-site key resolves against the en pack
    (2952 keys) …"; check-i18n-en-drift "No en value changed in this range." (46 keys
    added, 0 changed); check-control-bytes "OK (scanned 6048 tracked text file(s))";
    check-changeset-presence "24 source file(s) of 3 released package(s) changed, and
    this change declares 1 changeset(s)"; check-changeset-no-major "No changeset declares
    a major bump."

Reverse verification (run from the committed state, so the restore leg had a real
reference): the header-state fix was reverted on disk with perl -0pi, the mutation
proved by grep counts (injected 1 / removed 0) and a changed git hash-object, and
toolCardHeaderState-7254.test.tsx went 3 failed / 4 passed — exactly the three pins
that assert the header follows the body; the unanswered-proposal case and the four unit
cases stayed green, which is correct because resolveProposalCardState still existed.
Restored via git checkout HEAD -- path, proved by an empty git diff HEAD.

Lint, a declared narrowing. The repo-wide pnpm lint is CI's run; here eslint was
run on exactly the files this PR changes, and every message attributed to a changed line
by cross-referencing git diff -U0. Three facts, so the narrowing is a measurement and
not a skip: (a) the population came from eslint's own config resolution over the 24
changed .ts/.tsx paths, not from a guess about which files count; (b) the file count
is eslint's own --format json output — 24 files, 1 error, 36 warnings; (c) this repo
does not enable type-aware linting, so nothing in this diff can move a verdict on an
untouched file. Exactly one message lands on a line this PR wrote:
react-refresh/only-export-components on the newly exported resolveProposalCardState
— a warning, and the sixth of that same family already firing in that file (839, 942,
951, 1141, 3659). The single error, react-hooks/static-components in
StudioDesignSurface.tsx, is pre-existing: linting origin/main's own copy of that file
reports the identical error (line 2744 there, 2773 here — same code, shifted by this
diff's insertions).

Not measured / known-red elsewhere. packages/app-shell/src/console/__tests__/anonSeedScope-5746.enumeration.test.tsx
fails 7/7 — and fails identically with this PR's source changes reverted to the merge
base, so it is pre-existing and unrelated. The whole-package app-shell run exceeds the
container's 10-minute foreground cap; the two directories this diff touches were run in
full instead, plus preview/ and console/ai/ (the other consumers of the changed
components, found by grepping the tests for the strings this PR moves).

Overlap with in-flight work

Rebased onto ab9294039. One conflict, in packages-io.ts, against #7331 ("Studio
package switcher reads the server's writable verdict"): resolved as the union of both
intents
— their server-computed writable verdict kept verbatim, this card's
name-from-either-position added beside it, and the doc comment extended to say why
writable reading only the top level is a different thing from the name asymmetry
(it is the server's own computed field, which the manifest does not carry). No side was
taken wholesale.

One pin was updated rather than added: StudioDesignSurface.interfacesAction.test.tsx
asserted the canvas caption's old raw action · sync_now text. That text is precisely
what this card removes, so the assertion moves to the caption's testid — its label plus
the internal pair on the tooltip.

Generated by Claude Code

hotlong and others added 3 commits September 2, 2026 17:43
…ed 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>
…re 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>
`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>
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 48 chunks) 3173.2 KB 3191.4 KB
Main entry chunk (gzip) 142.7 KB 350 KB
Entry file index-Dwe3QBmX.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.33KB 5.59KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 514.59KB 117.40KB
core (index.js) 5.80KB 2.32KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 178.20KB 49.60KB
fields (index.js) 244.25KB 61.73KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.98KB 10.98KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 47.00KB 12.97KB
plugin-charts (index.js) 70.02KB 19.44KB
plugin-chatbot (index.js) 196.19KB 46.43KB
plugin-dashboard (index.js) 132.63KB 34.56KB
plugin-designer (index.js) 212.87KB 43.19KB
plugin-detail (index.js) 250.63KB 63.90KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.78KB 32.58KB
plugin-gantt (index.js) 166.93KB 40.82KB
plugin-grid (index.js) 208.92KB 56.59KB
plugin-kanban (index.js) 53.21KB 14.66KB
plugin-list (index.js) 113.51KB 27.67KB
plugin-map (index.js) 20.20KB 6.66KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.51KB 11.94KB
plugin-timeline (index.js) 29.15KB 8.40KB
plugin-tree (index.js) 8.98KB 3.08KB
plugin-view (index.js) 85.90KB 21.12KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 3.11KB 1.48KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.72KB 2.24KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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

Projects

None yet

1 participant