import { test } from 'node:test'; import assert from 'node:assert/strict'; import translations from '../src/lib/translations.json' with { type: 'json' }; import { t, message } from '../src/lib/i18n.js'; // Exercise edits to the same catalog the app imports, restoring it after each test. function withTranslations(values, run) { const catalog = translations.languages[translations.language]; const original = { ...catalog.strings }; Object.assign(catalog.strings, values); try { run(); } finally { catalog.strings = original; } } test('top-level language selects English or Catalan labels and formatting', async () => { const original = translations.language; try { for (const [language, label, formatted] of [['en', 'Sign out', '1.5'], ['ca', 'Tanca la sessió', '1,5']]) { translations.language = language; const active = await import(`../src/lib/i18n.js?selection=${language}`); assert.equal(active.t('Sign out'), label); assert.equal(new Intl.NumberFormat(active.locale).format(1.5), formatted); assert.equal(active.direction, translations.languages[language].direction); } } finally { translations.language = original; } }); test('an unknown language reports a configuration error', async () => { const original = translations.language; translations.language = 'missing'; try { await assert.rejects(import('../src/lib/i18n.js?selection=missing'), /Unknown translation language: missing/); } finally { translations.language = original; } }); test('catalog edits replace labels and allow reordered, repeated placeholders', () => { withTranslations({ 'Sign out': 'Sair', 'You: rank {rank} of {count}': 'Entre {count}, você está em {rank} de {count}' }, () => { assert.equal(t('Sign out'), 'Sair'); assert.equal(t('You: rank {rank} of {count}', { rank: 3, count: 6 }), 'Entre 6, você está em 3 de 6'); }); }); test('missing translations fall back to English and inserted data stays literal', () => { assert.equal(t('Unlisted text for {name}', { name: '{year}', year: 2025 }), 'Unlisted text for {year}'); assert.equal(t('constructor'), 'constructor'); assert.equal(t('Missing {value}'), 'Missing {value}'); const catalog = translations.languages[translations.language]; const original = catalog.strings['Sign out']; delete catalog.strings['Sign out']; try { assert.equal(t('Sign out'), 'Sign out'); } finally { catalog.strings['Sign out'] = original; } }); test('review messages, profiles and counted warnings use translated strings', () => { withTranslations({ 'Night Owl': 'Coruja noturna', 'Unable to sign in with those credentials.': 'Credenciais inválidas.', '{count} records excluded because media metadata was deleted or unavailable.': 'Registros excluídos: {count}.' }, () => { assert.equal(message('Night Owl'), 'Coruja noturna'); assert.equal(message('Unable to sign in with those credentials.'), 'Credenciais inválidas.'); assert.equal(message('12 records excluded because media metadata was deleted or unavailable.'), 'Registros excluídos: 12.'); assert.equal(message('Custom source description'), 'Custom source description'); assert.equal(message(undefined), ''); }); }); test('translations preserve all named placeholders', () => { const placeholders = text => [...text.matchAll(/\{\w+\}/g)].map(match => match[0]).sort(); for (const [language, catalog] of Object.entries(translations.languages)) { for (const [english, translated] of Object.entries(catalog.strings)) { assert.deepEqual([...new Set(placeholders(translated))], [...new Set(placeholders(english))], `${language}: ${english}`); } } });