Files
kandev/apps/web/scripts/convert-zh-cn-to-zh-hant.test.mjs
T
João Salavisa 5ee5161166 feat(ui): confirm saved view deletion (#3461)
Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-09-07 16:14:58 +01:00

324 lines
12 KiB
JavaScript

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, it } from "vitest";
import { convertWeb } from "./convert-zh-cn-to-zh-hant.mjs";
import {
convertCatalog,
convertMessage,
loadGlossary,
protectLiterals,
residualSimplifiedInCatalog,
TARGET_LOCALES,
} from "./lib/zh-hant-convert.mjs";
const glossary = loadGlossary();
const SOURCE_DISK_HELP = "遍历数据文件夹中的每个目录";
const REVIEWED_DISK_HELP = "遍歷資料目錄中的每個目錄";
const SYSTEM_CATALOG_FILENAME = "system.json";
describe("protectLiterals", () => {
it("restores interpolation and Trans tags after conversion", () => {
const source = "刪除 <1>{{count}}</1> 项";
const { text, restore } = protectLiterals(source);
assert.equal(text.includes("{{count}}"), false);
assert.equal(text.includes("<1>"), false);
assert.equal(restore(text), source);
});
it("keeps brand names intact", () => {
const source = "连接 GitHub 仓库";
const { text, restore } = protectLiterals(source);
assert.match(text, /\uE000\d+\uE001/);
assert.equal(restore(text).includes("GitHub"), true);
});
});
describe("glossary coverage", () => {
it("maps every glossary source and target to the exact regional term", () => {
for (const locale of ["zh-tw", "zh-hk"]) {
for (const row of [...glossary.phrases, ...glossary.tokens]) {
assert.equal(convertMessage(row.from, locale), row[locale], `${locale} source ${row.from}`);
assert.equal(
convertMessage(row[locale], locale),
row[locale],
`${locale} target ${row[locale]}`,
);
}
}
});
it("maps every glossary source before surrounding Chinese text", () => {
for (const locale of TARGET_LOCALES) {
for (const row of [...glossary.phrases, ...glossary.tokens]) {
assert.equal(
convertMessage(`${row.from}了`, locale),
`${row[locale]}了`,
`${locale}: ${row.from}`,
);
}
}
});
});
describe("convertMessage basics", () => {
it("preserves placeholders and Trans tags", () => {
const source = "删除 <1>{{count}}</1> 个任务";
for (const locale of ["zh-tw", "zh-hk"]) {
const out = convertMessage(source, locale);
assert.match(out, /\{\{count\}\}/);
assert.match(out, /<1>/);
assert.match(out, /<\/1>/);
assert.equal(out.includes("{{count}}"), true);
}
});
it("does not rewrite brand names", () => {
const source = "连接 GitHub 并保存设置";
const tw = convertMessage(source, "zh-tw");
const hk = convertMessage(source, "zh-hk");
assert.match(tw, /GitHub/);
assert.match(hk, /GitHub/);
assert.match(tw, /設定|設定/);
assert.match(hk, /設定/);
});
it("applies Taiwan vs Hong Kong glossary divergences", () => {
const software = convertMessage("软件更新", "zh-tw");
const softwareHk = convertMessage("软件更新", "zh-hk");
assert.match(software, /軟體/);
assert.match(softwareHk, /軟件/);
assert.notEqual(software, softwareHk);
const networkTw = convertMessage("网络错误", "zh-tw");
const networkHk = convertMessage("网络错误", "zh-hk");
assert.match(networkTw, /網路/);
assert.match(networkHk, /網絡/);
const projectTw = convertMessage("选择项目", "zh-tw");
const projectHk = convertMessage("选择项目", "zh-hk");
assert.match(projectTw, /專案/);
assert.match(projectHk, /項目/);
});
it("maps product UI verbs via glossary", () => {
assert.equal(convertMessage("登录", "zh-tw"), "登入");
assert.equal(convertMessage("保存", "zh-hk"), "儲存");
assert.equal(convertMessage("设置", "zh-tw"), "設定");
assert.equal(convertMessage("默认", "zh-hk"), "預設");
assert.match(convertMessage("显示语言", "zh-tw"), /顯示語言/);
});
it("uses reviewed local software vocabulary in both regions", () => {
const samples = [
["添加消息队列控件链接并运行", "新增訊息佇列控制項連結並執行"],
["当前智能体配置", "目前代理程式設定檔"],
["发送代码", "傳送程式碼"],
];
for (const locale of ["zh-tw", "zh-hk"]) {
for (const [source, expected] of samples) {
assert.equal(convertMessage(source, locale), expected, `${locale} ${source}`);
}
}
});
});
describe("convertMessage regional vocabulary", () => {
it("distinguishes AI agents from network proxies", () => {
for (const locale of TARGET_LOCALES) {
assert.equal(convertMessage("智能体配置和子智能体", locale), "代理程式設定檔和子代理程式");
assert.equal(convertMessage("智能体工作流", locale), "代理式工作流程");
assert.equal(convertMessage("反向代理客户端", locale), "反向代理用戶端");
assert.equal(convertMessage("代理", locale), "代理");
const catalog = convertCatalog({ agent: "智能体", proxy: "代理" }, locale, {
proxy: "代理伺服器",
});
assert.equal(catalog.agent, "代理程式");
assert.equal(catalog.proxy, "代理伺服器");
}
});
it("normalizes mainland technical vocabulary for Traditional Chinese", () => {
assert.equal(
convertMessage("调用自定义提供商凭据并审阅响应", "zh-tw"),
"呼叫自訂提供者認證資訊並審閱回應",
);
assert.equal(
convertMessage("调用自定义提供商凭据并审阅响应", "zh-hk"),
"呼叫自訂提供者認證資料並審閱回應",
);
assert.equal(convertMessage("磁盘进程", "zh-tw"), "磁碟處理程序");
assert.equal(convertMessage("磁盘进程", "zh-hk"), "磁碟程序");
assert.equal(convertMessage("显式回退标志", "zh-tw"), "明確指定的備援旗標");
assert.equal(convertMessage("显式回退标志", "zh-hk"), "明確指定的後備旗標");
});
it("uses reviewed regional vocabulary in high-traffic UI copy", () => {
assert.equal(convertMessage("生成文本列表并查看反馈", "zh-tw"), "產生文字清單並檢視意見回饋");
assert.equal(convertMessage("生成文本列表并查看反馈", "zh-hk"), "產生文字列表並查看反饋");
assert.equal(convertMessage("禁用组件后查看屏幕", "zh-tw"), "停用元件後檢視螢幕");
assert.equal(convertMessage("禁用组件后查看屏幕", "zh-hk"), "停用組件後查看屏幕");
assert.equal(convertMessage("邮箱字段和变量界面", "zh-tw"), "電子郵件地址欄位和變數介面");
assert.equal(convertMessage("邮箱字段和变量界面", "zh-hk"), "電郵地址欄位和變量介面");
assert.equal(
convertMessage("查看后台克隆历史并了解详情", "zh-tw"),
"檢視後台複製歷史並了解詳情",
);
assert.equal(
convertMessage("查看后台克隆历史并了解详情", "zh-hk"),
"查看後台複製歷史並了解詳情",
);
});
it("uses region pull-request wording", () => {
const tw = convertMessage("打开拉取请求", "zh-tw");
const hk = convertMessage("打开拉取请求", "zh-hk");
assert.match(tw, /提取要求/);
assert.match(hk, /拉取要求/);
});
it("does not rewrite 資料 inside 資料夾 for Hong Kong", () => {
assert.equal(convertMessage("文件夹", "zh-hk"), "資料夾");
assert.equal(convertMessage("资料夹", "zh-hk"), "資料夾");
assert.equal(convertMessage("文件夹", "zh-tw"), "資料夾");
});
it("maps filesystem 文件 to 檔案 in both regions", () => {
assert.equal(convertMessage("文件", "zh-tw"), "檔案");
assert.equal(convertMessage("文件", "zh-hk"), "檔案");
});
it("uses Traditional Chinese corner quotes for UI copy", () => {
assert.equal(convertMessage("点击“启动智能体”", "zh-tw"), "點選「啟動代理程式」");
assert.equal(convertMessage("点击“启动智能体”", "zh-hk"), "點擊「啓動代理程式」");
});
it("keeps the adverb in copy that means only", () => {
for (const locale of TARGET_LOCALES) {
assert.equal(convertMessage("这只会删除", locale), "這只會刪除");
}
});
});
describe("convertMessage idempotence", () => {
it("does not expand glossary targets when converting an existing target", () => {
for (const locale of TARGET_LOCALES) {
const converted = convertMessage("工作流和文件夹", locale);
assert.equal(converted, "工作流程和資料夾");
assert.equal(convertMessage(converted, locale), converted);
}
});
});
describe("convertCatalog", () => {
it("applies reviewed key overrides after mechanical conversion", () => {
const converted = convertCatalog({ diskRefreshHelp: SOURCE_DISK_HELP }, "zh-tw", {
diskRefreshHelp: REVIEWED_DISK_HELP,
});
assert.equal(converted.diskRefreshHelp, REVIEWED_DISK_HELP);
});
it("rejects reviewed overrides that do not match a catalog key", () => {
assert.throws(
() => convertCatalog({ known: "设置" }, "zh-tw", { typo: "設定" }),
/unknown override key.*typo/i,
);
});
});
describe("catalog integrity", () => {
it("reports malformed glossary expansion artifacts", () => {
assert.deepEqual(
residualSimplifiedInCatalog({
workflow: "工作流程程",
profile: "設定設定",
email: "電子電子郵件地址",
repository: "儲存儲存庫",
history: "曆史",
agent: "智能智能体",
healthy: "工作流程",
}),
["workflow", "profile", "email", "repository", "history", "agent"],
);
});
it("rejects invalid generated catalogs before writing any file", () => {
const root = mkdtempSync(path.join(tmpdir(), "kandev-zh-hant-"));
const sourceDir = path.join(root, "zh-cn");
const targetRoot = path.join(root, "output");
mkdirSync(sourceDir, { recursive: true });
writeFileSync(
path.join(sourceDir, SYSTEM_CATALOG_FILENAME),
`${JSON.stringify({ diskRefreshHelp: SOURCE_DISK_HELP })}\n`,
"utf8",
);
try {
assert.throws(
() =>
convertWeb({
locales: ["zh-tw"],
namespace: null,
write: true,
sourceDir,
targetRoot,
}),
/refusing to write/i,
);
assert.equal(existsSync(path.join(targetRoot, "zh-tw", SYSTEM_CATALOG_FILENAME)), false);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it("writes a catalog after a reviewed override clears the integrity issue", () => {
const root = mkdtempSync(path.join(tmpdir(), "kandev-zh-hant-"));
const sourceDir = path.join(root, "zh-cn");
const targetRoot = path.join(root, "output");
mkdirSync(sourceDir, { recursive: true });
writeFileSync(
path.join(sourceDir, SYSTEM_CATALOG_FILENAME),
`${JSON.stringify({ diskRefreshHelp: SOURCE_DISK_HELP })}\n`,
"utf8",
);
try {
convertWeb({
locales: ["zh-tw"],
namespace: null,
write: true,
sourceDir,
targetRoot,
overrides: {
"zh-tw": {
system: { diskRefreshHelp: REVIEWED_DISK_HELP },
},
},
});
const written = JSON.parse(
readFileSync(path.join(targetRoot, "zh-tw", SYSTEM_CATALOG_FILENAME), "utf8"),
);
assert.equal(written.diskRefreshHelp, REVIEWED_DISK_HELP);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it("loads reviewed overrides in the real CLI pipeline", () => {
const result = spawnSync(
process.execPath,
["scripts/convert-zh-cn-to-zh-hant.mjs", "--locale", "all", "--namespace", "system"],
{ cwd: path.resolve("."), encoding: "utf8" },
);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /residual-simplified warnings: web=0 backend=0/);
});
});