Skip to content

feat(app-shell): edit a Studio translation item as a source-to-translation table with its gaps counted - #30457

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-eme43k
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-eme43k

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #30285

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:Studio 里打开一个翻译条目,不再是一堆原始 JSON 框,而是一张单语言的「原文 / 译文」对照表:按对象、字段、选项、应用等分组,顶部显示「120 条中 37 条未翻译」这样的计数,可以只看未翻译的。JSON 还在,放到「高级:以 JSON 编辑」里。条目列表里客户写在 .custom 包里的翻译显示为「运行时」,不再显示「代码包」。
  • 为什么改:管理员看不出还有哪些文字没翻,尤其是「用户」这类平台对象的标签只自带 en / zh-CN / ja-JP / es-ES,换成 fr-FR、vi-VN 时就缺了。
  • 风险与回滚:低。保存的仍是同一个 translation 条目,存在同一个地方(只读代码包存在它的 .custom 包,ADR-0146 D2),没有新路由、新键或新存储。回滚即 revert 本 PR。
  • 先看:ui/packages/app-shell/src/views/studio-design/translationTable.ts(行从哪来、怎样算已翻译、怎样读声明)、StudioTranslationTable.tsx(表格本身)、StudioBundleItemEditor.tsx(翻译条目改用表格)。
  • 两分钟验证:启动 CRM 示例,zh-CN 打开 Studio → 全部元数据 → 翻译 → 点 .custom 里的一个翻译条目,预期看到计数、分组的对照表、「只看未翻译」开关,没有「尚未注册自定义渲染器」提示。
  • 需要你做的:无,席位落地。

What changes

A translation item in Studio now opens on a table for its language, replacing the per-group JSON boxes the form fell back to.

  • Rows. There is one row per string the package declares, grouped by item (an object, an app, a dashboard…) with the item's category and declared label as the heading. Each row shows its kind (object, field, option, navigation…), the source text, the key, and an input for the translation. The rows are collectExpectedEntries from @objectstack/lint (moved there in refactor(lint): move the i18n translatable-key walk from the CLI into @objectstack/lint #30375), the walk os i18n extract and os lint's coverage use, so a row's key is the key the runtime looks up.
  • What the walk is handed. It walks each item's declared document from the layered read: the code layer, or for a database-authored item its served document without the system: true fields the platform injects. It does not use the list read, which is translated into the reader's language and carries the injected owner, tenancy and audit fields. A view is keyed by its bare name, as bundles key it. The platform objects the items point at are walked too, including the lookups the platform injects (an owner is a user). That is the epic comment's case: sys_user, sys_organization and sys_business_unit labels ship in a few languages only, and under fr-FR or vi-VN they show as rows to translate. An item opened in a code package's .custom pair is walked over the base package and the pair together.
  • What counts as translated. A row is translated when the item has a value at its key, when the language already serves one from another bundle (read once from /i18n/translations/<locale>, and used only when the server answers that language and not a fallback), or when the source is an inline locale map that names the language. The count and the "Untranslated only" filter use that.
  • Saving is unchanged. A cell writes the item's JSON at its key, and clearing a cell removes the key and any group it leaves empty. The editor saves through the same guard, to the same package, as before. The item's label, language and description stay in the form above the table, with the translation groups hidden there.
  • JSON as the advanced view. "Advanced: edit as JSON" holds the item's translation groups, including what the table does not list: messages, settingsCommon, and the registry-driven metadataForms copy, which is Studio's own form text and the same for every package. The "no custom renderer registered, editing as JSON" notice no longer appears on this page, and the side preview is not shown for a translation, because the table shows its gaps itself.
  • Source label. The metadata item list labels an item whose _packageId is a code package's .custom pair (isCustomizationPackage from @objectstack/spec/kernel) as customer-written (运行时 / Runtime) instead of shipped in a code package.
  • Lazy linter. The walk is loaded with await import('@objectstack/lint') when a translation item opens, as capabilityLint.ts does. The console build asserts the linter stays out of the eager closure, and vite build passes that assertion.
  • Labels. 41 new engine.studio.translationTable.* strings in English and Chinese. Group headings reuse the existing engine.translationPreview.category.* labels.

