Skip to content

feat(ui): lazy studio i18n namespace; designer.* and perm.* strings move into it (#25922 slice 1) - #26593

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-zplnpz
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-zplnpz

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread

Part of #25922

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:@object-ui/i18n 新增懒加载的 studio 命名空间(en 常驻、zh 按需拉取、其他语言回落 en),并把 Studio 设计器表里的 designer.* / perm.* 共 349 个键(两种语言)迁入;app-shell 的 t() 先查原表、再查命名空间,调用点不变。
  • 为什么改:按 app-shell: a published library that is 66% Console product surface — retire the 67 zero-reference exports now, decide the split, and fold the second i18n table into @object-ui/i18n #25922 裁决 6431032267(Q2 = A,6431234244 维持)把设计器文案搬进 i18n 包,这是 (M) 的第 1 片,只搬机制和第一块连续的表。
  • 风险与回滚:zh 用户首次打开设计器时这些标签会先显示英文、zh 分片到达后同一挂载自动重渲染为中文;回滚即 revert 本 PR(表行原样回到 app-shell)。
  • 先看:ui/packages/i18n/src/namespaces/studio/index.ts,再看 app-shell views/metadata-admin/i18n.ts 的 lookupString 与 useMetadataLocale。
  • 两分钟验证:cd ui && pnpm run check:i18n-designer-parity && pnpm exec vitest run packages/i18n/src/__tests__/studio-namespace.test.ts packages/i18n/src/__tests__/studio-namespace-lazy.test.tsx。
  • 需要你做的:无需操作;剩余切片计划见下文 “Remaining slices”,确认或调整即可。ci.yml 的 parity 步骤本 PR 不动,退役放在最后一片(Tier H)。

What changes

  • New subpath @object-ui/i18n/namespaces/studio (ui/packages/i18n/src/namespaces/studio/{index,en,zh}.ts, package.json export). Same residency shape as locales/registry.ts: en is a static import (the fallback for every key and language), zh is one literal () => import('./zh.js'). API: studioString(key, lang) (synchronous; while the serving catalogue is in flight it starts the fetch and answers from en, never the raw key), loadStudioCatalogue(lang) (memoised, a rejected load is not memoised), isStudioCatalogueLoaded, resolveStudioLanguage (exact tag, then base language, else en), the useStudioCatalogue(lang) hook (useSyncExternalStore, re-renders its caller when a catalogue lands), STUDIO_NAMESPACE, STUDIO_FALLBACK_LANGUAGE, STUDIO_LANGUAGE_CODES, StudioCatalogue.
  • First slice of the table: every designer.* and perm.* row (349 keys per side; one contiguous run on each side of ENGINE_STRINGS_EN / ENGINE_STRINGS_ZH) moves out of ui/packages/app-shell/src/views/metadata-admin/i18n.ts into the namespace, text unchanged.
  • app-shell facade: the six pickTable(locale).strings[key] reads go through lookupString(key, locale) = table first, namespace second, so t / tFormat / tOptional / translateNodeLabel / translateNodeHint / translateEnumOption and every call site stay as they are. The 54 zh-only producer keys are untouched and keep their tOptional read. useMetadataLocale() now also calls useStudioCatalogue(locale), so every designer component re-renders once zh lands.
  • Not in the eager catalogue, not in all-locales-key-parity, by construction. locales/en.ts gains no key; the namespace's files live outside locales/ and are not members of builtInLocales, which is what that suite iterates. The index.ts docblock and the README say so; the namespace's own parity is studio-namespace.test.ts.
  • Console chunking (ui/apps/console/vite.config.ts): two priority-84 advancedChunks groups, i18n-ns-studio-en and i18n-ns-studio-zh, so the namespace is not swept into the eager i18n-runtime group (whose test matches all of packages/i18n).
  • Gates read the namespace homes (ui/scripts/check-i18n-dead-keys.mjs collectDesignerKeys, used by check-i18n-designer-table-parity.mjs and check-i18n-en-drift.mjs): when the table module imports @object-ui/i18n/namespaces/studio (the same edge the runtime follows), the homes' STUDIO_EN / STUDIO_ZH literals are merged into ENGINE_STRINGS_EN / _ZH by AST. A home that cannot be read throws; a key in both homes throws ("exactly one home"); the dead-keys sweep does not count a home literal as a reader. en-drift reads the homes at the same ref, so the move itself is not drift.
  • README: the "engine.* carve-out" section is rewritten as "Scope — the Studio designer strings (the studio namespace)", citing rulings 6431032267 and 6431234244; two comments that cited the old heading name follow it.
  • Changeset: root .changeset/25922-studio-i18n-namespace.md, @object-ui/i18n patch.
  • ⛔ Untouched: ui/.github/**, root ci.yml (its check:i18n-designer-parity step stays), existing keys of the eager locale packs, ObjectGrid.tsx.

Tests

New:

  • ui/packages/i18n/src/__tests__/studio-namespace.test.ts (unit): scoped parity (en ⊆ zh, same {placeholders}, zh-only keys only in documented families, non-empty values), no namespace key defined by locales/en.ts (with a lit control), language resolution table, resident zh answers zh while de / en-US answer en, unknown key is undefined.
  • ui/packages/i18n/src/__tests__/studio-namespace-lazy.test.tsx (dom, isolated): before the fetch a zh lookup renders en; the same component re-renders in zh once the chunk lands; de renders en and fetches nothing.
  • ui/packages/app-shell/src/views/studio-design/AddFieldPopover.studioNamespace-25922.test.tsx: a real Studio screen (Data pillar "Add field" popover, which mixes both homes) through useMetadataLocale() — zh first paint is English (never the key), then the same mount is Chinese; table keys read Chinese throughout; de renders English from both homes.
  • ui/scripts/__tests__/check-i18n-dead-keys.test.ts: six collectDesignerKeys() home cases (merge, no import → no homes, unreadable home throws, duplicate key throws, source + readHome, sweep ignores home literals).
  • ui/scripts/__tests__/check-eager-closure-budget.test.ts: the two namespace modules route to i18n-ns-studio-zh / i18n-ns-studio-en.

Forced test edits (the zh rows these suites assert synchronously now arrive in a lazily fetched chunk, so each makes zh resident before its first case with a top-level await loadStudioCatalogue('zh'); no assertion changed):

  • metadata-admin/zh-base-term.i18n-13131.test.ts — the zh census also reads the namespace's zh rows (otherwise it would silently stop covering 349 keys).
  • Preload only, 17 files: metadata-admin/CelTestRunDialog.i18n-10862.test.tsx, metadata-admin/PermissionMatrixEditor.cloneToCustomize.test.tsx, inspectors/FieldTypePicker.search-12516.test.tsx, inspectors/ObjectDefaultInspector.test.tsx, inspectors/ObjectFieldInspector.draft-locale.test.tsx, inspectors/ObjectFieldInspector.labelTranslation-11782.test.tsx, inspectors/ObjectFieldInspector.typePicker-11793.test.tsx, inspectors/ObjectFieldInspector.valueDomain-7597.test.tsx, inspectors/inspectorStrings.hookAction.i18n-10586.test.tsx, inspectors/inspectorStrings.i18n-10586.test.tsx, inspectors/inspectorStrings.i18n-10696.test.tsx, inspectors/inspectorStrings.i18n-10748.test.tsx, previews/PermissionPreview.rlsPolicies-11027.test.tsx, previews/dataPreviews.i18n-10862-s3.test.tsx, previews/field-types.studioSurfaces-11909.test.tsx, previews/flowFieldChrome.i18n-10862-s4.test.tsx, studio-design/BuilderLanding.environmentOverlayCopy-12171.test.ts (all under ui/packages/app-shell/src/views/).
  • Comment only: metadata-admin/ResourceEditPage.referencesErrorState.test.tsx (cites the README section's new name).

Acceptance notes

Console eager bundle does not grow. Same tree, console vite build, BEFORE = the base i18n.ts + vite.config.ts restored in place, AFTER = this branch:

before after Δ
eager closure, gzip 2,000,416 B 1,995,304 B −5,112
eager closure, raw 6,817,847 B 6,796,608 B −21,239
i18n chunk (app-shell table), gzip 118,144 105,711 −12,433
i18n-runtime, gzip 6,308 6,637 +329
i18n-ns-studio-en (new, eager), gzip — 6,950 +6,950
i18n-ns-studio-zh (new, lazy) — 22,332 raw not in the closure
i18n-locale-en (the eager en catalogue) 169,775 raw / 53,322 gz identical, same hash 0

locales/en.ts is 270,655 B before and after. check-eager-locale-catalogues and check-eager-closure-budget exit 0 on the AFTER dist (aggregate 1948.5 KB of 2431.6 KB).

Ablations (scripts/ablation-replace.mjs, mutation landed and restored, git diff HEAD empty after each):

  • studioString's en fallback → return undefined;: 2 red — studio-namespace-lazy "answers from en before the zh catalogue lands", AddFieldPopover "zh: English while the catalogue is in flight".
  • resolveStudioLanguage's ?? STUDIO_FALLBACK_LANGUAGE → ?? (lang ?? ""): 8 red, including both third-locale (de) screen cases.

Gates: check:i18n-designer-parity green (ENGINE_STRINGS 3121 en / 3175 zh, the union unchanged), check:i18n-drift green with 0 keys added or removed, check:i18n-dead-keys and check:i18n-keys green.

Open questions (implementation choices, not blockers):

  1. The namespace is not registered as i18next resources: its values use the designer's {token} syntax, which i18next would interpolate as literal text. A later slice could convert, but that is a text change to every row.
  2. useMetadataLocale() still collapses to en-US / zh-CN, so a zh-TW viewer reads zh (as before this PR).
  3. Clause-②: no is copied from the claim; the PR adds one subpath export, which is additive.

Remaining slices (proposal)

Remaining app-shell table ≈ 6,430 lines across both locales (engine.inspector 1,644; engine.studio 1,586; engine.packages/form/edit 802; other engine.* 2,398). Changed lines per slice ≈ 2× table lines, each ≤ ~3,000 and each keeping check:i18n-designer-parity green; each slice adds the zh preload to the zh suites it touches.

Slice Content Table lines
S2 engine.inspector.{pageBlock, action, dataset, flowNode, appNav, widget} ~1,141
S3 rest of engine.inspector + engine.packages / form / edit (incl. the zh-only packages.form.help keys, still read by tOptional) ~1,305
S4 engine.studio.{access, if, data, rules, auto, settings} + ~450 of other engine.* ~1,344
S5 rest of engine.studio + ~650 of other engine.* ~1,342
S6 the remaining engine.* ~1,300
S7 (Tier H, needs maintainer approval) TYPE_LABELS / DOMAIN_LABELS and the zh side tables (FLOW_FIELD_ZH …); delete the app-shell table; retire check:i18n-designer-parity and its ci.yml step —

Verification

  • 226 test files touched by the move (app-shell designer, i18n, ui scripts): 226 passed / 2,907 tests after the clone-to-customize preload; the first run's one red was that zh case, fixed in PermissionMatrixEditor.cloneToCustomize.test.tsx.
  • pnpm --filter @object-ui/i18n type-check, pnpm --filter @object-ui/app-shell type-check: both exit 0 (after pnpm --filter @object-ui/i18n build).
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 46 derived commands run, all exit 0; --ran reconciliation: 46 derived, 46 run, 0 NOT-MEASURED (derived zero). Also run: ui check:eager-closure, check:eager-locale-catalogues, check:i18n-dead-keys, check:i18n-designer-parity, check:i18n-drift, check:i18n-keys, all exit 0 on the AFTER console dist. Head e355404ca8; 36 files, +1,792 / −961 (2,753 changed lines, under the 3,000 threshold).

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · seat domain:ui#1 · 2026-10-27T09:44Z · #25922 (M) slice 1

Head e355404c (read from the PR object). Part of #25922. Rulings: Q2 = A (a lazy namespace, 6431032267) and Q1 → B (6431234244). This slice became dispatchable when #26483 landed.

  • Shape. 36 files, +1792 / −961 (2753 changed lines, under the 3000 threshold), with a root changeset (@object-ui/i18n patch).
    • The new lazy namespace is @object-ui/i18n/namespaces/studio. en is resident, zh is a separate chunk fetched on demand, and every other language falls back to en.
    • All 349 designer.* / perm.* keys per side move into it.
    • App-shell's t() reads its own table first and the namespace second, so no call site changed.
    • The README carve-out (objectui#4662) is rewritten to cite the rulings.
  • Eager bundle (measured on the same tree, before and after). The console's eager closure shrinks by 5,112 B gzip. The eager en catalogue is byte-identical (same hash). The zh chunk (22 KB raw) is outside the closure. check-eager-locale-catalogues and check-eager-closure-budget exit 0.
  • Forced test edits, re-read by the seat. 17 files gain only a top-level await loadStudioCatalogue('zh') preload, because their zh rows now arrive lazily. zh-base-term.i18n-13131 also reads the namespace's zh rows, so it keeps covering the 349 keys. One edit is a comment only. No assertion changed.
  • Gates. check-i18n-dead-keys and check-i18n-en-drift read the namespace homes, and check:i18n-designer-parity stays green. ⛔ The ci.yml parity step is untouched; retiring it is the last slice, Tier H.
  • Seat note, for the hand-off. A zh user's first Studio open renders English until the 22 KB chunk lands, then re-renders through the namespace's listener (tested: "zh: English while the catalogue is in flight"). A prefetch when the active language is zh would remove that flash. It is a possible follow-up and doesn't block this slice.
  • Governed surface. scripts/pm/check-governed-merges.mjs --pr 26593: not governed.
  • Checks pinned to the head. All check runs completed: 0 failing, 0 pending. The seat's landing script checked this before posting.
  • Evidence (dev). Two fallback ablations go red (2 cases and 8 cases). 226 affected test files pass. Gates: 46 derived + 6 ui i18n/eager gates, all exit 0.

Landing: ready + auto-merge through the relay.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 09:45
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit 500bc9d Oct 27, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-zplnpz branch October 27, 2026 10:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants