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('卡片已结束,请查看后续消息。'), 'This card has ended. Please check the next message.', ); assert.equal( t('工具调用「{name}」未成功,请检查工具配置或稍后重试。', { name: 'search' }), 'Tool call "search" did not succeed. Check the tool configuration or try again later.', ); 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'); });