Files
iptvnator/tools/i18n/check-duplicates.test.mjs
T
4grayandClaude Opus 5.5 afd7a5f331 fix(i18n): translate source cleanup, source health and diagnostics strings (#1884)
* fix(i18n): translate source cleanup, source health and diagnostics strings

Translate every English-identical SOURCE_CLEANUP, SOURCE_HEALTH and
PLAYBACK_DIAGNOSTICS value in all 18 locales, plus the external playback
dock statuses (the same strings as the diagnostic ones), the EPG source
list strings, and leftover English in hu, ko and el. The identical-English
baseline drops 973 entries (2106 -> 1133) and gains none.

Add tools/i18n/check-duplicates.mjs (pnpm run i18n:duplicates), a report
of English strings defined under several keys whose translations differ
per locale, and align drifted wordings: 101 diverging groups -> 65.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(i18n): call cleaned-up zh/zhtw sources invalid, not unactivated

The cleanup dialog lists expired and disabled accounts too, so 未激活 /
未啟用 ("not activated") misexplained why a source is offered for deletion.
Use 失效 ("no longer valid") in the title and the confirmed group heading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(i18n): one zh/zhtw wording for copying the stream URL

The playback diagnostics button said 复制 URL / 複製 URL while the
channel details dialog (new on master) says 复制流 URL / 複製串流網址
for the same action; use the details dialog wording in both places.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:59:50 +02:00

131 lines
3.5 KiB
JavaScript

import assert from 'node:assert/strict';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, test } from 'node:test';
import { findDivergentDuplicates, run } from './check-duplicates.mjs';
const EN = {
CLOSE: 'Close',
DIALOG: { CLOSE: 'Close', SAVE: 'Save' },
MENU: { SAVE: 'Save', TITLE: 'Menu' },
};
let dir;
beforeEach(() => {
dir = mkdtempSync(path.join(os.tmpdir(), 'i18n-duplicates-'));
});
afterEach(() => {
rmSync(dir, { recursive: true, force: true });
});
function writeLocale(locale, content) {
writeFileSync(path.join(dir, `${locale}.json`), JSON.stringify(content));
}
function report(...argv) {
const lines = [];
const code = run({ i18nDir: dir, argv, log: (line) => lines.push(line) });
return { code, output: lines.join('\n') };
}
test('groups keys by English text and keeps only diverging locales', () => {
const en = new Map([
['CLOSE', 'Close'],
['DIALOG.CLOSE', 'Close'],
['DIALOG.SAVE', 'Save'],
['MENU.SAVE', 'Save'],
['MENU.TITLE', 'Menu'],
]);
const locales = new Map([
[
'de',
new Map([
['CLOSE', 'Schließen'],
['DIALOG.CLOSE', 'Schliessen'],
['DIALOG.SAVE', 'Speichern'],
['MENU.SAVE', 'Speichern'],
]),
],
[
'fr',
new Map([
['CLOSE', 'Fermer'],
['DIALOG.CLOSE', 'Fermer'],
['DIALOG.SAVE', 'Save'],
['MENU.SAVE', 'Enregistrer'],
]),
],
]);
const groups = findDivergentDuplicates(en, locales);
assert.deepEqual(
groups.map(({ english, keys, locales: diverging }) => ({
english,
keys,
locales: [...diverging.keys()],
})),
[
{
english: 'Close',
keys: ['CLOSE', 'DIALOG.CLOSE'],
locales: ['de'],
},
{
english: 'Save',
keys: ['DIALOG.SAVE', 'MENU.SAVE'],
locales: ['fr'],
},
]
);
assert.deepEqual(
[...groups[1].locales.get('fr')],
[
['Save', ['DIALOG.SAVE']],
['Enregistrer', ['MENU.SAVE']],
]
);
});
test('reports nothing when every locale agrees', () => {
writeLocale('en', EN);
writeLocale('de', {
CLOSE: 'Schließen',
DIALOG: { CLOSE: 'Schließen', SAVE: 'Speichern' },
MENU: { SAVE: 'Speichern', TITLE: 'Menü' },
});
const { code, output } = report('--max', '0');
assert.equal(code, 0);
assert.match(output, /^ok 0 duplicate English string/m);
});
test('fails only when diverging groups exceed --max', () => {
writeLocale('en', EN);
writeLocale('de', {
CLOSE: 'Schließen',
DIALOG: { CLOSE: 'Schliessen', SAVE: 'Speichern' },
MENU: { SAVE: 'Sichern', TITLE: 'Menü' },
});
assert.equal(report('--max', '2').code, 0);
const { code, output } = report('--max', '1', '--verbose');
assert.equal(code, 1);
assert.match(output, /^"Close": 2 keys, 1 locale\(s\) differ \(de\)$/m);
assert.match(output, /^ {2}de "Schliessen": DIALOG\.CLOSE$/m);
assert.match(output, /^FAIL 2 duplicate English string/m);
});
test('rejects unsupported arguments', () => {
writeLocale('en', EN);
assert.equal(report('--max').code, 1);
assert.equal(report('--strict').code, 1);
});