Files
ai-video-editor/scripts/generate-ui-message-copy.mjs
2026-07-23 11:03:26 +08:00

116 lines
5.0 KiB
JavaScript

import { parse } from "@babel/parser";
import traverseModule from "@babel/traverse";
import { readdir, readFile, stat, writeFile } from "node:fs/promises";
import { join } from "node:path";
const traverse = traverseModule.default;
const LANGUAGES = { zh: "zh-CN", en: "en", ja: "ja", ko: "ko", es: "es", fr: "fr", de: "de", pt: "pt", th: "th", vi: "vi", ru: "ru" };
const messages = new Set();
const USER_MESSAGE_CALL = /^(?:notify|commit|clear|replace|setStatus|setStatusText|reject|onProgress|confirm|alert|Error)/;
function callName(node) {
const callee = node.callee;
if (callee?.type === "Identifier") return callee.name;
if (callee?.type === "MemberExpression") return callee.property?.name ?? callee.property?.value ?? "";
return "";
}
function belongsToUserMessage(path) {
return Boolean(path.findParent((parent) => (parent.isCallExpression() || parent.isNewExpression()) && USER_MESSAGE_CALL.test(callName(parent.node))));
}
async function collectMessages(directory) {
for (const name of await readdir(directory)) {
const path = join(directory, name);
const info = await stat(path);
if (info.isDirectory()) await collectMessages(path);
else if (/\.(?:js|jsx)$/.test(name) && !/\.test\.[^.]+$/.test(name) && !/i18n|ttsText|asr\.js|workers/.test(path)) {
const source = await readFile(path, "utf8");
const ast = parse(source, { sourceType: "module", plugins: ["jsx"] });
traverse(ast, {
StringLiteral(path) {
if (/[\u3400-\u9fff]/u.test(path.node.value) && belongsToUserMessage(path)) messages.add(path.node.value.replace(/\s+/g, " ").trim());
},
TemplateLiteral(path) {
const value = path.node.quasis.map((part, index) => `${part.value.cooked}${index < path.node.expressions.length ? `{${index}}` : ""}`).join("").replace(/\s+/g, " ").trim();
if (/[\u3400-\u9fff]/u.test(value) && belongsToUserMessage(path)) messages.add(value);
},
});
}
}
}
function chunksFor(entries, maximumLength = 900) {
const chunks = [];
let current = [];
let length = 0;
for (const entry of entries) {
const line = `@@${entry.index}@@ ${entry.source}`;
if (current.length && length + line.length + 1 > maximumLength) {
chunks.push(current);
current = [];
length = 0;
}
current.push({ ...entry, line });
length += line.length + 1;
}
if (current.length) chunks.push(current);
return chunks;
}
async function translateChunk(chunk, target) {
const url = new URL("https://translate.googleapis.com/translate_a/single");
url.searchParams.set("client", "gtx");
url.searchParams.set("sl", "zh-CN");
url.searchParams.set("tl", target);
url.searchParams.set("dt", "t");
url.searchParams.set("q", chunk.map(({ line }) => line).join("\n"));
const response = await fetch(url);
if (!response.ok) throw new Error(`Translation failed: ${response.status}`);
const payload = await response.json();
const translated = payload[0].map((part) => part[0]).join("");
const values = new Map();
for (const match of translated.matchAll(/@@(\d+)@@\s*([\s\S]*?)(?=\s*@@\d+@@|$)/g)) values.set(Number(match[1]), match[2].trim());
for (const entry of chunk) if (!values.has(entry.index)) throw new Error(`Missing translated message ${entry.index} for ${target}`);
return values;
}
async function translateText(source, target) {
const url = new URL("https://translate.googleapis.com/translate_a/single");
url.searchParams.set("client", "gtx");
url.searchParams.set("sl", "zh-CN");
url.searchParams.set("tl", target);
url.searchParams.set("dt", "t");
url.searchParams.set("q", source);
const response = await fetch(url);
if (!response.ok) throw new Error(`Translation failed: ${response.status}`);
const payload = await response.json();
return payload[0].map((part) => part[0]).join("").trim();
}
await collectMessages(new URL("../src", import.meta.url).pathname);
const entries = [...messages].sort().map((source, index) => ({ index, source }));
const output = {};
for (const [language, target] of Object.entries(LANGUAGES)) {
output[language] = {};
if (language === "zh") {
for (const entry of entries) output[language][entry.source] = entry.source;
} else {
for (const chunk of chunksFor(entries)) {
const translated = await translateChunk(chunk, target);
for (const entry of chunk) output[language][entry.source] = translated.get(entry.index);
}
const templates = entries.filter(({ source }) => /\{\d+\}/.test(source));
for (let index = 0; index < templates.length; index += 6) {
const batch = templates.slice(index, index + 6);
const translated = await Promise.all(batch.map(({ source }) => translateText(source, target)));
batch.forEach((entry, itemIndex) => { output[language][entry.source] = translated[itemIndex]; });
}
}
process.stdout.write(`${language}: ${entries.length} messages\n`);
}
const source = `// Generated by scripts/generate-ui-message-copy.mjs.\nexport const UI_MESSAGE_COPY = ${JSON.stringify(output, null, 2)};\n`;
await writeFile(new URL("../src/i18nMessages.js", import.meta.url), source);