Files
2026-07-13 12:22:50 +08:00

193 lines
6.5 KiB
JavaScript

const {execSync} = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');
const SCRIPT_PATH = path.join(__dirname, '..', 'validate-l10n.js');
const LOCALES_DIR = path.join(__dirname, '..', '..', 'src', 'locales');
/**
* Run the validate-l10n.js script against a temporary locale directory.
* This avoids modifying the real locale files (which would cause race conditions
* when Jest runs tests in parallel).
*
* Creates a modified copy of the script that points to the temp directory,
* copies the locale files there, applies any overrides, and runs.
*/
function runWithLocales(overrides = {}) {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'l10n-test-'));
const tmpLocalesDir = path.join(tmpDir, 'locales');
fs.mkdirSync(tmpLocalesDir);
try {
// Copy original locale files to temp dir
for (const filename of [
'en.json',
'fa.json',
'he.json',
'id.json',
'ja.json',
'ko.json',
'ms.json',
'pt_BR.json',
'ru.json',
'uk.json',
'zh.json',
'zh_Hant.json',
]) {
const src = path.join(LOCALES_DIR, filename);
const dest = path.join(tmpLocalesDir, filename);
fs.copyFileSync(src, dest);
}
// Apply overrides
for (const [filename, content] of Object.entries(overrides)) {
const dest = path.join(tmpLocalesDir, filename);
fs.writeFileSync(dest, content, 'utf-8');
}
// Create a modified copy of the script that points to the temp dir
let scriptContent = fs.readFileSync(SCRIPT_PATH, 'utf-8');
scriptContent = scriptContent.replace(
/const LOCALES_DIR = .+;/,
`const LOCALES_DIR = ${JSON.stringify(tmpLocalesDir)};`,
);
scriptContent = scriptContent.replace(
/const EN_PATH = .+;/,
`const EN_PATH = ${JSON.stringify(path.join(tmpLocalesDir, 'en.json'))};`,
);
const tmpScriptPath = path.join(tmpDir, 'validate-l10n.js');
fs.writeFileSync(tmpScriptPath, scriptContent, 'utf-8');
// Run the modified script
try {
const output = execSync(`node "${tmpScriptPath}" 2>&1`, {
encoding: 'utf-8',
timeout: 10000,
});
return {exitCode: 0, output};
} catch (e) {
return {
exitCode: e.status,
output: (e.stdout || '') + (e.stderr || ''),
};
}
} finally {
// Clean up temp directory
fs.rmSync(tmpDir, {recursive: true, force: true});
}
}
describe('validate-l10n.js', () => {
it('passes with valid locale files', () => {
const result = runWithLocales();
expect(result.exitCode).toBe(0);
expect(result.output).toContain('en.json: valid JSON');
expect(result.output).toContain('fa.json: valid JSON');
expect(result.output).toContain('he.json: valid JSON');
expect(result.output).toContain('id.json: valid JSON');
expect(result.output).toContain('ja.json: valid JSON');
expect(result.output).toContain('ko.json: valid JSON');
expect(result.output).toContain('ms.json: valid JSON');
expect(result.output).toContain('pt_BR.json: valid JSON');
expect(result.output).toContain('ru.json: valid JSON');
expect(result.output).toContain('uk.json: valid JSON');
expect(result.output).toContain('zh.json: valid JSON');
expect(result.output).toContain('zh_Hant.json: valid JSON');
expect(result.output).toContain('All l10n files valid');
});
it('reports the number of keys in en.json', () => {
const result = runWithLocales();
expect(result.output).toMatch(/en\.json: \d+ keys/);
});
it('fails on invalid JSON in ja.json', () => {
const result = runWithLocales({
'ja.json': '{ invalid json content',
});
expect(result.exitCode).not.toBe(0);
expect(result.output).toContain('INVALID JSON');
});
it('fails on invalid JSON in zh.json', () => {
const result = runWithLocales({
'zh.json': '{ "unclosed": ',
});
expect(result.exitCode).not.toBe(0);
expect(result.output).toContain('INVALID JSON');
});
it('warns about missing keys in translation files', () => {
const result = runWithLocales({
'ja.json': JSON.stringify({common: {cancel: 'test'}}),
});
// Missing keys are warnings, not errors -- script should still pass
expect(result.exitCode).toBe(0);
expect(result.output).toContain('missing keys');
});
it('fails on placeholder mismatch', () => {
// en.json has storage.lowStorage with {{modelSize}} and {{freeSpace}}
// Create ja.json with a wrong placeholder at that path
const enData = JSON.parse(
fs.readFileSync(path.join(LOCALES_DIR, 'en.json'), 'utf-8'),
);
const jaModified = JSON.parse(JSON.stringify(enData));
jaModified.storage.lowStorage = 'wrong {{wrong}} > {{freeSpace}}';
const result = runWithLocales({
'ja.json': JSON.stringify(jaModified),
});
expect(result.exitCode).not.toBe(0);
expect(result.output).toContain('placeholder mismatch');
});
it('passes when translation file has identical placeholders to en', () => {
const enContent = fs.readFileSync(
path.join(LOCALES_DIR, 'en.json'),
'utf-8',
);
const result = runWithLocales({
'ja.json': enContent,
});
expect(result.exitCode).toBe(0);
expect(result.output).toContain('All l10n files valid');
});
it('falls back to auto-discovery when index.ts is absent', () => {
// Without index.ts in the temp dir, the script falls back to
// filesystem scanning and picks up any .json file present.
const enContent = fs.readFileSync(
path.join(LOCALES_DIR, 'en.json'),
'utf-8',
);
const result = runWithLocales({
'ko.json': enContent,
});
expect(result.exitCode).toBe(0);
expect(result.output).toContain('ko.json: valid JSON');
expect(result.output).toContain('All l10n files valid');
});
it('falls back to auto-discovery and reports errors', () => {
const result = runWithLocales({
'ko.json': '{ invalid json',
});
expect(result.exitCode).not.toBe(0);
expect(result.output).toContain('ko.json');
expect(result.output).toContain('INVALID JSON');
});
it('does not validate en.json as a non-en language file', () => {
// en.json should only appear as the base reference, not as a target
// The auto-discovery filters out en.json from the language list
const result = runWithLocales();
// en.json should appear exactly once as the base, not as a validated language
const enValidLines = result.output
.split('\n')
.filter(line => line.includes('en.json: valid JSON'));
expect(enValidLines).toHaveLength(1);
});
});