From 37238cc17945b59d3c677bf29ca9b31a6c728e10 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 13:46:13 +0000 Subject: [PATCH] test(i18n): give the studio zh-only key families one home The studio namespace pin and the app-shell ZH_ONLY_REASONS pin each held their own copy of the three zh-only prefixes. The list now lives in an i18n test-support module that the namespace pin reads, and the app-shell pin asserts its reason prefixes equal it, so neither list can widen alone. Claude-Session: https://claude.ai/code/session_01JQwxban7wCJnPiiSY4ULYz Co-Authored-By: Claude --- ...inspectorStrings.studioNamespace-25922.test.ts | 8 ++++++++ .../i18n/src/__tests__/studio-namespace.test.ts | 11 +++-------- .../i18n/src/__tests__/studio-zh-only-families.ts | 15 +++++++++++++++ 3 files changed, 26 insertions(+), 8 deletions(-) create mode 100644 ui/packages/i18n/src/__tests__/studio-zh-only-families.ts diff --git a/ui/packages/app-shell/src/views/metadata-admin/inspectors/inspectorStrings.studioNamespace-25922.test.ts b/ui/packages/app-shell/src/views/metadata-admin/inspectors/inspectorStrings.studioNamespace-25922.test.ts index 9624116b9c7..138a6f73a35 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/inspectors/inspectorStrings.studioNamespace-25922.test.ts +++ b/ui/packages/app-shell/src/views/metadata-admin/inspectors/inspectorStrings.studioNamespace-25922.test.ts @@ -29,6 +29,8 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio'; import { t, tOptional } from '../i18n'; +// The i18n pin's own family list (a test-support module, not a package export). +import { STUDIO_ZH_ONLY_FAMILIES } from '../../../../../i18n/src/__tests__/studio-zh-only-families'; const EN = await loadStudioCatalogue('en'); // Resident before any case, so the zh lookups below read synchronously. @@ -152,6 +154,8 @@ const ZH_ONLY_FLOOR: Readonly> = Object.freeze({ * here from the retired `check:i18n-designer-parity` gate with slice S7. A * citation is `{ file, anchor }`, the anchor is TEXT and never a line number, * and it must occur exactly once in its file or it stops locating anything. + * Its prefixes are held equal to `STUDIO_ZH_ONLY_FAMILIES`, the list the i18n + * pin `studio-namespace.test.ts` allows, so neither can widen alone. */ const ZH_ONLY_REASONS = Object.freeze([ { @@ -248,6 +252,10 @@ describe('the namespace is the only home (objectstack#25922 slice S7)', () => { expect(wrong).toEqual([]); }); + it('a reason is given for exactly the families the i18n pin allows', () => { + expect(ZH_ONLY_REASONS.map((f) => f.prefix).sort()).toEqual([...STUDIO_ZH_ONLY_FAMILIES].sort()); + }); + it.each(ZH_ONLY_REASONS.map((f) => [f.prefix, f] as const))('%s: its citation anchor occurs exactly once', (_prefix, family) => { const text = readFileSync(path.join(here, family.file), 'utf8'); expect(text.split(family.anchor).length - 1).toBe(1); diff --git a/ui/packages/i18n/src/__tests__/studio-namespace.test.ts b/ui/packages/i18n/src/__tests__/studio-namespace.test.ts index 2205cb91879..158fccbfe74 100644 --- a/ui/packages/i18n/src/__tests__/studio-namespace.test.ts +++ b/ui/packages/i18n/src/__tests__/studio-namespace.test.ts @@ -30,18 +30,13 @@ import { resolveStudioLanguage, studioString, } from '../namespaces/studio/index.js'; +// The `zh`-only families, each one-sided on purpose; the list's one home. +import { STUDIO_ZH_ONLY_FAMILIES } from './studio-zh-only-families.js'; /** `tFormat()`'s own placeholder pattern (app-shell `views/metadata-admin/i18n.ts`). */ const PLACEHOLDER = /\{(\w+)\}/g; const placeholders = (value: string) => [...new Set(Array.from(value.matchAll(PLACEHOLDER), (m) => m[1]))].sort(); -/** - * The `zh`-only families, each one-sided on purpose. `ZH_ONLY_REASONS` in - * app-shell's `inspectorStrings.studioNamespace-25922.test.ts` carries the - * reason and the consumer for each, and checks that its citation still locates it. - */ -const ZH_ONLY_FAMILIES = ['engine.flowNode.', 'engine.enum.type.', 'engine.packages.form.help.']; - function flatKeys(node: unknown, prefix = ''): string[] { if (typeof node !== 'object' || node === null) return [prefix]; return Object.entries(node).flatMap(([k, v]) => flatKeys(v, prefix ? `${prefix}.${k}` : k)); @@ -69,7 +64,7 @@ describe('studio namespace — scoped parity', () => { it('a zh-only key belongs to a documented family', () => { const stray = zhKeys.filter( - (key) => !(key in STUDIO_EN) && !ZH_ONLY_FAMILIES.some((family) => key.startsWith(family)), + (key) => !(key in STUDIO_EN) && !STUDIO_ZH_ONLY_FAMILIES.some((family) => key.startsWith(family)), ); expect(stray).toEqual([]); }); diff --git a/ui/packages/i18n/src/__tests__/studio-zh-only-families.ts b/ui/packages/i18n/src/__tests__/studio-zh-only-families.ts new file mode 100644 index 00000000000..dfe1a88f141 --- /dev/null +++ b/ui/packages/i18n/src/__tests__/studio-zh-only-families.ts @@ -0,0 +1,15 @@ +/** + * The `studio` namespace's `zh`-only key families, the one list. + * + * Two pins read it: `studio-namespace.test.ts` here, which reds on a `zh`-only + * key outside these prefixes, and app-shell's + * `inspectorStrings.studioNamespace-25922.test.ts`, whose `ZH_ONLY_REASONS` + * carries the reason and consumer for each family and asserts its prefixes + * equal this list. A test-support module, not an export of the package entry: + * `__tests__/` is excluded from the build, so nothing here ships. + */ +export const STUDIO_ZH_ONLY_FAMILIES: readonly string[] = Object.freeze([ + 'engine.flowNode.', + 'engine.enum.type.', + 'engine.packages.form.help.', +]);