|
| 1 | +/** |
| 2 | + * The `studio` namespace (objectstack#25922, ruling 6431032267 Q2 = A): its |
| 3 | + * catalogues' own parity, and the two properties the ruling put on its shape. |
| 4 | + * |
| 5 | + * 1. PARITY, scoped to the namespace. Every `en` key has a `zh` row carrying |
| 6 | + * the same `{placeholders}` (the designer's `tFormat()` syntax), and a |
| 7 | + * `zh`-only key belongs to a documented family. This is the namespace's |
| 8 | + * own section of the parity suite: `all-locales-key-parity.test.ts` does |
| 9 | + * not read it — that suite iterates `builtInLocales`, the ten packs, and |
| 10 | + * these catalogues are not packs — and `check:i18n-designer-parity` |
| 11 | + * retires with the app-shell table. |
| 12 | + * 2. NOT IN THE EAGER CATALOGUE. No namespace key is defined by |
| 13 | + * `locales/en.ts`, the console's one eager catalogue, so moving a key here |
| 14 | + * adds nothing to the first load's translation bytes. |
| 15 | + * |
| 16 | + * The lazy runtime — `en` before the fetch, the fetch, the re-render — is |
| 17 | + * `studio-namespace-lazy.test.tsx`'s, in an isolated project: this file runs |
| 18 | + * in the `unit` project, which shares one module graph per worker, so "zh is |
| 19 | + * not resident yet" is not a state it can assume. |
| 20 | + */ |
| 21 | +import { describe, expect, it } from 'vitest'; |
| 22 | + |
| 23 | +import en from '../locales/en.js'; |
| 24 | +import STUDIO_EN from '../namespaces/studio/en.js'; |
| 25 | +import STUDIO_ZH from '../namespaces/studio/zh.js'; |
| 26 | +import { |
| 27 | + STUDIO_LANGUAGE_CODES, |
| 28 | + loadStudioCatalogue, |
| 29 | + resolveStudioLanguage, |
| 30 | + studioString, |
| 31 | +} from '../namespaces/studio/index.js'; |
| 32 | + |
| 33 | +/** `tFormat()`'s own placeholder pattern (app-shell `views/metadata-admin/i18n.ts`). */ |
| 34 | +const PLACEHOLDER = /\{(\w+)\}/g; |
| 35 | +const placeholders = (value: string) => [...new Set(Array.from(value.matchAll(PLACEHOLDER), (m) => m[1]))].sort(); |
| 36 | + |
| 37 | +/** |
| 38 | + * The `zh`-only families the designer table documents, each one-sided on |
| 39 | + * purpose — `ZH_ONLY_FAMILIES` in `ui/scripts/check-i18n-designer-table-parity.mjs` |
| 40 | + * carries the reason and the consumer for each. |
| 41 | + */ |
| 42 | +const ZH_ONLY_FAMILIES = ['engine.flowNode.', 'engine.enum.type.', 'engine.packages.form.help.']; |
| 43 | + |
| 44 | +function flatKeys(node: unknown, prefix = ''): string[] { |
| 45 | + if (typeof node !== 'object' || node === null) return [prefix]; |
| 46 | + return Object.entries(node).flatMap(([k, v]) => flatKeys(v, prefix ? `${prefix}.${k}` : k)); |
| 47 | +} |
| 48 | + |
| 49 | +describe('studio namespace — scoped parity', () => { |
| 50 | + const enKeys = Object.keys(STUDIO_EN); |
| 51 | + const zhKeys = Object.keys(STUDIO_ZH); |
| 52 | + |
| 53 | + it('reads a populated catalogue (the comparisons below are not over nothing)', () => { |
| 54 | + expect(enKeys.length).toBeGreaterThan(0); |
| 55 | + expect(zhKeys.length).toBeGreaterThanOrEqual(enKeys.length); |
| 56 | + }); |
| 57 | + |
| 58 | + it('every en key has a zh row', () => { |
| 59 | + expect(enKeys.filter((key) => !(key in STUDIO_ZH))).toEqual([]); |
| 60 | + }); |
| 61 | + |
| 62 | + it('every shared key carries the same {placeholders} on both sides', () => { |
| 63 | + const mismatched = enKeys |
| 64 | + .filter((key) => key in STUDIO_ZH) |
| 65 | + .filter((key) => placeholders(STUDIO_EN[key]).join(' ') !== placeholders(STUDIO_ZH[key]).join(' ')); |
| 66 | + expect(mismatched).toEqual([]); |
| 67 | + }); |
| 68 | + |
| 69 | + it('a zh-only key belongs to a documented family', () => { |
| 70 | + const stray = zhKeys.filter( |
| 71 | + (key) => !(key in STUDIO_EN) && !ZH_ONLY_FAMILIES.some((family) => key.startsWith(family)), |
| 72 | + ); |
| 73 | + expect(stray).toEqual([]); |
| 74 | + }); |
| 75 | + |
| 76 | + it('every value is a non-empty string', () => { |
| 77 | + for (const catalogue of [STUDIO_EN, STUDIO_ZH]) { |
| 78 | + expect(Object.entries(catalogue).filter(([, v]) => typeof v !== 'string' || v.length === 0)).toEqual([]); |
| 79 | + } |
| 80 | + }); |
| 81 | +}); |
| 82 | + |
| 83 | +describe('studio namespace — outside the eager catalogue', () => { |
| 84 | + it('no namespace key is defined by locales/en.ts', () => { |
| 85 | + const eager = new Set(flatKeys(en)); |
| 86 | + // Lit control: the flattening reaches real pack keys. |
| 87 | + expect(eager.has('common.save')).toBe(true); |
| 88 | + expect(Object.keys(STUDIO_EN).filter((key) => eager.has(key))).toEqual([]); |
| 89 | + }); |
| 90 | +}); |
| 91 | + |
| 92 | +describe('studio namespace — language resolution', () => { |
| 93 | + it('ships en and zh', () => { |
| 94 | + expect([...STUDIO_LANGUAGE_CODES].sort()).toEqual(['en', 'zh']); |
| 95 | + }); |
| 96 | + |
| 97 | + it.each([ |
| 98 | + ['zh-CN', 'zh'], |
| 99 | + ['zh', 'zh'], |
| 100 | + ['ZH-cn', 'zh'], |
| 101 | + ['zh-TW', 'zh'], |
| 102 | + ['en-US', 'en'], |
| 103 | + ['de', 'en'], |
| 104 | + ['ar-EG', 'en'], |
| 105 | + ['', 'en'], |
| 106 | + [undefined, 'en'], |
| 107 | + ])('%s is served by the %s catalogue', (lang, code) => { |
| 108 | + expect(resolveStudioLanguage(lang)).toBe(code); |
| 109 | + }); |
| 110 | + |
| 111 | + it('a resident zh catalogue answers in zh; a third locale answers in en', async () => { |
| 112 | + await loadStudioCatalogue('zh'); |
| 113 | + const key = Object.keys(STUDIO_EN).find((k) => STUDIO_ZH[k] !== STUDIO_EN[k])!; |
| 114 | + expect(studioString(key, 'zh-CN')).toBe(STUDIO_ZH[key]); |
| 115 | + expect(studioString(key, 'de')).toBe(STUDIO_EN[key]); |
| 116 | + expect(studioString(key, 'en-US')).toBe(STUDIO_EN[key]); |
| 117 | + }); |
| 118 | + |
| 119 | + it('a key neither catalogue defines is undefined, never the key', () => { |
| 120 | + expect(studioString('designer.no.such.key', 'zh-CN')).toBeUndefined(); |
| 121 | + expect(studioString('designer.no.such.key', 'de')).toBeUndefined(); |
| 122 | + }); |
| 123 | +}); |
0 commit comments