arnaucube
arnaucube/jellyfin-toolsPublic Self-hosted repository
jellyfin-tools/jellyfin-year-review/web/unit/i18n.test.js
jellyfin-year-review/web/unit/i18n.test.jsRaw
 1import { test } from 'node:test';
 2import assert from 'node:assert/strict';
 3import translations from '../src/lib/translations.json' with { type: 'json' };
 4import { t, message } from '../src/lib/i18n.js';
 5
 6// Exercise edits to the same catalog the app imports, restoring it after each test.
 7function withTranslations(values, run) {
 8  const catalog = translations.languages[translations.language];
 9  const original = { ...catalog.strings };
10  Object.assign(catalog.strings, values);
11  try { run(); }
12  finally { catalog.strings = original; }
13}
14
15test('top-level language selects English or Catalan labels and formatting', async () => {
16  const original = translations.language;
17  try {
18    for (const [language, label, formatted] of [['en', 'Sign out', '1.5'], ['ca', 'Tanca la sessió', '1,5']]) {
19      translations.language = language;
20      const active = await import(`../src/lib/i18n.js?selection=${language}`);
21      assert.equal(active.t('Sign out'), label);
22      assert.equal(new Intl.NumberFormat(active.locale).format(1.5), formatted);
23      assert.equal(active.direction, translations.languages[language].direction);
24    }
25  } finally { translations.language = original; }
26});
27
28test('an unknown language reports a configuration error', async () => {
29  const original = translations.language;
30  translations.language = 'missing';
31  try {
32    await assert.rejects(import('../src/lib/i18n.js?selection=missing'), /Unknown translation language: missing/);
33  } finally { translations.language = original; }
34});
35
36test('catalog edits replace labels and allow reordered, repeated placeholders', () => {
37  withTranslations({
38    'Sign out': 'Sair',
39    'You: rank {rank} of {count}': 'Entre {count}, você está em {rank} de {count}'
40  }, () => {
41    assert.equal(t('Sign out'), 'Sair');
42    assert.equal(t('You: rank {rank} of {count}', { rank: 3, count: 6 }), 'Entre 6, você está em 3 de 6');
43  });
44});
45
46test('missing translations fall back to English and inserted data stays literal', () => {
47  assert.equal(t('Unlisted text for {name}', { name: '<b>{year}</b>', year: 2025 }), 'Unlisted text for <b>{year}</b>');
48  assert.equal(t('constructor'), 'constructor');
49  assert.equal(t('Missing {value}'), 'Missing {value}');
50  const catalog = translations.languages[translations.language];
51  const original = catalog.strings['Sign out'];
52  delete catalog.strings['Sign out'];
53  try { assert.equal(t('Sign out'), 'Sign out'); }
54  finally { catalog.strings['Sign out'] = original; }
55});
56
57test('review messages, profiles and counted warnings use translated strings', () => {
58  withTranslations({
59    'Night Owl': 'Coruja noturna',
60    'Unable to sign in with those credentials.': 'Credenciais inválidas.',
61    '{count} records excluded because media metadata was deleted or unavailable.': 'Registros excluídos: {count}.'
62  }, () => {
63    assert.equal(message('Night Owl'), 'Coruja noturna');
64    assert.equal(message('Unable to sign in with those credentials.'), 'Credenciais inválidas.');
65    assert.equal(message('12 records excluded because media metadata was deleted or unavailable.'), 'Registros excluídos: 12.');
66    assert.equal(message('Custom source description'), 'Custom source description');
67    assert.equal(message(undefined), '');
68  });
69});
70
71test('translations preserve all named placeholders', () => {
72  const placeholders = text => [...text.matchAll(/\{\w+\}/g)].map(match => match[0]).sort();
73  for (const [language, catalog] of Object.entries(translations.languages)) {
74    for (const [english, translated] of Object.entries(catalog.strings)) {
75      assert.deepEqual([...new Set(placeholders(translated))], [...new Set(placeholders(english))], `${language}: ${english}`);
76    }
77  }
78});