Skip to content

Commit 18f4d0f

Browse files
committed
test(ui): S2 inspector rows read from the studio namespace — pin, zh preloads, both-homes extractor
The page block, action, dataset, flow node, app nav and widget suites that assert zh synchronously make the zh catalogue resident first; the pageBlock retirement gate reads its keys from both homes; a per-family pin holds t() to the namespace value in en, zh and a fallback language. Patch changeset for @object-ui/i18n. Claude-Session: https://claude.ai/code/session_01JQwxbX9LnJhjTQPJEmoBYi Co-authored-by: Claude <noreply@anthropic.com>
1 parent a35ae44 commit 18f4d0f

11 files changed

Lines changed: 159 additions & 14 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+
feat(i18n): the Studio inspectors' page block, action, dataset, flow node, app navigation and dashboard widget strings move into the `studio` namespace
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Second slice of the move ruled on objectstack#25922** (6431032267, Q2 = A; kept by 6431234244). The 520 keys under `engine.inspector.pageBlock.*`, `engine.inspector.action.*`, `engine.inspector.dataset.*`, `engine.inspector.flowNode.*`, `engine.inspector.appNav.*` and `engine.inspector.widget.*` now live in `@object-ui/i18n/namespaces/studio` instead of `@object-ui/app-shell`'s private table, text unchanged in both `en` and `zh`. Studio reads the same strings; a Chinese viewer sees these labels in English for the moment the `zh` catalogue takes to load, as with the first slice. No export changes.

‎ui/packages/app-shell/src/views/metadata-admin/inspectors/DashboardWidgetInspector.test.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ vi.mock('../previews/useDatasetCatalog', () => ({
3131
}));
3232

3333
import { DashboardWidgetInspector } from './DashboardWidgetInspector';
34+
// The inspector's `engine.inspector.*` rows live in `@object-ui/i18n`'s `studio`
35+
// namespace, whose `zh` catalogue is fetched on demand (objectstack#25922).
36+
// Resident before any case, so the zh rows below read synchronously.
37+
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
38+
await loadStudioCatalogue('zh');
3439

3540
afterEach(cleanup);
3641

‎ui/packages/app-shell/src/views/metadata-admin/inspectors/DatasetDefaultInspector.firstPickKeepsCount-12848.test.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ import { registerDefaultMetadataSchemas } from '../default-schemas';
8080
import { registerBuiltinPreviews } from '../previews';
8181
import { registerBuiltinInspectors } from '.';
8282
import { DatasetDefaultInspector, objectChangePatch } from './DatasetDefaultInspector';
83+
// The inspector's `engine.inspector.*` rows live in `@object-ui/i18n`'s `studio`
84+
// namespace, whose `zh` catalogue is fetched on demand (objectstack#25922).
85+
// Resident before any case, so the zh rows below read synchronously.
86+
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
87+
await loadStudioCatalogue('zh');
8388

