Skip to content

Commit 9ff8154

Browse files
refactor(app-shell): retire the designer string table and its parity gate (#27136)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8ScBVj68xfGbS6qVYg8VwB1)_ Part of #25922 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:删掉 app-shell 设计器私有表 `ENGINE_STRINGS_EN` / `ENGINE_STRINGS_ZH`(S6b 起已为空)和「先查表、再查命名空间」的查找,`t()` / `tOptional()` 现在只从 `@object-ui/i18n` 的 `studio` 命名空间取字;退役 `check:i18n-designer-parity`(脚本、测试、`ui/package.json` 脚本行、根 `ci.yml` 的一个步骤)。 - 为什么改:执行 #25922 裁决 6431032267(Q2 = A,6431234244 维持)的最后一片 S7,派单 6461796872。 - 风险与回滚:所有语言的文案与改前一致(同一 pin 在改前、改后都全绿);本 PR 删了 `.github/workflows/ci.yml` 的一个步骤,因此是 Tier H,需审批人批准后落地;回滚即 revert 本 PR。 - 先看:`ui/packages/app-shell/src/views/metadata-admin/i18n.ts` 的 `lookupString`,再看 `inspectorStrings.studioNamespace-25922.test.ts` 末尾新增的「the namespace is the only home」一组,以及 `check-i18n-dead-keys.test.ts` 末尾接手类型与领域标签配对校验的 pin。 - 两分钟验证:`cd ui && pnpm exec vitest run packages/app-shell/src/views/metadata-admin/inspectors/inspectorStrings.studioNamespace-25922.test.ts scripts/__tests__/check-i18n-dead-keys.test.ts`,应全绿。 - 需要你做的:Tier H,等席位的合同审查通过后,由审批人批准;在那之前本 PR 保持草稿。 ## What changes - **The table is gone.** Both `ENGINE_STRINGS_*` declarations in `ui/packages/app-shell/src/views/metadata-admin/i18n.ts` are deleted, and `lookupString` returns `studioString(key, locale)` alone. `pickTable` keeps `TYPE_LABELS` / `DOMAIN_LABELS`; `FLOW_FIELD_ZH` never read the table and is untouched. - **The gate is retired:** `ui/scripts/check-i18n-designer-table-parity.mjs`, its test, the `check:i18n-designer-parity` line in `ui/package.json`, and the root `.github/workflows/ci.yml` step "Verify the metadata-admin designer string tables stay paired" (one step; no other `.github/**` line). - **Its exports move, its coverage stays.** `DESIGNER_TABLE_PAIRS` and `readDesignerPairs` move into the shared extractor `ui/scripts/check-i18n-dead-keys.mjs`, where `check-i18n-en-drift.mjs` (CI's `check:i18n-drift`) and two Studio tests now import them. The extractor treats a table the module no longer declares as its namespace home alone. What the gate enforced is held by tests: - `ENGINE_STRINGS` parity (en ⊆ zh, `{placeholder}` parity, zh-only rows in documented families): `ui/packages/i18n/src/__tests__/studio-namespace.test.ts`, describe "studio namespace — scoped parity", which already covered every moved key. - `TYPE_LABELS` / `DOMAIN_LABELS` parity: a new describe "the real designer pairs stay paired" in `ui/scripts/__tests__/check-i18n-dead-keys.test.ts`, on the real tree, with floors per pair. - The gate's zh-only family reasons and their citation anchors: now `ZH_ONLY_REASONS` in the namespace pin, which checks each anchor still occurs exactly once. - **Readers adapted, none deleted.** `previews/__tests__/block-config-i18n.test.ts` reads its key sets from the namespace catalogues and asserts `i18n.ts` declares no `ENGINE_STRINGS_` constant (it had asserted both tables empty). `zh-base-term.i18n-13131.test.ts` runs its census over the loaded `zh` catalogue. `check-i18n-dead-keys.test.ts` and `check-i18n-en-drift.test.ts` gain fixtures for a module without the table. - **Prose:** `ui/packages/i18n/README.md`, the namespace's `index.ts` / `zh.ts` headers, and the extractor and drift-gate docs. The CI/CD guide page that listed the gate (`ui/content/docs/guide/ci-cd-pipeline.md`) was retired on `main` by #27071 while this branch was open; the merge keeps it deleted. - **Changeset:** `.changeset/25922-studio-i18n-table-retired.md`, `@object-ui/i18n` patch (its README ships). ## Why Ruling 6431032267 Q2 = A (upheld by 6431234244): the designer table becomes one lazy `@object-ui/i18n` namespace, moved in slices. S7 is the last slice, deleting the empty table and the gate that paired its two homes. ## Tests - **Pin:** `inspectors/inspectorStrings.studioNamespace-25922.test.ts` gains describe "the namespace is the only home (objectstack#25922 slice S7)": - every namespace key (3,216 `en`) through `t()` and `tOptional()` in `en-US`, `en`, `de-DE`, no locale, `zh-CN`, `zh` and `zh-TW` equals its catalogue value; - the 54 zh-only rows (9 package form help, 41 flow node, 4 flow enum) all belong to a documented family, answer in the three zh tags, and stay `undefined` through `tOptional()` in the four others; - each zh-only family's citation anchor occurs exactly once. - **Before and after:** the same pin file (182 tests) is green against this head, and green with `i18n.ts` set back to its base blob at `5fe0b967` (the table-first lookup over the empty tables). So every lookup resolves the same before and after. - **Ablations** (`scripts/ablation-replace.mjs`, each landed by anchor count and blob, restored to the `HEAD` blob with `git diff HEAD` empty): - (A) the namespace dropped from `lookupString`: pin red, 92 of 182, including both new S7 cases. - (B) a table asked in front of the namespace, holding one differing row (`engine.translationPreview.coverage`): pin red, 2 (that family's `t()` case and the every-key case). - (C) two `TYPE_LABELS_ZH` rows deleted: the new real-tree pair pin red ("TYPE_LABELS: every en key has a zh row with the same {placeholders}"). - (D) the `engine.packages.form.help.` citation anchor reworded in `package-schema.ts`: that citation case red. ### The sweep The `zh` loader in `namespaces/studio/index.ts` was delayed by 4,000 ms (`ablation-replace.mjs --hold`, restored to the `HEAD` blob). Under the delay, 312 test files ran: every app-shell test naming a zh locale or a moved family, the i18n tests naming `studio`, and the dead-keys and en-drift script tests. 3,826 of 3,827 tests passed. The one red is `i18n/src/__tests__/studio-namespace-lazy.test.tsx`, red by construction (its `findByText` waits 1 s against a 4 s delay), as in S6b. No racer. ## Acceptance notes - **Mechanism assumptions, measured:** the gate paired `ENGINE_STRINGS`, `TYPE_LABELS` and `DOMAIN_LABELS` (not `FLOW_FIELD_ZH`); all three keep a check, listed above. `TYPE_LABELS`, `DOMAIN_LABELS` and `FLOW_FIELD_ZH` stay, since none reads the deleted tables. The zh-only producer rows are 54 and stay optional lookups through `tOptional()`. - **Tier H:** `.github/workflows/ci.yml` is the register row `workflows` (`glob: '.github/workflows/**', tier: GOVERNED_TIER_H` in `scripts/pm/check-governed-merges.mjs`). `dispatch-gates --tier` derives no model-tier mandate: "Model tier — no path-derived mandate: the surface hits none of the 4 declared glob(s)". Changed lines 1,286 (+370 / −916) against the 3,000 threshold: under. This PR stays draft; no reviewers requested. The ci.yml step was re-applied after create, so pr_create's governed follow-up (#26617) did not request approvers before the contract review: `f8e5fa25` restored the step so the pull opened with no governed path, and the next commit reverts it, so the head carries the full S7 diff (its tree equals `d40b118a`'s). CI on `f8e5fa25` may be red for those minutes, since that step names a script this PR retires. - **Eager bundle:** not re-measured locally, as in S2–S6b. CI's `Build & E2E UI` measures it. ## Verification At `d40b118a` (the head's tree after the revert above), which merges `origin/main` `a98daf75` (21 commits past the branch point `5fe0b967`; the one conflict was the retired CI/CD guide page). The pin, ablations and sweep ran at `7c7a23e1`, before the merge; the merged commits touch the ten locale packs but not the namespace, the app-shell string module or an i18n gate script, and the focused set and every gate re-ran on the merged head. <details><summary>Tests and gates</summary> - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 88 commands at `d40b118a`, each exit 0 (captured before any pipe), including `pnpm --dir ui exec vitest run scripts/` (`Test Files 174 passed | 2 skipped (176)`). `--ran`: "88 derived famil(ies) accounted for — 88 run, 0 NOT-MEASURED (a DERIVED zero — all 88 recorded an exit code and none of them is 3)". The same 88 were also all exit 0 at `7c7a23e1`, before the merge. - `pnpm --filter @object-ui/i18n --filter @object-ui/app-shell run type-check`: exit 0. `pnpm --dir ui run type-check:scripts`: exit 0. - ui `check:i18n-dead-keys`, `check:i18n-drift`, `check:i18n-keys`: exit 0. `check:new-line-citations`: exit 0. `node scripts/check-nul-bytes.mjs`: exit 0. (`check:i18n-designer-parity` is retired here.) - Focused set on the merged head (the pin, `studio-namespace`, `block-config-i18n`, `zh-base-term`, the two Studio tests, the dead-keys and en-drift script tests): `Tests 487 passed (487)`. - Changed lines: 1,286 (+370 / −916) over 19 files. </details> ## Human Minimum - [x] 1. No `packages/spec` change. - [x] 2. No new names beyond the moved `DESIGNER_TABLE_PAIRS` / `readDesignerPairs` exports and the changeset. - [x] 3. No formatter ran; `pnpm lint` is CI's. - [x] 4. One card slice, one branch off `main`, one pull request. - [x] 5. Patch changeset with both declaration lines. - [x] 6. n/a. - [x] 7. No hand edits to generated or release-owned files. - [x] 8. English. --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbX3S5QrD29Ghii633f)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 40320f1 commit 9ff8154

19 files changed

Lines changed: 370 additions & 916 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/i18n': patch
3+
---
4+
5+
docs(i18n): the `studio` namespace is the designer strings' only home
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Last slice of the move ruled on objectstack#25922** (6431032267, Q2 = A; kept by 6431234244). `@object-ui/app-shell`'s private designer table, empty since the previous slice, is deleted, and its `t()` / `tOptional()` read `@object-ui/i18n/namespaces/studio` alone. Every string reads the same as before in every language. The README and the namespace's doc comments now say so, and point at `src/__tests__/studio-namespace.test.ts` as the namespace's one parity check, since the paired-table gate `check:i18n-designer-parity` is retired with the table. No export or string changes.

‎.github/workflows/ci.yml‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3540,9 +3540,6 @@ jobs:
35403540
- name: Verify changed en strings were followed by the nine translations
35413541
run: pnpm --dir ui run check:i18n-drift
35423542

3543-
- name: Verify the metadata-admin designer string tables stay paired
3544-
run: pnpm --dir ui run check:i18n-designer-parity
3545-
35463543
- name: Type-check the ui scripts
35473544
run: pnpm --dir ui run type-check:scripts
35483545

‎ui/package.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@
5858
"check:i18n-keys": "node scripts/check-i18n-call-site-keys.mjs",
5959
"check:i18n-drift": "node scripts/check-i18n-en-drift.mjs",
6060
"check:i18n-dead-keys": "node scripts/check-i18n-dead-keys.mjs",
61-
"check:i18n-designer-parity": "node scripts/check-i18n-designer-table-parity.mjs",
6261
"check:skills-paths": "node scripts/check-skills-paths.mjs",
6362
"check:skill-examples": "node scripts/check-skill-examples.mjs",
6463
"check:skill-eval-tokens": "node scripts/check-skill-eval-tokens.mjs",

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

Lines changed: 22 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -44,24 +44,26 @@
4444
* 6431234244) reopened the objectui#4662 carve-out: this table becomes the
4545
* `studio` namespace of `@object-ui/i18n` (`packages/i18n/src/namespaces/studio/`),
4646
* one lazy namespace carrying the existing `en` and `zh` text, every other
47-
* language falling back to `en`. It moves in slices. Until the last one, a key
48-
* lives either here or there, never both, and every lookup below asks this
49-
* table first and the namespace second (`lookupString`), so no call site
50-
* changes. `packages/i18n/README.md`, section "Scope — the Studio designer
51-
* strings", is the conventions-side record; keep the two in step.
47+
* language falling back to `en`. It moved in slices, and the last one (S7)
48+
* deleted the engine-string table from this module: every designer string now
49+
* answers from the namespace alone (`lookupString`), with no call site changed.
50+
* `packages/i18n/README.md`, section "Scope — the Studio designer strings", is
51+
* the conventions-side record; keep the two in step.
5252
*
53-
* What has not changed for the keys still here: only `en` and `zh` exist, so
54-
* the other eight shipped locales render English on these screens, and the
55-
* pack gates do not read this module (`scripts/check-i18n-call-site-keys.mjs`
56-
* skips it by declaration). `check:i18n-designer-parity` reads both homes.
53+
* Only `en` and `zh` exist, so the other eight shipped locales render English
54+
* on these screens, and the pack gates do not read the namespace
55+
* (`scripts/check-i18n-call-site-keys.mjs` skips this module by declaration).
56+
* The namespace's own parity is `packages/i18n/src/__tests__/studio-namespace.test.ts`;
57+
* the type and domain label tables below keep theirs in
58+
* `ui/scripts/__tests__/check-i18n-dead-keys.test.ts`.
5759
*/
5860

