From bc3d0ccdcde2e6fc28275844f7de165e2607ebb1 Mon Sep 17 00:00:00 2001 From: Nicolas Pastorello Date: Mon, 20 Jul 2026 12:58:41 -0300 Subject: [PATCH] test(i18n): guard message catalogue key parity across locales --- src/i18n/messages.test.ts | 45 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/i18n/messages.test.ts diff --git a/src/i18n/messages.test.ts b/src/i18n/messages.test.ts new file mode 100644 index 0000000..8bec6ae --- /dev/null +++ b/src/i18n/messages.test.ts @@ -0,0 +1,45 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +// Locale dictionaries are hand-maintained. English is the source of +// truth (src/i18n/request.ts falls back to en.json only when a whole +// locale file is missing — there is no per-key fallback), so a key +// that lands in en.json and not in a translation renders as a raw +// keypath for users on that locale. This guards the parity. + +const MESSAGES_DIR = join(process.cwd(), 'messages'); +const SOURCE_LOCALE = 'en'; +const TRANSLATED_LOCALES = ['ko']; + +function loadKeys(locale: string): Set { + const raw = readFileSync(join(MESSAGES_DIR, `${locale}.json`), 'utf8'); + const out = new Set(); + const walk = (node: unknown, path: string) => { + if (node && typeof node === 'object' && !Array.isArray(node)) { + for (const [k, v] of Object.entries(node)) { + walk(v, path ? `${path}.${k}` : k); + } + return; + } + out.add(path); + }; + walk(JSON.parse(raw), ''); + return out; +} + +describe('message catalogue parity', () => { + const source = loadKeys(SOURCE_LOCALE); + + it.each(TRANSLATED_LOCALES)('%s.json covers every en.json key', (locale) => { + const translated = loadKeys(locale); + const missing = [...source].filter((k) => !translated.has(k)).sort(); + expect(missing, `${locale}.json is missing these keys`).toEqual([]); + }); + + it.each(TRANSLATED_LOCALES)('%s.json has no orphaned keys', (locale) => { + const translated = loadKeys(locale); + const orphaned = [...translated].filter((k) => !source.has(k)).sort(); + expect(orphaned, `${locale}.json has keys absent from en.json`).toEqual([]); + }); +});