dsh-im-ops/test/channels/shared/i18n.test.mjs
2026-08-25 00:41:44 +08:00

131 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import assert from 'node:assert/strict';
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { after, test } from 'node:test';
import {
getImHostLanguage,
setImHostLanguage,
t,
} from '../../../src/channels/shared/i18n.mjs';
import { EN } from '../../../src/channels/shared/i18n-en.mjs';
const here = dirname(fileURLToPath(import.meta.url));
const srcChannelsDir = join(here, '..', '..', '..', 'src', 'channels');
after(() => {
setImHostLanguage('zh');
});
test('host language defaults to Chinese and t() is the identity function', () => {
setImHostLanguage(undefined);
assert.equal(getImHostLanguage(), 'zh');
assert.equal(t('连接成功'), '连接成功');
assert.equal(t('共 {count} 个机器人', { count: 3 }), '共 3 个机器人');
});
test('setImHostLanguage accepts English spellings and falls back to Chinese', () => {
for (const value of ['en', 'EN', 'en-US', ' english ']) {
setImHostLanguage(value);
assert.equal(getImHostLanguage(), 'en', `expected ${value} to select English`);
}
for (const value of [undefined, null, '', 'zh', 'zh-CN', 'fr', 'enabled', 42]) {
setImHostLanguage(value);
assert.equal(getImHostLanguage(), 'zh', `expected ${value} to select Chinese`);
}
});
test('t() translates known keys and fills placeholders in English mode', () => {
setImHostLanguage('en');
const [key] = Object.keys(EN);
if (key) {
assert.equal(t(key), EN[key]);
}
assert.equal(t('未收录的中文'), '未收录的中文');
assert.equal(t('{value} 测试', { value: 'x' }), EN['{value} 测试'] ?? 'x 测试');
assert.equal(
t('示例:先发 /models,再发 /model 2 [推理等级ID]'),
'Example: send /models first, then /model 2 [reasoning effort ID]',
);
assert.equal(t(42), 42);
});
test('every English dictionary entry is a non-empty translation of a Chinese key', () => {
for (const [key, value] of Object.entries(EN)) {
assert.ok(/[一-鿿()]/.test(key), `dictionary key is not a Chinese source string: ${key}`);
assert.ok(typeof value === 'string' && value.trim().length > 0, `empty translation for: ${key}`);
const placeholders = (text) => [...new Set(
[...text.matchAll(/\{([a-zA-Z][a-zA-Z0-9_]*)\}/g)].map((match) => match[1]),
)].sort();
assert.deepEqual(
placeholders(value),
placeholders(key),
`placeholder mismatch for: ${key}`,
);
}
});
test('English dictionary source files do not define duplicate keys', async () => {
const dictionaryDir = join(srcChannelsDir, 'shared', 'i18n-en');
const seen = new Map();
const duplicates = [];
const files = readdirSync(dictionaryDir)
.filter((file) => file.endsWith('.mjs'))
.sort();
for (const file of files) {
const { default: entries } = await import(pathToFileURL(join(dictionaryDir, file)).href);
for (const key of Object.keys(entries)) {
if (seen.has(key)) duplicates.push(`${key}: ${seen.get(key)}, ${file}`);
else seen.set(key, file);
}
}
assert.deepEqual(duplicates, []);
});
test('every literal t() key in src/channels has an English dictionary entry', () => {
const files = [];
const walk = (dir) => {
for (const entry of readdirSync(dir)) {
const path = join(dir, entry);
if (statSync(path).isDirectory()) {
if (entry !== 'i18n-en') walk(path);
} else if (entry.endsWith('.mjs') && entry !== 'i18n.mjs' && entry !== 'i18n-en.mjs') {
files.push(path);
}
}
};
walk(srcChannelsDir);
const keyPattern = /\bt\(\s*(['`])((?:[^\\`'"$]|\$(?!\{)|\\.)*)\1/g;
// Decode the escapes a real string literal would apply, so keys written as
// `...\n...` in source compare against the runtime dictionary key (newline).
const decodeLiteral = (raw) => {
let out = '';
for (let i = 0; i < raw.length; i += 1) {
if (raw[i] !== '\\' || i + 1 >= raw.length) {
out += raw[i];
continue;
}
const next = raw[i + 1];
if (next === 'n') out += '\n';
else if (next === 't') out += '\t';
else if (next === 'r') out += '\r';
else out += next; // \', \", \\, \` and anything else keep the char
i += 1;
}
return out;
};
const missing = [];
for (const file of files) {
const source = readFileSync(file, 'utf8');
for (const match of source.matchAll(keyPattern)) {
const key = decodeLiteral(match[2]);
if (!/[一-鿿]/.test(key)) continue;
if (!Object.hasOwn(EN, key)) {
missing.push(`${file}: ${match[2]}`);
}
}
}
assert.deepEqual(missing, [], 't() keys missing from the English dictionary');
});