Skip to content

Commit 500bc9d

Browse files
feat(ui): lazy studio i18n namespace; designer.* and perm.* strings move into it (#25922 slice 1) (#26593)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9TW2qPuXi9wW2vYi3QL9EbS)_ Part of #25922 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:`@object-ui/i18n` 新增懒加载的 `studio` 命名空间(en 常驻、zh 按需拉取、其他语言回落 en),并把 Studio 设计器表里的 `designer.*` / `perm.*` 共 349 个键(两种语言)迁入;app-shell 的 `t()` 先查原表、再查命名空间,调用点不变。 - 为什么改:按 #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](https://claude.ai/code/session_01ADHkmRuDqEXC3CybUtz5d1)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9dc8fba commit 500bc9d

36 files changed

Lines changed: 1792 additions & 961 deletions
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/i18n': patch
3+
---
4+
5+
feat(i18n): a lazy `studio` namespace for the Studio designer's strings; the `designer.*` and `perm.*` keys move into it
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **New subpath `@object-ui/i18n/namespaces/studio`.** The Studio designer's two-locale string set becomes a namespace of this package, as the maintainer ruled on objectstack#25922 (6431032267, Q2 = A; kept by 6431234244). `en` is resident; `zh` is its own chunk, fetched the first time a Chinese lookup asks for it; every other language reads `en`. Exports: `studioString(key, lang)` (synchronous; answers from `en` while a catalogue is in flight), `loadStudioCatalogue(lang)`, `isStudioCatalogueLoaded(lang)`, `resolveStudioLanguage(lang)`, the `useStudioCatalogue(lang)` hook (re-renders its caller when a fetched catalogue lands), `STUDIO_NAMESPACE`, `STUDIO_FALLBACK_LANGUAGE`, `STUDIO_LANGUAGE_CODES` and the `StudioCatalogue` type. Additive: nothing existing changes.
11+
- **First slice of the move.** Every key under `designer.*` and `perm.*` (the object/field designer and the permission designer) now lives in the namespace instead of `@object-ui/app-shell`'s private table. Studio reads the same text in `en` and `zh`; a Chinese viewer sees these labels in English for the moment the `zh` catalogue takes to load, and a non-Chinese viewer no longer downloads these keys' Chinese text with the console.
12+
- **Not in the eager catalogue.** `locales/en.ts` gains no key, and the namespace is not one of the ten packs, so `all-locales-key-parity` does not read it; its own parity (every `en` key has a `zh` row with the same `{placeholders}`) is a test of this package.

‎ui/apps/console/vite.config.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,6 +1156,18 @@ export default defineConfig({
11561156
{ name: 'i18n-locale-ru', test: /[\\/]packages[\\/]i18n[\\/]src[\\/]locales[\\/]ru\.ts$/, priority: 84 },
11571157
{ name: 'i18n-locale-ar', test: /[\\/]packages[\\/]i18n[\\/]src[\\/]locales[\\/]ar\.ts$/, priority: 84 },
11581158
//
1159+
// The Studio designer's `studio` namespace (objectstack#25922): the
1160+
// same one-group-per-catalogue rule, for the same reason. Its `zh`
1161+
// catalogue is `import()`ed on demand by
1162+
// `packages/i18n/src/namespaces/studio/index.ts`; without a group of
1163+
// its own it would fall to `i18n-runtime` below, which is eager, and
1164+
// the laziness would be real in the source and absent in the bundle.
1165+
// `en` is the namespace's resident fallback and gets its own chunk so
1166+
// its bytes are named in `eager-closure.json` instead of hiding in
1167+
// the runtime's.
1168+
{ name: 'i18n-ns-studio-en', test: /[\\/]packages[\\/]i18n[\\/]src[\\/]namespaces[\\/]studio[\\/]en\.ts$/, priority: 84 },
1169+
{ name: 'i18n-ns-studio-zh', test: /[\\/]packages[\\/]i18n[\\/]src[\\/]namespaces[\\/]studio[\\/]zh\.ts$/, priority: 84 },
1170+
//
11591171
// ⛔ `i18n-runtime` takes `includeDependenciesRecursively: false`
11601172
// for the reason `data-adapter` below does (objectui#9345, read out
11611173
// there in full): it outranks `framework` (83 over 80), and since

‎ui/packages/app-shell/src/views/metadata-admin/CelTestRunDialog.i18n-10862.test.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ import { I18nProvider } from '@object-ui/i18n';
4040
import { t } from './i18n';
4141
import { __setCelFormulaLoader } from './celAuthoring';
4242
import { CelTestRunDialog } from './CelTestRunDialog';
43+
// The designer's `designer.*` / `perm.*` rows live in `@object-ui/i18n`'s `studio`
44+
// namespace, whose `zh` catalogue is fetched on demand (objectstack#25922).
45+
// Resident before any case, so the zh rows below read synchronously.
46+
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
47+
await loadStudioCatalogue('zh');
4348

4449
afterEach(() => {
4550
cleanup();

‎ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.cloneToCustomize.test.tsx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,12 @@ vi.mock('./AssignedUsersSection', () => ({
161161

162162
import { PermissionMatrixEditPage } from './PermissionMatrixEditor';
163163

164+
// The designer's `designer.*` / `perm.*` rows live in `@object-ui/i18n`'s `studio`
165+
// namespace, whose `zh` catalogue is fetched on demand (objectstack#25922).
166+
// Resident before any case, so the zh rows below read synchronously.
167+
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
168+
await loadStudioCatalogue('zh');
169+
164170
/* ── The door ────────────────────────────────────────────────────────────── */
165171

166172
interface Call {

‎ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.referencesErrorState.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ describe('References panel — error ≠ empty ≠ loading (#5110)', () => {
260260
/**
261261
* The designer table carries `en` and `zh` in one file and is deliberately
262262
* out of reach of the pack gates (see `metadata-admin/i18n.ts`'s header and
263-
* `packages/i18n/README.md`, "Scope — the `engine.*` carve-out"), so a new
263+
* `packages/i18n/README.md`, "Scope — the Studio designer strings"), so a new
264264
* key added to only one of the two tables fails nowhere. This is that file's
265265
* parity check for the three keys this card adds.
266266
*

0 commit comments

Comments
 (0)