8489
const DATASET_JSON_SCHEMA = z.toJSONSchema(getMetadataTypeSchema('dataset') as z.ZodType, {
8590
io: 'input',

‎ui/packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.renameNode-11827.test.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ import { FlowNodeInspector } from './FlowNodeInspector';
3131
import { encodeNestedNodeId } from './flow-nested-selection';
3232
import { t, tFormat, type SupportedLocale } from '../i18n';
3333
import type { MetadataSelection } from '../preview-registry';
34+
// The inspector's `engine.inspector.*` rows live in `@object-ui/i18n`'s `studio`
35+
// namespace, whose `zh` catalogue is fetched on demand (objectstack#25922).
36+
// Resident before any case, so the zh rows below read synchronously.
37+
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
38+
await loadStudioCatalogue('zh');
3439

3540
afterEach(cleanup);
3641

‎ui/packages/app-shell/src/views/metadata-admin/inspectors/PageBlockInspector.colorLabelling.test.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ const state = vi.hoisted(() => ({
4545
vi.mock('../useMetadata', () => ({ useMetadataClient: () => state.metadataClient }));
4646

4747
import { PageBlockInspector } from './PageBlockInspector';
48+
// The inspector's `engine.inspector.*` rows live in `@object-ui/i18n`'s `studio`
49+
// namespace, whose `zh` catalogue is fetched on demand (objectstack#25922).
50+
// Resident before any case, so the zh rows below read synchronously.
51+
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
52+
await loadStudioCatalogue('zh');
4853

4954
afterEach(cleanup);
5055

‎ui/packages/app-shell/src/views/metadata-admin/inspectors/PageBlockInspector.i18n.test.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ vi.mock('../useMetadata', () => ({
4848
}));
4949

5050
import { PageBlockInspector } from './PageBlockInspector';
51+
// The inspector's `engine.inspector.*` rows live in `@object-ui/i18n`'s `studio`
52+
// namespace, whose `zh` catalogue is fetched on demand (objectstack#25922).
53+
// Resident before any case, so the zh rows below read synchronously.
54+
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
55+
await loadStudioCatalogue('zh');
5156

5257
afterEach(cleanup);
5358

‎ui/packages/app-shell/src/views/metadata-admin/inspectors/PageBlockInspector.pageHeaderBreadcrumb-11173.test.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ vi.mock('../useMetadata', () => ({
4141
}));
4242

4343
import { PageBlockInspector } from './PageBlockInspector';
44+
// The inspector's `engine.inspector.*` rows live in `@object-ui/i18n`'s `studio`
45+
// namespace, whose `zh` catalogue is fetched on demand (objectstack#25922).
46+
// Resident before any case, so the zh rows below read synchronously.
47+
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
48+
await loadStudioCatalogue('zh');
4449

4550
afterEach(cleanup);
4651

‎ui/packages/app-shell/src/views/metadata-admin/inspectors/PageBlockInspector.visibleWhen.test.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ import { SchemaRenderer, PredicateScopeProvider } from '@object-ui/react';
3434
import { PageBlockInspector } from './PageBlockInspector';
3535
import type { MetadataInspectorProps } from '../inspector-registry';
3636
import type { SupportedLocale } from '../i18n';
37+
// The inspector's `engine.inspector.*` rows live in `@object-ui/i18n`'s `studio`
38+
// namespace, whose `zh` catalogue is fetched on demand (objectstack#25922).
39+
// Resident before any case, so the zh rows below read synchronously.
40+
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
41+
await loadStudioCatalogue('zh');
3742

3843
afterEach(cleanup);
3944

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* objectstack#25922 slice S2 — the page block, action, dataset, flow node, app
5+
* navigation and dashboard widget inspector rows (`engine.inspector.{pageBlock,
6+
* action,dataset,flowNode,appNav,widget}.*`) live in `@object-ui/i18n`'s
7+
* `studio` namespace, and app-shell's `t()` still answers every one of them the
8+
* way it did when they sat in its own table.
9+
*
10+
* What would break it:
11+
* - a row copied back into the app-shell table with a different value — the
12+
* table is asked first, so it would shadow the namespace silently;
13+
* - the namespace fallback dropped from `lookupString` — every moved key
14+
* would echo back as itself;
15+
* - a family emptied out of the namespace, which every per-key assertion
16+
* below would then pass over nothing. The floors are per FAMILY, by name,
17+
* so one family draining cannot hide behind another one growing.
18+
*
19+
* The oracle is the namespace's own catalogues, read through
20+
* `loadStudioCatalogue` (the module's public door), not a copy of their text.
21+
*/
22+
import { describe, expect, it } from 'vitest';
23+
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
24+
import { t } from '../i18n';
25+
26+
const EN = await loadStudioCatalogue('en');
27+
// Resident before any case, so the zh lookups below read synchronously.
28+
const ZH = await loadStudioCatalogue('zh');
29+
30+
/** Family → the fewest keys it may carry (measured at the move, rounded down). */
31+
const S2_FAMILY_FLOOR: Readonly<Record<string, number>> = Object.freeze({
32+
pageBlock: 160,
33+
action: 120,
34+
dataset: 80,
35+
flowNode: 40,
36+
appNav: 40,
37+
widget: 40,
38+
});
39+
40+
const familyKeys = (family: string): string[] =>
41+
Object.keys(EN).filter((key) => key.startsWith(`engine.inspector.${family}.`));
42+
43+
describe('S2 inspector rows answer from the studio namespace (objectstack#25922)', () => {
44+
it.each(Object.entries(S2_FAMILY_FLOOR))('%s: the namespace carries the family, in en and zh', (family, floor) => {
45+
const keys = familyKeys(family);
46+
expect(keys.length).toBeGreaterThanOrEqual(floor);
47+
expect(keys.filter((key) => !(key in ZH))).toEqual([]);
48+
});
49+
50+
it.each(Object.keys(S2_FAMILY_FLOOR))('%s: t() answers every key with the namespace value', (family) => {
51+
const wrong = familyKeys(family).flatMap((key) => {
52+
const out: string[] = [];
53+
if (t(key, 'en-US') !== EN[key]) out.push(`en-US ${key}: ${t(key, 'en-US')}`);
54+
if (t(key, 'zh-CN') !== ZH[key]) out.push(`zh-CN ${key}: ${t(key, 'zh-CN')}`);
55+
// A language the namespace has no catalogue for reads English, as it did
56+
// from the app-shell table.
57+
if (t(key, 'de-DE') !== EN[key]) out.push(`de-DE ${key}: ${t(key, 'de-DE')}`);
58+
return out;
59+
});
60+
expect(wrong).toEqual([]);
61+
});
62+
});

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

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ import path from 'node:path';
8080
import { BLOCK_CONFIG, type BlockPropField, type PlaceholderSpec } from '../block-config';
8181
import { t } from '../../i18n';
8282
import { uiRoot } from '../../../../../../../scripts/ui-root.mjs';
83+
// `engine.inspector.pageBlock.*` lives in `@object-ui/i18n`'s `studio`
84+
// namespace, whose `zh` catalogue is fetched on demand (objectstack#25922).
85+
// Resident before any case, so the zh rows below read synchronously.
86+
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
87+
await loadStudioCatalogue('zh');
8388

8489
const PREFIX = 'engine.inspector.pageBlock';
8590

@@ -474,7 +479,8 @@ describe('placeholders that must NOT be translated (#3979)', () => {
474479
* must LEAVE both tables — and until this describe nothing measured it. The two
475480
* repo-wide i18n gates cannot: `check:i18n-keys` and `check:i18n-dead-keys`
476481
* both read `packages/i18n/src/locales/`, and `engine.inspector.pageBlock.*`
477-
* lives only in `metadata-admin/i18n.ts` (which says so in its own header). So
482+
* lives only in the designer strings (the `studio` namespace since
483+
* objectstack#25922; `metadata-admin/i18n.ts` says so in its own header). So
478484
* each of the four renames so far (objectui#3829, objectui#5212, objectui#8279,
479485
* objectui#8278) shipped with a HAND-WRITTEN pin naming the one key it retired
480486
* — a convention, not an instrument, and the fifth rename is one forgotten pin
@@ -498,6 +504,11 @@ describe('placeholders that must NOT be translated (#3979)', () => {
498504
* of the source, the same way `PageBlockInspector.i18n.test.tsx` reads its
499505
* own chrome key list rather than hand-copying it.
500506
*
507+
* Since objectstack#25922 slice S2 the prefix lives in the `studio` namespace
508+
* of `@object-ui/i18n` (`packages/i18n/src/namespaces/studio/{en,zh}.ts`), so
509+
* a locale's key set is read from BOTH homes — the app-shell table and the
510+
* namespace catalogue — and a key is retired only when it has left both.
511+
*
501512
* Measured on this checkout: every one of the 171 `engine.inspector.pageBlock.*`
502513
* keys in each table is covered by one of those two, and no key is constructed
503514
* dynamically anywhere (`grep` for a template head under this prefix returns
@@ -540,6 +551,11 @@ const I18N_SOURCE_PATH = path.join(
540551
ROOT,
541552
'packages/app-shell/src/views/metadata-admin/i18n.ts',
542553
);
554+
/** The `studio` namespace's catalogues — the prefix's home since objectstack#25922 S2. */
555+
const STUDIO_HOME_PATH = {
556+
en: path.join(ROOT, 'packages/i18n/src/namespaces/studio/en.ts'),
557+
zh: path.join(ROOT, 'packages/i18n/src/namespaces/studio/zh.ts'),
558+
};
543559

544560
interface EngineStringTable {
545561
/** The `const` name, for assertion messages. */
@@ -565,7 +581,7 @@ interface EngineStringTable {
565581
function engineStringTable(source: string, name: string): EngineStringTable {
566582
const lines = source.split('\n');
567583
const open = lines.findIndex((l) => l.startsWith(`const ${name}: Record<string, string> = {`));
568-
if (open === -1) throw new Error(`${name}: declaration not found in i18n.ts — the extractor is stale`);
584+
if (open === -1) throw new Error(`${name}: declaration not found — the extractor is stale`);
569585
const close = lines.findIndex((l, i) => i > open && l === '};');
570586
if (close === -1) throw new Error(`${name}: no column-0 '};' closes the table — the extractor is stale`);
571587
const keys: string[] = [];
@@ -602,6 +618,7 @@ function shippedLiteralKeys(): Map<string, string[]> {
602618
if (!/\.(ts|tsx|js|jsx|mjs)$/.test(entry.name)) continue;
603619
if (/\.(test|spec)\.[tj]sx?$/.test(entry.name)) continue;
604620
if (full === I18N_SOURCE_PATH) continue; // the DEFINITION site, not a reader
621+
if (full === STUDIO_HOME_PATH.en || full === STUDIO_HOME_PATH.zh) continue; // likewise
605622
const content = readFileSync(full, 'utf8');
606623
if (!content.includes(PREFIX)) continue;
607624
for (const m of content.matchAll(/['"`](engine\.inspector\.pageBlock\.[A-Za-z0-9_.:$-]+)['"`]/g)) {
@@ -619,6 +636,15 @@ function shippedLiteralKeys(): Map<string, string[]> {
619636
const I18N_SOURCE = readFileSync(I18N_SOURCE_PATH, 'utf8');
620637
const EN_TABLE = engineStringTable(I18N_SOURCE, 'ENGINE_STRINGS_EN');
621638
const ZH_TABLE = engineStringTable(I18N_SOURCE, 'ENGINE_STRINGS_ZH');
639+
const EN_HOME = engineStringTable(readFileSync(STUDIO_HOME_PATH.en, 'utf8'), 'STUDIO_EN');
640+
const ZH_HOME = engineStringTable(readFileSync(STUDIO_HOME_PATH.zh, 'utf8'), 'STUDIO_ZH');
641+
642+
/** One locale's keys across both homes: the app-shell table and the namespace. */
643+
function bothHomes(table: EngineStringTable, home: EngineStringTable): EngineStringTable {
644+
return { name: `${table.name} + ${home.name}`, keys: [...table.keys, ...home.keys], from: table.from, to: table.to };
645+
}
646+
const EN_KEYS = bothHomes(EN_TABLE, EN_HOME);
647+
const ZH_KEYS = bothHomes(ZH_TABLE, ZH_HOME);
622648

623649
/** The keys `SITES` implies — the complete BLOCK_CONFIG-derived vocabulary. */
624650
const DERIVED = new Set(SITES.map((s) => s.derived));
@@ -656,15 +682,21 @@ describe('a retired key leaves BOTH locale tables (objectui#8368)', () => {
656682
// set arithmetic below would hide it.
657683
expect(EN_TABLE.keys.length).toBe(new Set(EN_TABLE.keys).size);
658684
expect(ZH_TABLE.keys.length).toBe(new Set(ZH_TABLE.keys).size);
685+
// The namespace homes are read the same way, and a key in both homes of
686+
// one locale would be counted twice below.
687+
expect(EN_HOME.keys.length).toBeGreaterThan(150);
688+
expect(ZH_HOME.keys.length).toBeGreaterThan(150);
689+
expect(EN_KEYS.keys.length).toBe(new Set(EN_KEYS.keys).size);
690+
expect(ZH_KEYS.keys.length).toBe(new Set(ZH_KEYS.keys).size);
659691
});
660692

661693
it('the extractor really sees the keys the derivation implies', () => {
662694
// The strongest non-vacuity available: two INDEPENDENT channels for the
663695
// same fact. `SITES` comes from walking BLOCK_CONFIG; these sets come from
664696
// parsing the source text. A parser that silently stopped matching (a
665697
// reformat, a changed indent) fails here by name, not by going quiet.
666-
const enSet = new Set(EN_TABLE.keys);
667-
const zhSet = new Set(ZH_TABLE.keys);
698+
const enSet = new Set(EN_KEYS.keys);
699+
const zhSet = new Set(ZH_KEYS.keys);
668700
expect([...DERIVED].filter((k) => !enSet.has(k))).toEqual([]);
669701
expect([...DERIVED].filter((k) => !zhSet.has(k))).toEqual([]);
670702
expect(DERIVED.size).toBeGreaterThan(150);
@@ -693,8 +725,8 @@ describe('a retired key leaves BOTH locale tables (objectui#8368)', () => {
693725
// name and the fix is to teach the reader set about it, not to delete the
694726
// key. Loud is the correct direction to be wrong in.
695727
expect(
696-
unreachable(EN_TABLE),
697-
'ENGINE_STRINGS_EN keys under this prefix that BLOCK_CONFIG no longer implies and no ' +
728+
unreachable(EN_KEYS),
729+
'en keys under this prefix that BLOCK_CONFIG no longer implies and no ' +
698730
'shipped source asks for — delete them, or teach the reader set about the new reader',
699731
).toEqual([]);
700732
});
@@ -704,19 +736,19 @@ describe('a retired key leaves BOTH locale tables (objectui#8368)', () => {
704736
// sibling `every key resolves in zh-CN` cannot see it — a LEFTOVER key
705737
// resolves perfectly well.
706738
expect(
707-
unreachable(ZH_TABLE),
708-
'ENGINE_STRINGS_ZH keys under this prefix that BLOCK_CONFIG no longer implies and no ' +
739+
unreachable(ZH_KEYS),
740+
'zh keys under this prefix that BLOCK_CONFIG no longer implies and no ' +
709741
'shipped source asks for — the half a rename forgets',
710742
).toEqual([]);
711743
});
712744

713745
it('both tables hold the same pageBlock key set', () => {
714746
// "Left BOTH tables", stated directly. Asymmetry here is the signature of a
715747
// half-applied retirement, and the message names which side kept the key.
716-
const en = new Set(pageBlockKeys(EN_TABLE));
717-
const zh = new Set(pageBlockKeys(ZH_TABLE));
718-
expect([...en].filter((k) => !zh.has(k)), 'in ENGINE_STRINGS_EN but not ZH').toEqual([]);
719-
expect([...zh].filter((k) => !en.has(k)), 'in ENGINE_STRINGS_ZH but not EN').toEqual([]);
748+
const en = new Set(pageBlockKeys(EN_KEYS));
749+
const zh = new Set(pageBlockKeys(ZH_KEYS));
750+
expect([...en].filter((k) => !zh.has(k)), 'in the en homes but not zh').toEqual([]);
751+
expect([...zh].filter((k) => !en.has(k)), 'in the zh homes but not en').toEqual([]);
720752
expect(en.size).toBeGreaterThan(150);
721753
});
722754
});

0 commit comments

Comments
 (0)