Console chrome (新建, 保存…) is not a key of a translation item. It comes from the console's own language packs (messages maps to app.messages), so the table leaves it out. That was the card's first settlement, accepted by the seat.

Tests

  • translationTable.test.ts (new, 10 tests):

    • the rows are exactly the walk's translatable keys for a package with an object, a select with options, a lookup and an app with navigation, each with its declared text and kind;
    • metadataForms is left to JSON;
    • the count over draft, served and inline-map translations, and a fallback language not counting;
    • the write at a key, the clear that prunes empty groups, and the round trip (what a row writes is what the next build reads back, in the JSON editor's shape);
    • referenced objects;
    • the declared read: the code layer is preferred, a database-authored object loses its injected fields, an injected owner brings sys_user in, a pair is read with its base, a view is keyed by its bare name, the request count, and at most 4 reads in flight.
  • StudioTranslationTable.test.tsx (new, 5 tests), mounted over a mocked metadata client and SDK:

    • grouped rows including the platform object and the N of M untranslated count;
    • the filter;
    • a served row counting as translated, but not a fallback-language answer;
    • typing hands back the item with the value at the row's key;
    • the JSON view;
    • no fallback notice.
  • ResourceListPage.sourceLabel-30285.test.ts (new, 4 tests): a .custom pair's item is customer-written, the code package's is not, a doubled or bare suffix is not a pair, and the server's own classification still wins.

  • Ablations, each applied, run red, and restored (tree clean after each):

    # Ablation Result
    A1 walk not run 4 failed
    A2 source text not the declared text 2 failed
    A3 platform objects not walked 5 failed
    A4 served-elsewhere ignored 3 failed
    A5 the .custom source-label check removed 1 failed
    A6 effective layer instead of the declared one 2 failed

Acceptance notes

  • Request count on CRM (measured live, com.example.crm.custom item): the table makes 28 list reads (14 types × base and pair), 28 layered reads (one per item), 3 platform-object reads (sys_user, sys_organization, sys_business_unit) and 1 served-translations read, so 60 requests, at most 4 in flight. There is no per-row request. Reading a pair with its base doubles the list reads. That is the cost of covering objects a customer adds in the pair.
  • Live check found two defects in the first build, both fixed here. (1) The effective layer carries the injected fields, which produced rows such as "Created At" that no package declares: 508 rows dropped to 402. (2) An item opened from the list lives in the .custom pair, which declares nothing of its own, so the table was empty until the pair was read with its base.
  • Finding, not fixed (server/config): the CRM boot serves the locales en and zh-CN only. A published vi-VN item is served by /i18n/translations/vi-VN (checked: objects.sys_user.label = "Người dùng"), but a metadata read under Accept-Language: vi-VN falls back to en, so a vi-VN browser still sees "Accounts" (07-console-vi-VN-falls-back-to-en.png). Enabling the language is the deployment's locale setting, outside this card.
  • Finding, not fixed (pre-existing): a pair-only item opened through the base package's URL by hand (/studio/<base>/metadata/translation/<name>?package=<base>) loads an empty draft, because the editor reads the item at the base. The list links to the pair, so the normal path is unaffected.
  • Gate derivation gap (named here, not fixed): check:i18n and check:i18n-coverage derive their run from packages/cli/src/** only. Since refactor(lint): move the i18n translatable-key walk from the CLI into @objectstack/lint #30375 moved the walk to packages/lint/src/i18n-walk.ts, an edit there alone does not derive them locally. CI still runs both. The fix is to add packages/lint/src/i18n-walk.ts to their derivation roster.
  • Serial check at PR time: no open PR touches StudioBundleItemEditor.tsx, TranslationPreview.tsx or ResourceListPage.tsx. feat(app-shell): Studio opens an off-nav list view on its object's page, lists only views no dropdown reaches, and reads form views as forms #30401 and feat(app-shell): configure a list view's selection-bar actions in Studio (#30089) #30339 add keys to studio/en.ts / zh.ts elsewhere in those files.
  • Size: 10 files, +1322 / −6. check-governed-merges: 0 of 10 paths governed, 1328 ≤ 3000, ordinary queue landing.

Verification

app-shell, i18n, type-check, eslint, under os-verify-lock (head c488a30)
build (app-shell closure) :: exit 0
app-shell targeted (19 files, 195 tests) :: exit 0
@object-ui/i18n suite (95 files, 1443 passed, 13 skipped) :: exit 0
type-check @object-ui/app-shell + @object-ui/i18n :: exit 0
eslint (7 changed files) :: exit 0
console `VITE_BASE_PATH=/ vite build` (lazy-linter assertion included) :: exit 0
dispatch-gates: 35 derived, 35 run, all exit 0 (--ran: 0 NOT-MEASURED, 0 UNRUN)
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
Live check, zh-CN console, CRM example, built console served with vite preview

Item crm_vi (vi-VN) in com.example.crm.custom. Shots are in the project's ui-orders/30285-shots/.

  • 01-list-zh.png: the list shows crm_vi with source 运行时 and the 新增 mark.
  • 02-table-zh.png: 402 条中 401 条未翻译. Groups: 6 CRM objects, sys_business_unit, sys_organization, sys_user, the app, the dashboard, the dataset, 3 positions, 2 permission sets, the page and the flow. objects.crm_account.label reads 已在此翻译 ("Khách hàng"). No 尚未注册自定义渲染器 notice.
  • 03-filter-untranslated-zh.png: 只看未翻译 shows only missing rows.
  • 04-edited-row-zh.png / 04-edited-zh.png: typing "Người dùng" on objects.sys_user.label turns the row to here, the count goes to 402 条中 400 条未翻译, and the autosave sends PUT /api/v1/meta/translation/crm_vi?mode=draft&package=com.example.crm.custom&surface=studio.
  • 05-publish-sheet-zh.png / 06-published-zh.png: 发布 → 全部发布. Afterwards /api/v1/i18n/translations/vi-VN answers locale: vi-VN, with objects.sys_user.label = "Người dùng" and objects.crm_account.label = "Khách hàng".
  • 07-console-vi-VN-falls-back-to-en.png: see the locale finding above.

Generated by Claude Code

…ation table

The table lists a row per string the package declares (the lint walk over
the package's declared documents, plus the platform objects it points at),
the source text beside the translation, grouped by item, with an
untranslated count and filter. Each cell writes the item's JSON at its key,
so the saved item and where it is saved are unchanged. The JSON stays as an
advanced view. The item list labels a .custom pair's item customer-written.

Claude-Session: https://claude.ai/code/session_01WGufzfqCqBH59yrHmuqXH3
Co-authored-by: Claude <noreply@anthropic.com>
… .custom pair with its base

The layered read's code layer is what a package declares; the effective
layer carries the fields the platform injects into every object. A
database-authored object is read from its effective layer less those
fields. The platform objects the injected lookups point at (an owner is a
user) are still walked. An item opened in a code package's .custom pair is
walked over the base package and the pair, and the editor loads the lint
walk lazily, as the console build requires.

Claude-Session: https://claude.ai/code/session_01WGufzfqCqBH59yrHmuqXH3
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Nov 5, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-support-ai · session_01WGufzfE4V9fzZgJLrLDmMC

维护者速读(席位终稿)

  • 改了什么:Studio 里打开一个翻译条目,变成单语言的「原文 / 译文」对照表:按对象、字段、选项、应用等分组,顶部计数「N 条中 M 条未翻译」,可只看未翻译;原来的 JSON 收进「高级:以 JSON 编辑」。条目列表里 .custom 包中的条目显示为「运行时」。
  • 为什么改:管理员看不出哪些文字还没翻,尤其平台对象(如「用户」)只自带少数几种语言。
  • 风险与回滚:低。保存的仍是同一个 translation 条目、同一条保存路径,没有新路由、新键或新存储;遍历代码在打开翻译条目时才懒加载,控制台首屏不变。回滚即 revert 本 PR。
  • 先看:ui/packages/app-shell/src/views/studio-design/translationTable.ts、StudioTranslationTable.tsx、StudioBundleItemEditor.tsx。
  • 两分钟验证:CRM 示例,zh-CN 打开 Studio → 全部元数据 → 翻译 → 点 .custom 里的一个翻译条目,看到计数、分组对照表和「只看未翻译」开关。
  • 需要你做的:无。不是受管面,1328 行未过线,席位直接落地。
  • 席位意见:ACCEPT。

席位核对 · ACCEPT · head c488a30b01 · 2026-11-05T12:11Z

  1. Shape: draft, base main, line 1 Fixes #30285 (PR-A refactor(lint): move the i18n translatable-key walk from the CLI into @objectstack/lint #30375 merged as 8293c74df9); the only other numbers in the body (refactor(lint): move the i18n translatable-key walk from the CLI into @objectstack/lint #30375, feat(app-shell): Studio opens an off-nav list view on its object's page, lists only views no dropdown reaches, and reads form views as forms #30401, feat(app-shell): configure a list view's selection-bar actions in Studio (#30089) #30339) carry no closing verb. 10 files, +1322/−6, all under ui/packages/app-shell and ui/packages/i18n. check-governed-merges --pr 30457: 0 of 10 governed, 1328 ≤ 3000, ordinary landing. No packages/spec/src path and Clause-②: no: no contract review owed. git merge-tree vs current main clean.
  2. Read by the seat: rows come from collectExpectedEntries, loaded with await import('@objectstack/lint') in StudioTranslationTable.tsx (app-shell already depends on @objectstack/lint); translationTable.ts keeps metadataForms in JSON, counts here / elsewhere / inline as translated, and writeTranslationAt removes a cleared key and the groups it empties without mutating the item. StudioBundleItemEditor hides the translation groups from the form (keeps label / locale / description), mounts the table, and drops the side preview for a translation only. classifyProvenance adds one branch: isCustomizationPackage(pkg) ⇒ runtime, after the explicit rawSource answers.
  3. Accepted deviations: none against the card. Two live-check defects of the first build (injected fields as rows; a .custom pair walked without its base) were found and fixed in this PR, each with a test.
  4. Evidence: dev's runs on this head: app-shell targeted 195/195, @object-ui/i18n 1443 passed, type-check and eslint exit 0, console vite build with the lazy-linter assertion exit 0, dispatch-gates 35/35 exit 0 with --ran clean; six ablations each red, restored. Live zh-CN check on the CRM example with shots in ui-orders/30285-shots/. Changeset: @object-ui/app-shell, @object-ui/i18n patch, matching the diff. Check-runs on c488a30b still running at this record; the seat flips ready and arms only after every check reads success or an expected skip.
  5. Out of scope, with destinations: (a) check:i18n / check:i18n-coverage do not derive from packages/lint/src/i18n-walk.ts since refactor(lint): move the i18n translatable-key walk from the CLI into @objectstack/lint #30375 (CI still runs them): devx follow-up, carried in the ui#1 hand-off; (b) the CRM boot serves en / zh-CN only, so a metadata read under vi-VN falls back to en: deployment locale setting, recorded on Studio translation editor: a source-to-translation table with visible gaps, replacing the raw JSON boxes #30285; (c) a pair-only item opened by hand through the base package URL loads an empty draft (pre-existing; the list links to the pair): recorded on Studio translation editor: a source-to-translation table with visible gaps, replacing the raw JSON boxes #30285 and in the hand-off. Skills-docs: n/a (Studio UI, no authoring change).
  6. Landing: ordinary queue once CI is green: relay pr_ready + automerge_enable, follow to MERGED, landing record on Studio translation editor: a source-to-translation table with visible gaps, replacing the raw JSON boxes #30285 (closes the card).

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 12:28
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 648089d Nov 5, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-eme43k branch November 5, 2026 12:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Studio translation editor: a source-to-translation table with visible gaps, replacing the raw JSON boxes

1 participant