5961
import { useObjectTranslation } from '@object-ui/i18n';
6062
// The designer strings' new home. Its `en` catalogue is resident; its other
6163
// catalogues are fetched on demand, and `useMetadataLocale()` re-renders its
6264
// caller when one lands. ⚠️ `ui/scripts/check-i18n-dead-keys.mjs` reads the
63-
// namespace's catalogues as part of this table BECAUSE this module imports it:
64-
// drop the import and the gates stop reading them.
65+
// namespace's catalogues as this module's engine strings BECAUSE this module
66+
// imports it: drop the import and the gates stop reading them.
6567
import { studioString, useStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
6668
// Type-only (erased at build): the overlay-scope and lock-state label tables
6769
// below are keyed by their producer's own union rather than by hand — see
@@ -191,16 +193,6 @@ const DOMAIN_LABELS_ZH: Record<string, string> = {
191193
other: '其他',
192194
};
193195

194-
const ENGINE_STRINGS_EN: Record<string, string> = {
195-
// Empty since #25922 slice S6b: every row lives in `@object-ui/i18n`'s
196-
// `studio` namespace. Kept until slice S7 retires the table.
197-
};
198-
199-
const ENGINE_STRINGS_ZH: Record<string, string> = {
200-
// Empty since #25922 slice S6b: every row lives in `@object-ui/i18n`'s
201-
// `studio` namespace. Kept until slice S7 retires the table.
202-
};
203-
204196
/** Whether a locale resolves to Chinese (the only non-English catalog). */
205197
export function isZhLocale(locale?: SupportedLocale | string): boolean {
206198
return (locale ?? '').toLowerCase().startsWith('zh');
@@ -515,27 +507,25 @@ export function flowFieldZh(type: string, id: string): FlowFieldZh | undefined {
515507

516508
function pickTable(
517509
locale: SupportedLocale | string | undefined,
518-
): { types: Record<string, string>; domains: Record<string, string>; strings: Record<string, string> } {
510+
): { types: Record<string, string>; domains: Record<string, string> } {
519511
const lower = (locale ?? '').toLowerCase();
520512
if (lower.startsWith('zh')) {
521-
return { types: TYPE_LABELS_ZH, domains: DOMAIN_LABELS_ZH, strings: ENGINE_STRINGS_ZH };
513+
return { types: TYPE_LABELS_ZH, domains: DOMAIN_LABELS_ZH };
522514
}
523-
return { types: TYPE_LABELS_EN, domains: DOMAIN_LABELS_EN, strings: ENGINE_STRINGS_EN };
515+
return { types: TYPE_LABELS_EN, domains: DOMAIN_LABELS_EN };
524516
}
525517

526518
/**
527-
* The value of one designer string: this module's own table for the locale
528-
* first, then the `studio` namespace in `@object-ui/i18n`, which the table is
529-
* moving into slice by slice (objectstack#25922, ruling 6431032267 Q2 = A).
530-
* A key lives in exactly one of the two homes — `check:i18n-designer-parity`
531-
* fails on a key in both — so the order decides nothing but cost.
519+
* The value of one designer string, from the `studio` namespace in
520+
* `@object-ui/i18n` (objectstack#25922, ruling 6431032267 Q2 = A), which holds
521+
* every row this module's own table used to.
532522
*
533523
* The namespace answers a language it has no catalogue for, and a catalogue
534-
* still being fetched, from its `en` catalogue; this table keeps its own
535-
* behaviour for the keys it still holds (a key its zh half lacks echoes back).
524+
* still being fetched, from its `en` catalogue; a zh-only row (a flow node
525+
* label, an enum value, a package form help text) is `undefined` outside zh.
536526
*/
537527
function lookupString(key: string, locale: SupportedLocale | string | undefined): string | undefined {
538-
return pickTable(locale).strings[key] ?? studioString(key, locale);
528+
return studioString(key, locale);
539529
}
540530

541531
export function translateMetadataType(

‎ui/packages/app-shell/src/views/metadata-admin/inspectors/inspectorStrings.studioNamespace-25922.test.ts‎

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22

33
/**
4-
* objectstack#25922 slices S2 to S6b — the designer rows that moved into
4+
* objectstack#25922 slices S2 to S7 — the designer rows that moved into
55
* `@object-ui/i18n`'s `studio` namespace (every `engine.inspector.*` row;
66
* `engine.packages.*`, `engine.form.*`, `engine.edit.*`; all of
77
* `engine.studio.*`; the data source pages, the field types, the metadata
88
* preview panes, the flow designer, the rest of the Studio chrome and, last,
99
* the translation preview), and
1010
* app-shell's `t()` / `tOptional()` still answer every one of them the way they
11-
* did when the rows sat in its own table.
11+
* did when the rows sat in its own table. Slice S7 deleted that table and the
12+
* table-first lookup, so the namespace is the only home.
1213
*
1314
* What would break it:
14-
* - a row copied back into the app-shell table with a different value — the
15-
* table is asked first, so it would shadow the namespace silently;
16-
* - the namespace fallback dropped from `lookupString` — every moved key
15+
* - a lookup asked in front of the namespace again (a table brought back
16+
* with a row whose value differs) — it would shadow the namespace silently;
17+
* - the namespace dropped from `lookupString` — every moved key
1718
* would echo back as itself;
1819
* - a family emptied out of the namespace, which every per-key assertion
1920
* below would then pass over nothing. The floors are per FAMILY, by name,
@@ -23,6 +24,9 @@
2324
* `loadStudioCatalogue` (the module's public door), not a copy of their text.
2425
*/
2526
import { describe, expect, it } from 'vitest';
27+
import { readFileSync } from 'node:fs';
28+
import path from 'node:path';
29+
import { fileURLToPath } from 'node:url';
2630
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
2731
import { t, tOptional } from '../i18n';
2832

@@ -134,15 +138,49 @@ const FAMILY_FLOOR: Readonly<Record<string, number>> = Object.freeze({
134138
* carry. The package form's help text reads its English from the producer's own
135139
* `.describe()` sentence; the flow node labels and hints and the flow enum
136140
* values read theirs from the engine descriptor or the value itself. Each is
137-
* read through `tOptional` or a `lookupString` fallback (`ZH_ONLY_FAMILIES` in
138-
* `ui/scripts/check-i18n-designer-table-parity.mjs` carries the reasons).
141+
* read through `tOptional` or a `lookupString` fallback; `ZH_ONLY_REASONS`
142+
* below carries the reasons.
139143
*/
140144
const ZH_ONLY_FLOOR: Readonly<Record<string, number>> = Object.freeze({
141145
'engine.packages.form.help.': 9,
142146
'engine.flowNode.': 40,
143147
'engine.enum.': 4,
144148
});
145149

150+
/**
151+
* Why each zh-only family has no `en` row, and the reader that says so. Moved
152+
* here from the retired `check:i18n-designer-parity` gate with slice S7. A
153+
* citation is `{ file, anchor }`, the anchor is TEXT and never a line number,
154+
* and it must occur exactly once in its file or it stops locating anything.
155+
*/
156+
const ZH_ONLY_REASONS = Object.freeze([
157+
{
158+
prefix: 'engine.flowNode.',
159+
reason:
160+
'zh-only by design: for English the flow palette falls back to the engine descriptor’s own ' +
161+
'server-authoritative name/description, so an `en` row would OVERRIDE the server',
162+
file: '../i18n.ts',
163+
anchor: 'resolved by translateNodeLabel/Hint',
164+
},
165+
{
166+
prefix: 'engine.enum.type.',
167+
reason:
168+
'zh-only by construction: translateEnumOption returns the raw value before it ever looks the ' +
169+
'key up for any non-zh locale, so an `en` row is unreachable',
170+
file: '../i18n.ts',
171+
anchor: 'export function translateEnumOption',
172+
},
173+
{
174+
prefix: 'engine.packages.form.help.',
175+
reason:
176+
'zh-only on purpose: with no `en` row tOptional returns undefined, helpText drops out, and the row ' +
177+
'falls back to ManifestSchema’s .describe() in @objectstack/spec — an `en` row would copy that ' +
178+
'text into a second producer, against AGENTS.md #0.1',
179+
file: '../package-schema.ts',
180+
anchor: 'en-US deliberately has NO entry',
181+
},
182+
]);
183+
146184
const familyKeys = (prefix: string): string[] => Object.keys(EN).filter((key) => key.startsWith(prefix));
147185

148186
describe('moved designer rows answer from the studio namespace (objectstack#25922)', () => {
@@ -173,3 +211,45 @@ describe('moved designer rows answer from the studio namespace (objectstack#2592
173211
expect(keys.filter((key) => tOptional(key, 'en-US') !== undefined)).toEqual([]);
174212
});
175213
});
214+
215+
describe('the namespace is the only home (objectstack#25922 slice S7)', () => {
216+
const here = path.dirname(fileURLToPath(import.meta.url));
217+
const zhOnly = Object.keys(ZH).filter((key) => !(key in EN));
218+
219+
it('every namespace key answers through t() and tOptional() with its catalogue value', () => {
220+
// Every key, not a family sample: with the table gone nothing else may
221+
// answer, so a lookup in front of the namespace shows up here by key.
222+
expect(Object.keys(EN).length).toBeGreaterThan(3000);
223+
const wrong = Object.keys(EN).flatMap((key) => {
224+
const out: string[] = [];
225+
const zh = ZH[key] ?? EN[key];
226+
for (const locale of ['en-US', 'en', 'de-DE', undefined]) {
227+
if (t(key, locale) !== EN[key]) out.push(`t ${locale} ${key}`);
228+
if (tOptional(key, locale) !== EN[key]) out.push(`tOptional ${locale} ${key}`);
229+
}
230+
for (const locale of ['zh-CN', 'zh', 'zh-TW']) {
231+
if (t(key, locale) !== zh) out.push(`t ${locale} ${key}`);
232+
if (tOptional(key, locale) !== zh) out.push(`tOptional ${locale} ${key}`);
233+
}
234+
return out;
235+
});
236+
expect(wrong).toEqual([]);
237+
});
238+
239+
it('the zh-only rows are the documented families, and answer in zh alone', () => {
240+
// 54 at the move (9 package form help rows, 41 flow node rows, 4 flow enum
241+
// values); all of them belong to a family with a reason below.
242+
expect(zhOnly.length).toBeGreaterThanOrEqual(50);
243+
expect(zhOnly.filter((key) => !ZH_ONLY_REASONS.some((f) => key.startsWith(f.prefix)))).toEqual([]);
244+
const wrong = zhOnly.flatMap((key) => [
245+
...['en-US', 'en', 'de-DE', undefined].filter((locale) => tOptional(key, locale) !== undefined).map((l) => `en-side ${l} ${key}`),
246+
...['zh-CN', 'zh', 'zh-TW'].filter((locale) => tOptional(key, locale) !== ZH[key]).map((l) => `zh-side ${l} ${key}`),
247+
]);
248+
expect(wrong).toEqual([]);
249+
});
250+
251+
it.each(ZH_ONLY_REASONS.map((f) => [f.prefix, f] as const))('%s: its citation anchor occurs exactly once', (_prefix, family) => {
252+
const text = readFileSync(path.join(here, family.file), 'utf8');
253+
expect(text.split(family.anchor).length - 1).toBe(1);
254+
});
255+
});

‎ui/packages/app-shell/src/views/metadata-admin/previews/__tests__/block-config-i18n.test.ts‎

Lines changed: 12 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -517,9 +517,9 @@ describe('placeholders that must NOT be translated (#3979)', () => {
517517
* own chrome key list rather than hand-copying it.
518518
*
519519
* Since objectstack#25922 slice S2 the prefix lives in the `studio` namespace
520-
* of `@object-ui/i18n` (`packages/i18n/src/namespaces/studio/{en,zh}.ts`), so
521-
* a locale's key set is read from BOTH homes — the app-shell table and the
522-
* namespace catalogue — and a key is retired only when it has left both.
520+
* of `@object-ui/i18n` (`packages/i18n/src/namespaces/studio/{en,zh}.ts`), and
521+
* since slice S7 the app-shell table is gone, so a locale's key set is read
522+
* from its namespace catalogue alone.
523523
*
524524
* Measured on this checkout: every one of the 171 `engine.inspector.pageBlock.*`
525525
* keys in each table is covered by one of those two, and no key is constructed
@@ -574,13 +574,10 @@ interface EngineStringTable {
574574
name: string;
575575
/** Every key the table declares, in file order. */
576576
keys: string[];
577-
/** 1-based line range of the object literal, for the disjointness guard. */
578-
from: number;
579-
to: number;
580577
}
581578

582579
/**
583-
* The KEYS of one `ENGINE_STRINGS_*` table, read out of the source by LINE
580+
* The KEYS of one `STUDIO_*` catalogue, read out of the source by LINE
584581
* RANGE rather than by scanning the whole file.
585582
*
586583
* Table membership is the whole question here: a key that MOVED from one locale
@@ -601,7 +598,7 @@ function engineStringTable(source: string, name: string): EngineStringTable {
601598
const m = /^ {2}'((?:[^'\\]|\\.)*)':/.exec(lines[i]);
602599
if (m) keys.push(m[1]);
603600
}
604-
return { name, keys, from: open + 1, to: close + 1 };
601+
return { name, keys };
605602
}
606603

607604
/**
@@ -646,17 +643,8 @@ function shippedLiteralKeys(): Map<string, string[]> {
646643
}
647644

648645
const I18N_SOURCE = readFileSync(I18N_SOURCE_PATH, 'utf8');
649-
const EN_TABLE = engineStringTable(I18N_SOURCE, 'ENGINE_STRINGS_EN');
650-
const ZH_TABLE = engineStringTable(I18N_SOURCE, 'ENGINE_STRINGS_ZH');
651-
const EN_HOME = engineStringTable(readFileSync(STUDIO_HOME_PATH.en, 'utf8'), 'STUDIO_EN');
652-
const ZH_HOME = engineStringTable(readFileSync(STUDIO_HOME_PATH.zh, 'utf8'), 'STUDIO_ZH');
653-
654-
/** One locale's keys across both homes: the app-shell table and the namespace. */
655-
function bothHomes(table: EngineStringTable, home: EngineStringTable): EngineStringTable {
656-
return { name: `${table.name} + ${home.name}`, keys: [...table.keys, ...home.keys], from: table.from, to: table.to };
657-
}
658-
const EN_KEYS = bothHomes(EN_TABLE, EN_HOME);
659-
const ZH_KEYS = bothHomes(ZH_TABLE, ZH_HOME);
646+
const EN_KEYS = engineStringTable(readFileSync(STUDIO_HOME_PATH.en, 'utf8'), 'STUDIO_EN');
647+
const ZH_KEYS = engineStringTable(readFileSync(STUDIO_HOME_PATH.zh, 'utf8'), 'STUDIO_ZH');
660648

661649
/** The keys `SITES` implies — the complete BLOCK_CONFIG-derived vocabulary. */
662650
const DERIVED = new Set(SITES.map((s) => s.derived));
@@ -682,26 +670,15 @@ function pageBlockKeys(table: EngineStringTable): string[] {
682670
}
683671

684672
describe('a retired key leaves BOTH locale tables (objectui#8368)', () => {
685-
it('reads two disjoint tables out of i18n.ts, both emptied into the namespace', () => {
673+
it('reads both locale catalogues out of the studio namespace, the app-shell table retired', () => {
686674
// The extractor is a source parser, so its silent failure mode is finding
687675
// FEWER keys — which would make every assertion below vacuously green over
688-
// an empty corpus. Ranges first: overlapping ones would mean the `};`
689-
// scan ran past a table and both sets became the same set.
690-
// The table drained slice by slice into the namespace (objectstack#25922)
691-
// and is empty since slice S6b: a key back in it would shadow the
692-
// namespace's copy. The 3,000-key floor, held over both homes together
693-
// below, is what keeps an empty table from reading as a parser gone quiet.
694-
expect(EN_TABLE.to).toBeLessThan(ZH_TABLE.from);
695-
expect(EN_TABLE.keys).toEqual([]);
696-
expect(ZH_TABLE.keys).toEqual([]);
676+
// an empty corpus. The table drained slice by slice into the namespace
677+
// (objectstack#25922) and slice S7 deleted it: i18n.ts declaring one again
678+
// would be a second home this census cannot see.
679+
expect(I18N_SOURCE).not.toMatch(/\bconst ENGINE_STRINGS_/);
697680
// No duplicates: a key declared twice would be a silent overwrite, and the
698681
// set arithmetic below would hide it.
699-
expect(EN_TABLE.keys.length).toBe(new Set(EN_TABLE.keys).size);
700-
expect(ZH_TABLE.keys.length).toBe(new Set(ZH_TABLE.keys).size);
701-
// The namespace homes are read the same way, and a key in both homes of
702-
// one locale would be counted twice below.
703-
expect(EN_HOME.keys.length).toBeGreaterThan(150);
704-
expect(ZH_HOME.keys.length).toBeGreaterThan(150);
705682
expect(EN_KEYS.keys.length).toBe(new Set(EN_KEYS.keys).size);
706683
expect(ZH_KEYS.keys.length).toBe(new Set(ZH_KEYS.keys).size);
707684
expect(EN_KEYS.keys.length).toBeGreaterThan(3000);

0 commit comments

Comments
 (0)