/** * i18n-io — existing-catalogue reader (fail-closed on malformed JSON) + * atomic writer (tmp + rename, no residue). */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { mkdtempSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { readExistingI18n, writeFileAtomic } from '../i18n-io.js' let root: string beforeEach(() => { root = mkdtempSync(join(tmpdir(), 'i18n-io-')) }) afterEach(() => { rmSync(root, { recursive: true, force: true }) }) function writeLocale(locale: string, module: string, content: string): string { const dir = join(root, 'src', 'i18n', 'locales', locale) mkdirSync(dir, { recursive: true }) const p = join(dir, `${module}.json`) writeFileSync(p, content, 'utf-8') return p } describe('scaffold-component / i18n-io — readExistingI18n', () => { it('returns parsed catalogues only for locales whose file exists', () => { writeLocale('fr', 'crm', JSON.stringify({ contact: { list: { title: 'Contacts' } } })) writeLocale('de', 'crm', JSON.stringify({ contact: { list: { title: 'Kontakte' } } })) const { existingI18n, malformed } = readExistingI18n(root, 'crm') expect(malformed).toEqual([]) expect(Object.keys(existingI18n).sort()).toEqual(['de', 'fr']) expect((existingI18n.fr as { contact: { list: { title: string } } }).contact.list.title).toBe('Contacts') }) it('reports a malformed locale file in malformed[] and never returns it silently', () => { const bad = writeLocale('fr', 'crm', '{ "contact": ') writeLocale('en', 'crm', JSON.stringify({ contact: {} })) const { existingI18n, malformed } = readExistingI18n(root, 'crm') // Fail-closed input: the caller must refuse to write — the historical // silent overwrite wiped whole modules' manual translations. expect(malformed).toEqual([bad]) expect(existingI18n.fr).toBeUndefined() expect(existingI18n.en).toBeDefined() }) it('treats a non-object JSON root (array / string) as malformed', () => { const arr = writeLocale('fr', 'crm', '["not", "a", "catalogue"]') const str = writeLocale('it', 'crm', '"just a string"') const { existingI18n, malformed } = readExistingI18n(root, 'crm') expect(malformed.sort()).toEqual([arr, str].sort()) expect(Object.keys(existingI18n)).toEqual([]) }) it('reads only the requested module, other namespaces untouched', () => { writeLocale('fr', 'crm', JSON.stringify({ contact: {} })) writeLocale('fr', 'billing', JSON.stringify({ invoice: {} })) const { existingI18n } = readExistingI18n(root, 'crm') expect(Object.keys(existingI18n.fr as Record)).toEqual(['contact']) }) }) describe('scaffold-component / i18n-io — writeFileAtomic', () => { it('overwrites in place and leaves no .tmp-* residue', () => { const p = writeLocale('fr', 'crm', '{"old": true}') writeFileAtomic(p, '{"new": true}\n') expect(readFileSync(p, 'utf-8')).toBe('{"new": true}\n') const siblings = readdirSync(join(root, 'src', 'i18n', 'locales', 'fr')) expect(siblings.filter((f) => f.includes('.tmp-'))).toEqual([]) }) it('creates missing parent directories', () => { const p = join(root, 'src', 'i18n', 'locales', 'it', 'crm.json') writeFileAtomic(p, '{}\n') expect(readFileSync(p, 'utf-8')).toBe('{}\n') }) })