import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { ChromeVisualBrowser, findChrome } from '../bin/visual-check.mjs';
import {
SUPPORTED_LOCALES,
catalogKeys,
translateCount,
translateMessage,
} from '../renderers/shared/i18n.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(__dirname, '..');
const cli = path.join(skillRoot, 'bin/archify.mjs');
const templatePath = path.join(skillRoot, 'assets/template.html');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-i18n-'));
const chromePath = process.env.ARCHIFY_CHROME ? findChrome() : null;
let sequence = 0;
const EXAMPLES = {
architecture: 'web-app.architecture.json',
workflow: 'agent-tool-call.workflow.json',
sequence: 'cache-miss-request.sequence.json',
dataflow: 'product-analytics.dataflow.json',
lifecycle: 'agent-run.lifecycle.json',
};
function example(type) {
return JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', EXAMPLES[type]), 'utf8'));
}
const AUTHORED_TEXT_KEYS = new Set([
'title',
'subtitle',
'label',
'sublabel',
'tag',
'note',
'context',
'responsibility',
'classification',
'step',
]);
function authoredExample(type, locale) {
const document = example(type);
const authored = [];
let authoredIndex = 0;
const nextAuthoredText = () => {
authoredIndex += 1;
const value = locale === 'zh-CN'
? `文案${String(authoredIndex).padStart(2, '0')}`
: `Copy${String(authoredIndex).padStart(2, '0')}`;
authored.push(value);
return value;
};
const rewrite = (value, path = []) => {
if (Array.isArray(value)) {
value.forEach((item, index) => rewrite(item, [...path, index]));
return;
}
if (!value || typeof value !== 'object') return;
for (const [key, child] of Object.entries(value)) {
if (typeof child === 'string' && AUTHORED_TEXT_KEYS.has(key)) {
value[key] = nextAuthoredText();
} else if (key === 'items' && path.includes('cards') && Array.isArray(child)) {
value[key] = child.map((item) => (typeof item === 'string' ? nextAuthoredText() : item));
} else {
rewrite(child, [...path, key]);
}
}
};
rewrite(document);
document.meta.locale = locale;
if (!document.meta.subtitle) document.meta.subtitle = nextAuthoredText();
return { document, authored };
}
function run(type, document, command = 'render') {
const id = sequence++;
const input = path.join(tmp, `${id}-${type}.json`);
const output = path.join(tmp, `${id}-${type}.html`);
fs.writeFileSync(input, JSON.stringify(document));
const args = command === 'render'
? [cli, 'render', type, input, output]
: [cli, 'validate', type, input, '--json'];
const result = spawnSync(process.execPath, args, { cwd: skillRoot, encoding: 'utf8' });
return {
...result,
output,
html: result.status === 0 && command === 'render' ? fs.readFileSync(output, 'utf8') : '',
};
}
async function evaluate(browser, sessionId, expression, awaitPromise = false) {
const response = await browser.cdp.send('Runtime.evaluate', {
expression,
returnByValue: true,
awaitPromise,
}, sessionId);
if (response.exceptionDetails) {
throw new Error(response.exceptionDetails.exception?.description
|| response.exceptionDetails.text
|| 'browser evaluation failed');
}
return response.result?.value;
}
async function loadArtifact(browser, artifactPath) {
const sessionId = await browser.sessionPromise;
await browser.cdp.send('Emulation.setDeviceMetricsOverride', {
width: 1440,
height: 900,
deviceScaleFactor: 1,
mobile: false,
}, sessionId);
const loaded = browser.cdp.waitFor('Page.loadEventFired', sessionId);
const navigation = await browser.cdp.send('Page.navigate', {
url: pathToFileURL(artifactPath).href,
}, sessionId);
if (navigation.errorText) throw new Error(`Chrome navigation failed: ${navigation.errorText}`);
await loaded;
await evaluate(browser, sessionId, `new Promise(function (resolve) {
requestAnimationFrame(function () { requestAnimationFrame(function () { resolve(true); }); });
})`, true);
return sessionId;
}
test('zh-CN localizes renderer-owned output across all five modes without translating authored content', () => {
assert.deepEqual(SUPPORTED_LOCALES, ['en', 'zh-CN']);
for (const type of Object.keys(EXAMPLES)) {
const document = example(type);
const authoredTitle = document.meta.title;
document.meta.locale = 'zh-CN';
delete document.meta.subtitle;
const result = run(type, document);
assert.equal(result.status, 0, `${type}: ${result.stderr || result.stdout}`);
assert.match(result.html, /^\n]*\blang="zh-CN"/);
assert.ok(result.html.includes(`
${authoredTitle}`), `${type}: authored title changed`);
assert.ok(result.html.includes(`${authoredTitle}
`), `${type}: authored heading changed`);
assert.match(result.html, /]*>\u56fe\u4f8b<\/text>/);
assert.match(result.html, /aria-label="\u805a\u7126/);
assert.match(result.html, new RegExp(`\u7531 Archify \u751f\u6210\u7684`));
assert.match(result.html, /"locale":"zh-CN"/);
assert.match(result.html, />\u5bfc\u51fa\u56fe\u8868);
assert.doesNotMatch(result.html, /\{\{i18n:/);
}
});
test('explicit en and zh-CN preserve complete authored field inventories across all five modes', () => {
for (const type of Object.keys(EXAMPLES)) {
const english = authoredExample(type, 'en');
const chinese = authoredExample(type, 'zh-CN');
assert.equal(english.authored.length, chinese.authored.length, `${type}: authored shapes differ`);
assert.ok(english.authored.length >= 10, `${type}: authored inventory is unexpectedly small`);
if (type === 'dataflow') {
assert.ok(
english.authored.includes(english.document.flows[0].classification),
'dataflow: classification is missing from the authored inventory',
);
}
if (type === 'lifecycle') {
assert.ok(
english.authored.includes(english.document.states[0].step),
'lifecycle: step is missing from the authored inventory',
);
}
for (const candidate of [english, chinese]) {
const locale = candidate.document.meta.locale;
const result = run(type, candidate.document);
assert.equal(result.status, 0, `${type}/${locale}: ${result.stderr || result.stdout}`);
assert.match(result.html, new RegExp(`^\\n]*\\blang="${locale}"`));
assert.match(result.html, new RegExp(`"locale":"${locale}"`));
for (const authoredText of candidate.authored) {
assert.ok(result.html.includes(authoredText), `${type}/${locale}: lost authored text ${authoredText}`);
}
if (locale === 'zh-CN') {
assert.ok(result.html.includes(`${candidate.document.meta.title}`), type);
assert.match(result.html, />导出图表);
} else {
assert.ok(result.html.includes(`${candidate.document.meta.title} Diagram`), type);
assert.match(result.html, />Export diagram);
}
}
}
});
test('omitted locale preserves non-English authored content and the English Viewer contract in all five modes', () => {
for (const type of Object.keys(EXAMPLES)) {
const document = example(type);
const authoredTitle = `作者内容-${type}`;
document.meta.title = authoredTitle;
delete document.meta.locale;
delete document.meta.subtitle;
const result = run(type, document);
assert.equal(result.status, 0, `${type}: ${result.stderr || result.stdout}`);
assert.match(result.html, /^\n${authoredTitle} Diagram`), `${type}: authored title changed`);
assert.ok(result.html.includes(`${authoredTitle}
`), `${type}: authored heading changed`);
assert.match(result.html, /