|
| 1 | +/** |
| 2 | + * `console.objectView.systemViewReadonly` / `.expandToPage` hold real |
| 3 | + * translations in all ten packs — not English (objectui#3582). |
| 4 | + * |
| 5 | + * This is a different defect class from objectui#3546's ledger. There the key |
| 6 | + * was MISSING from a pack; here the key was PRESENT in all ten and eight of |
| 7 | + * them stored the English string as their "translation". Every existing gate |
| 8 | + * compares key *sets* or placeholder *shape*, so all of them stayed green: |
| 9 | + * |
| 10 | + * - `all-locales-key-parity` — key sets are identical, and neither key |
| 11 | + * interpolates anything, so its placeholder check has nothing to compare. |
| 12 | + * It is green before AND after this fix, by construction; that is exactly |
| 13 | + * why it could not see the bug. |
| 14 | + * - `scripts/check-i18n-call-site-keys.mjs` and its baseline ratchet — both |
| 15 | + * ask whether the key exists in `en`. It does. These two keys were never |
| 16 | + * in the ratchet. |
| 17 | + * |
| 18 | + * `systemViewReadonly` carried the sharper half: the eight packs did not hold |
| 19 | + * `en`'s current sentence, they held a STALE one `en` itself had already |
| 20 | + * abandoned — "duplicate to customize", a workflow the product no longer |
| 21 | + * offers. So eight locales were not merely untranslated, they described a |
| 22 | + * path that does not exist. The `en` pin below is the guard against a repeat: |
| 23 | + * change `en`'s copy again and this file goes red, which forces the eight to |
| 24 | + * be revisited in the same PR instead of drifting for another release. |
| 25 | + */ |
| 26 | +import { describe, it, expect } from 'vitest'; |
| 27 | +import { builtInLocales } from '../locales'; |
| 28 | + |
| 29 | +const LANGS = Object.keys(builtInLocales); |
| 30 | + |
| 31 | +const objectViewOf = (lang: string) => |
| 32 | + (builtInLocales[lang] as any)?.console?.objectView as Record<string, string>; |
| 33 | + |
| 34 | +const readonlyOf = (lang: string) => objectViewOf(lang)?.systemViewReadonly; |
| 35 | +const expandOf = (lang: string) => objectViewOf(lang)?.expandToPage; |
| 36 | + |
| 37 | +/** |
| 38 | + * Both English spellings that must not survive outside `en`: the CURRENT `en` |
| 39 | + * copy, and the STALE sentence the eight packs stored before objectui#3582. |
| 40 | + * Checking only the current one would let the original defect back in |
| 41 | + * verbatim. |
| 42 | + */ |
| 43 | +const EN_CURRENT = { |
| 44 | + systemViewReadonly: 'System view defined in code — read-only.', |
| 45 | + expandToPage: 'Open as full page', |
| 46 | +} as const; |
| 47 | +const EN_STALE_READONLY = 'System view defined in code - duplicate to customize.'; |
| 48 | + |
| 49 | +/** |
| 50 | + * Script ranges for the packs where "is this value in the pack's language?" is |
| 51 | + * mechanically decidable. de/fr/es/pt share the Latin alphabet with `en`, so |
| 52 | + * they get the equality checks below instead — there is no clean positive |
| 53 | + * criterion for them, which is the reason objectui#3582 recorded a repo-wide |
| 54 | + * ASCII gate as a SEPARATE, non-acceptance item rather than folding it in here. |
| 55 | + */ |
| 56 | +const NATIVE_SCRIPT: Record<string, RegExp> = { |
| 57 | + zh: /\p{Script=Han}/u, |
| 58 | + ja: /\p{Script=Hiragana}|\p{Script=Katakana}|\p{Script=Han}/u, |
| 59 | + ko: /\p{Script=Hangul}/u, |
| 60 | + ru: /\p{Script=Cyrillic}/u, |
| 61 | + ar: /\p{Script=Arabic}/u, |
| 62 | +}; |
| 63 | + |
| 64 | +describe('console.objectView value-language coverage (objectui#3582)', () => { |
| 65 | + it('covers all ten built-in packs', () => { |
| 66 | + expect(LANGS).toHaveLength(10); |
| 67 | + }); |
| 68 | + |
| 69 | + it.each(LANGS)('%s defines both keys as non-empty strings', (lang) => { |
| 70 | + for (const key of ['systemViewReadonly', 'expandToPage'] as const) { |
| 71 | + const value = objectViewOf(lang)?.[key]; |
| 72 | + expect(typeof value, `${lang}.console.objectView.${key}`).toBe('string'); |
| 73 | + expect(value!.trim().length, `${lang}.console.objectView.${key} is empty`).toBeGreaterThan(0); |
| 74 | + } |
| 75 | + }); |
| 76 | + |
| 77 | + it('the en pack still says read-only, not "duplicate to customize"', () => { |
| 78 | + // The semantic the other nine were translated against. If this literal |
| 79 | + // changes, the nine translations below are stale by definition — that is |
| 80 | + // the failure this pin exists to make loud, and it is precisely what went |
| 81 | + // unnoticed when `en` was last reworded. |
| 82 | + expect(readonlyOf('en')).toBe(EN_CURRENT.systemViewReadonly); |
| 83 | + expect(expandOf('en')).toBe(EN_CURRENT.expandToPage); |
| 84 | + }); |
| 85 | + |
| 86 | + it('no pack but en serves either English spelling of systemViewReadonly', () => { |
| 87 | + const english = LANGS.filter( |
| 88 | + (l) => |
| 89 | + l !== 'en' && |
| 90 | + (readonlyOf(l) === EN_CURRENT.systemViewReadonly || readonlyOf(l) === EN_STALE_READONLY), |
| 91 | + ); |
| 92 | + expect(english, `packs serving English: ${english.join(', ')}`).toEqual([]); |
| 93 | + }); |
| 94 | + |
| 95 | + it('no pack but en serves the English spelling of expandToPage', () => { |
| 96 | + const english = LANGS.filter((l) => l !== 'en' && expandOf(l) === EN_CURRENT.expandToPage); |
| 97 | + expect(english, `packs serving English: ${english.join(', ')}`).toEqual([]); |
| 98 | + }); |
| 99 | + |
| 100 | + it('the retired "duplicate to customize" workflow is named by no pack', () => { |
| 101 | + // Scoped to these two keys on purpose. The same stale claim still lives in |
| 102 | + // `view.readonlyTooltip`, TRANSLATED into all eight languages — objectui#3625. |
| 103 | + // Widening this regex to the whole pack would red on that one, which is a |
| 104 | + // separate fix; and note no equality-with-English or ASCII heuristic can |
| 105 | + // see it, because those eight values ARE idiomatic translations. They just |
| 106 | + // translate a sentence `en` retired. |
| 107 | + const leaked = LANGS.filter((l) => /duplicate to customi[sz]e/i.test(readonlyOf(l)!)); |
| 108 | + expect(leaked).toEqual([]); |
| 109 | + }); |
| 110 | + |
| 111 | + it.each(Object.keys(NATIVE_SCRIPT))('%s writes both values in its own script', (lang) => { |
| 112 | + const re = NATIVE_SCRIPT[lang]; |
| 113 | + expect(re.test(readonlyOf(lang)!), `${lang} systemViewReadonly has no native script`).toBe(true); |
| 114 | + expect(re.test(expandOf(lang)!), `${lang} expandToPage has no native script`).toBe(true); |
| 115 | + }); |
| 116 | + |
| 117 | + it('all ten packs differ from one another on both keys', () => { |
| 118 | + // The tightest statement of "nobody pasted one language into another". |
| 119 | + // These two sentences are legitimately distinct in all ten today; a future |
| 120 | + // translation that genuinely collides is a reason to relax this line with |
| 121 | + // a note, not to widen it silently. |
| 122 | + for (const [key, read] of [ |
| 123 | + ['systemViewReadonly', readonlyOf], |
| 124 | + ['expandToPage', expandOf], |
| 125 | + ] as const) { |
| 126 | + const values = LANGS.map(read); |
| 127 | + expect(new Set(values).size, `${key} has duplicate values across packs`).toBe(LANGS.length); |
| 128 | + } |
| 129 | + }); |
| 130 | + |
| 131 | + it('expandToPage matches its own pack detail.openAsFullPage, in every pack', () => { |
| 132 | + // `detail.openAsFullPage` is the SAME English sentence in a neighbouring |
| 133 | + // namespace ("Open as full page"), and it was already translated in all |
| 134 | + // ten while `console.objectView.expandToPage` was not. en and zh already |
| 135 | + // held the two byte-identical to each other; this keeps the eight that way |
| 136 | + // too, so the icon button and the expand affordance cannot drift into two |
| 137 | + // different words for one action in a single locale. |
| 138 | + for (const lang of LANGS) { |
| 139 | + expect(expandOf(lang), `${lang} expandToPage vs detail.openAsFullPage`).toBe( |
| 140 | + (builtInLocales[lang] as any).detail.openAsFullPage, |
| 141 | + ); |
| 142 | + } |
| 143 | + }); |
| 144 | +}); |
0 commit comments