/** * i18n key parity tests — ensures `en.json` and `vi.json` stay in sync. * * Rules enforced: * 1. Both files must have the same set of flattened leaf keys. * 2. No key value may be an empty string in either locale. * 3. Locale metadata keys must be present in both files. */ import { describe, expect, it } from 'vitest'; import enMessages from '../messages/en.json'; import viMessages from '../messages/vi.json'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- type MessageDict = Record; function flattenKeys(obj: MessageDict, prefix = ''): string[] { return Object.entries(obj).flatMap(([key, value]) => { const fullKey = prefix ? `${prefix}.${key}` : key; if (value !== null && typeof value === 'object' && !Array.isArray(value)) { return flattenKeys(value as MessageDict, fullKey); } return [fullKey]; }); } function flattenEntries(obj: MessageDict, prefix = ''): Array<[string, string]> { return Object.entries(obj).flatMap(([key, value]) => { const fullKey = prefix ? `${prefix}.${key}` : key; if (value !== null && typeof value === 'object' && !Array.isArray(value)) { return flattenEntries(value as MessageDict, fullKey); } return [[fullKey, String(value)]]; }); } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe('i18n message key parity (en ↔ vi)', () => { const enKeys = new Set(flattenKeys(enMessages)); const viKeys = new Set(flattenKeys(viMessages)); it('vi.json has no extra keys missing from en.json', () => { const extraInVi = [...viKeys].filter((k) => !enKeys.has(k)); expect(extraInVi, `Keys present in vi.json but not en.json: ${JSON.stringify(extraInVi)}`).toHaveLength(0); }); it('en.json has no extra keys missing from vi.json', () => { const extraInEn = [...enKeys].filter((k) => !viKeys.has(k)); expect(extraInEn, `Keys present in en.json but not vi.json: ${JSON.stringify(extraInEn)}`).toHaveLength(0); }); it('en.json has no empty string values', () => { const empty = flattenEntries(enMessages).filter(([, v]) => v.trim() === ''); expect(empty, `Empty values in en.json: ${JSON.stringify(empty.map(([k]) => k))}`).toHaveLength(0); }); it('vi.json has no empty string values', () => { const empty = flattenEntries(viMessages).filter(([, v]) => v.trim() === ''); expect(empty, `Empty values in vi.json: ${JSON.stringify(empty.map(([k]) => k))}`).toHaveLength(0); }); it('both locales define the same top-level namespace keys', () => { const enTopLevel = Object.keys(enMessages).sort(); const viTopLevel = Object.keys(viMessages).sort(); expect(viTopLevel).toEqual(enTopLevel); }); }); describe('i18n locale config', () => { it('language namespace exists in en.json with the expected locale keys', () => { const lang = (enMessages as MessageDict).language as MessageDict | undefined; expect(lang).toBeDefined(); expect(Object.keys(lang ?? {})).toContain('vi'); expect(Object.keys(lang ?? {})).toContain('en'); }); it('language namespace exists in vi.json with the expected locale keys', () => { const lang = (viMessages as MessageDict).language as MessageDict | undefined; expect(lang).toBeDefined(); expect(Object.keys(lang ?? {})).toContain('vi'); expect(Object.keys(lang ?? {})).toContain('en'); }); });