@alauda-fe/i18n-tools
Version:
基于 Azure OpenAI 的 JSON i18n 文件翻译和英文语法检查工具集
922 lines (778 loc) • 27.5 kB
JavaScript
import fs from "node:fs/promises";
import path from "node:path";
import { AzureOpenAI } from "openai";
import { setTimeout } from "node:timers/promises";
import {
Logger,
Validator,
ErrorHandler,
ProgressTracker,
CONFIG,
} from "./utils.js";
import {
buildTranslationUserPrompt,
buildCustomTranslationSystemPrompt,
getFriendlyLanguageName,
} from "./prompts.js";
// ====================== 初始化工具 ======================
const logger = new Logger("Translate");
const errorHandler = new ErrorHandler(logger, CONFIG.LOGS.TRANSLATE);
// ====================== 转义/反转义 JSON key 中的点 ======================
function escapeKey(seg) {
return seg.replace(/\./g, CONFIG.DOT_ESC);
}
function unescapeKey(seg) {
return seg.replace(new RegExp(CONFIG.DOT_ESC, "g"), ".");
}
// ====================== 工具函数 ======================
// 递归收集所有叶子节点路径,并 escapeKey 转义 "."
// 过滤掉 key 为空字符串或者 value 为空字符串的条目
function collectKeys(obj, prefix = "") {
return Object.entries(obj).flatMap(([key, val]) => {
// 忽略空字符串的 key
if (key === "") {
return [];
}
const esc = escapeKey(key);
const p = prefix ? `${prefix}.${esc}` : esc;
if (val !== null && typeof val === "object") {
return collectKeys(val, p);
}
// 忽略空字符串的 value
if (val === "") {
return [];
}
return [p];
});
}
// 深度删除嵌套对象中的键
function deleteNestedKey(obj, pathStr) {
const segs = pathStr.split(".").map(unescapeKey);
let cur = obj;
for (let i = 0; i < segs.length - 1; i++) {
if (!cur[segs[i]] || typeof cur[segs[i]] !== "object") {
return; // 路径不存在,无需删除
}
cur = cur[segs[i]];
}
delete cur[segs[segs.length - 1]];
// 递归删除空对象
const parent = segs.slice(0, -1);
if (parent.length > 0 && Object.keys(cur).length === 0) {
deleteNestedKey(obj, parent.join("."));
}
}
// 比较两个JSON对象的差异
function compareJsonObjects(newObj, snapshotObj) {
const newKeys = new Set(collectKeys(newObj));
const snapshotKeys = new Set(collectKeys(snapshotObj));
const added = [...newKeys].filter((k) => !snapshotKeys.has(k));
const deleted = [...snapshotKeys].filter((k) => !newKeys.has(k));
const modified = [...newKeys].filter((k) => {
if (!snapshotKeys.has(k)) return false;
const newVal = nestedUtils.get(newObj, k);
const snapshotVal = nestedUtils.get(snapshotObj, k);
return newVal !== snapshotVal;
});
return { added, deleted, modified };
}
// 嵌套对象读写:路径分割后 unescapeKey
const nestedUtils = {
get(obj, pathStr) {
return pathStr
.split(".")
.map(unescapeKey)
.reduce((o, k) => (o || {})[k], obj);
},
set(obj, pathStr, value) {
const segs = pathStr.split(".").map(unescapeKey);
let cur = obj;
while (segs.length > 1) {
const k = segs.shift();
if (!cur[k] || typeof cur[k] !== "object") cur[k] = {};
cur = cur[k];
}
cur[segs[0]] = value;
},
};
// 将数组按指定大小分块
function chunkArray(arr, size) {
const out = [];
for (let i = 0; i < arr.length; i += size) {
out.push(arr.slice(i, i + size));
}
return out;
}
// ====================== 占位符处理 ======================
function placeholderize(text) {
const map = {};
// 支持 {{var}} 和 ${var} 两种模板格式
const parts = text.match(/(\{\{[\s\S]+?\}\}|\$\{[\s\S]+?\})/g) || [];
parts.forEach((p, i) => {
const key = `__PH_${i}__`;
map[key] = p;
text = text.replace(p, key);
});
return { text, map };
}
function restorePlaceholders(text, map) {
Object.entries(map).forEach(([key, orig]) => {
text = text.replace(new RegExp(key, "g"), orig);
});
return text;
}
function encodeBatch(batch) {
const maps = {};
const newBatch = {};
for (const [k, v] of Object.entries(batch)) {
// 忽略空字符串的 key 或 value
if (k === "" || v === "") {
continue;
}
if (typeof v === "string") {
const { text, map } = placeholderize(v);
newBatch[k] = text;
maps[k] = map;
} else {
newBatch[k] = v;
maps[k] = {};
}
}
return { newBatch, maps };
}
function decodeBatch(result, maps) {
const out = {};
for (const [k, v] of Object.entries(result)) {
// 忽略空字符串的 key 或 value
if (k === "" || v === "") {
continue;
}
out[k] = typeof v === "string" ? restorePlaceholders(v, maps[k] || {}) : v;
}
return out;
}
// ====================== 翻译器工厂 ======================
function createTranslator(
openai,
srcLang,
tgtLang,
customPromptPath,
extraRules
) {
// 获取友好的语言名称用于提示词
const srcLangName = getFriendlyLanguageName(srcLang);
const tgtLangName = getFriendlyLanguageName(tgtLang);
async function doRequest(batch) {
await setTimeout(CONFIG.REQUEST_INTERVAL);
// 构建自定义提示词
const systemPrompt = await buildCustomTranslationSystemPrompt(
customPromptPath,
extraRules
);
const payload = {
model: "gpt-4o-mini",
messages: [
{ role: "system", content: systemPrompt },
{
role: "user",
content: buildTranslationUserPrompt(srcLangName, tgtLangName),
},
{ role: "user", content: JSON.stringify(batch) },
],
temperature: 0.2, // 降低随机性,提高一致性
response_format: { type: "json_object" },
};
logger.apiRequest(batch, `翻译 (${srcLang} → ${tgtLang})`);
const resp = await openai.chat.completions.create(payload);
const content = resp.choices[0].message.content;
logger.apiResponse(content);
return content;
}
return async (batch) => {
// 分离需要翻译的条目、注释条目和英文条目
const commentKeys = [];
const englishKeys = [];
const translateBatch = {};
Object.entries(batch).forEach(([k, v]) => {
// 忽略空字符串的 key 或 value
if (k === "" || v === "") {
return;
}
if (typeof v === "string" && v.startsWith("// ")) {
commentKeys.push(k);
} else if (k.endsWith("_en")) {
englishKeys.push(k);
} else {
translateBatch[k] = v;
}
});
// 如果没有需要翻译的条目,直接返回注释条目和英文条目
if (Object.keys(translateBatch).length === 0) {
const commentEntries = commentKeys.map(k => [k, batch[k]]);
const englishEntries = englishKeys.map(k => [k, batch[k]]);
return Object.fromEntries([...commentEntries, ...englishEntries]);
}
const { newBatch, maps } = encodeBatch(translateBatch);
try {
const content = await doRequest(newBatch);
const data = JSON.parse(content);
const decoded = decodeBatch(data, maps);
// 验证模板变量完整性(支持 {{var}} 和 ${var} 格式)
const translatedEntries = Object.entries(translateBatch).map(([k, v]) => {
if (typeof v !== "string") return [k, decoded[k]];
const originalVars = (v.match(/(\{\{.*?\}\}|\$\{.*?\})/g) || [])
.sort()
.join();
const translatedVars = (
(decoded[k] || "").match(/(\{\{.*?\}\}|\$\{.*?\})/g) || []
)
.sort()
.join();
// 如果模板变量不匹配,返回 null 表示翻译失败
return [k, originalVars === translatedVars ? decoded[k] : null];
});
// 合并翻译结果、注释条目和英文条目
const commentEntries = commentKeys.map(k => [k, batch[k]]);
const englishEntries = englishKeys.map(k => [k, batch[k]]);
return Object.fromEntries([...translatedEntries, ...commentEntries, ...englishEntries]);
} catch (err) {
const errorResult = errorHandler.handleApiError(err, () =>
doRequest(newBatch)
);
if (errorResult.shouldRetry && errorResult.retryCallback) {
await setTimeout(errorResult.waitMs || 60000);
try {
const content = await errorResult.retryCallback();
const data = JSON.parse(content);
const decoded = decodeBatch(data, maps);
// 验证模板变量完整性(支持 {{var}} 和 ${var} 格式)
const translatedEntries = Object.entries(translateBatch).map(([k, v]) => {
if (typeof v !== "string") return [k, decoded[k]];
const originalVars = (v.match(/(\{\{.*?\}\}|\$\{.*?\})/g) || [])
.sort()
.join();
const translatedVars = (
(decoded[k] || "").match(/(\{\{.*?\}\}|\$\{.*?\})/g) || []
)
.sort()
.join();
return [k, originalVars === translatedVars ? decoded[k] : null];
});
// 合并翻译结果、注释条目和英文条目
const commentEntries = commentKeys.map(k => [k, batch[k]]);
const englishEntries = englishKeys.map(k => [k, batch[k]]);
return Object.fromEntries([...translatedEntries, ...commentEntries, ...englishEntries]);
} catch {}
}
await errorHandler.logError(err, {
batch: Object.keys(batch),
srcLang,
tgtLang,
});
// 即使翻译失败,也返回注释条目和英文条目
const failedEntries = Object.keys(translateBatch).map(k => [k, null]);
const commentEntries = commentKeys.map(k => [k, batch[k]]);
const englishEntries = englishKeys.map(k => [k, batch[k]]);
return Object.fromEntries([...failedEntries, ...commentEntries, ...englishEntries]);
}
};
}
// ====================== 主翻译函数 ======================
async function translateFiles(
srcLang,
tgtLang,
token,
customPromptPath,
extraRules
) {
// 参数验证
const validatedToken = Validator.validateApiKey(token);
const validatedSrcLang = Validator.validateLanguage(srcLang, "源语言");
const validatedTgtLang = Validator.validateLanguage(tgtLang, "目标语言");
// 验证自定义提示词文件(如果提供)
if (customPromptPath) {
try {
await Validator.validateFile(customPromptPath);
logger.info(`使用自定义提示词: ${path.basename(customPromptPath)}`);
} catch (err) {
throw new Error(`自定义提示词文件无效: ${err.message}`);
}
}
if (extraRules) {
logger.info("应用额外处理规则");
}
const srcDir = await Validator.validateDirectory(validatedSrcLang);
const tgtDir = path.resolve(validatedTgtLang);
// 创建目标目录
try {
await fs.mkdir(tgtDir, { recursive: true });
logger.info(`目标目录已准备: ${path.basename(tgtDir)}`);
} catch (err) {
throw new Error(`无法创建目标目录: ${err.message}`);
}
// 初始化 OpenAI 客户端
const openai = new AzureOpenAI({
endpoint: CONFIG.ENDPOINT,
apiKey: validatedToken,
apiVersion: CONFIG.API_VERSION,
});
const translate = createTranslator(
openai,
validatedSrcLang,
validatedTgtLang,
customPromptPath,
extraRules
);
// 获取需要处理的文件
const files = (await fs.readdir(srcDir)).filter(
(f) => f.endsWith(".json") && f !== "_config.json"
);
if (files.length === 0) {
logger.skip("源目录中没有找到可翻译的 JSON 文件");
return;
}
logger.start(
`开始翻译 ${files.length} 个文件从 ${validatedSrcLang} 到 ${validatedTgtLang}`
);
let totalTranslated = 0;
let totalSkipped = 0;
for (const file of files) {
const srcPath = path.join(srcDir, file);
const tgtPath = path.join(tgtDir, file);
logger.file("read", srcPath);
const srcObj = JSON.parse(await fs.readFile(srcPath, "utf8"));
let tgtObj = {};
try {
tgtObj = JSON.parse(await fs.readFile(tgtPath, "utf8"));
} catch {
logger.info(`目标文件不存在,将创建新文件: ${file}`);
}
const allKeys = collectKeys(srcObj);
const todo = allKeys.filter(
(k) =>
nestedUtils.get(tgtObj, k) === undefined ||
nestedUtils.get(tgtObj, k) === null
);
if (!todo.length) {
logger.skip(`${file} 已完成翻译`);
totalSkipped++;
continue;
}
logger.progress(`处理 ${file}: 需翻译 ${todo.length} 条目`);
const chunks = chunkArray(todo, CONFIG.TRANSLATE_BATCH_SIZE);
const progress = new ProgressTracker(logger, chunks.length);
for (const keys of chunks) {
const batch = {};
keys.forEach((k) => {
batch[k] = nestedUtils.get(srcObj, k);
});
const result = await translate(batch);
const successCount = Object.values(result).filter(
(v) => v !== null
).length;
Object.entries(result).forEach(([k, v]) => nestedUtils.set(tgtObj, k, v));
logger.file("write", tgtPath);
await fs.writeFile(tgtPath, JSON.stringify(tgtObj, null, 2));
progress.update(successCount === Object.keys(batch).length);
totalTranslated += successCount;
}
progress.finish();
logger.success(`${file} 翻译完成`);
}
logger.stats("翻译任务完成统计", {
处理文件数: files.length,
跳过文件数: totalSkipped,
翻译条目数: totalTranslated,
源语言: validatedSrcLang,
目标语言: validatedTgtLang,
});
}
// ====================== 重试失败翻译函数 ======================
export async function retryFailedTranslations(
srcLang,
tgtLang,
token,
customPromptPath,
extraRules
) {
// 参数验证
const validatedToken = Validator.validateApiKey(token);
const validatedSrcLang = Validator.validateLanguage(srcLang, "源语言");
const validatedTgtLang = Validator.validateLanguage(tgtLang, "目标语言");
// 验证自定义提示词文件(如果提供)
if (customPromptPath) {
try {
await Validator.validateFile(customPromptPath);
logger.info(`使用自定义提示词: ${path.basename(customPromptPath)}`);
} catch (err) {
throw new Error(`自定义提示词文件无效: ${err.message}`);
}
}
if (extraRules) {
logger.info("应用额外处理规则");
}
const srcDir = await Validator.validateDirectory(validatedSrcLang);
const tgtDir = await Validator.validateDirectory(validatedTgtLang);
// 初始化 OpenAI 客户端
const openai = new AzureOpenAI({
endpoint: CONFIG.ENDPOINT,
apiKey: validatedToken,
apiVersion: CONFIG.API_VERSION,
});
const translate = createTranslator(
openai,
validatedSrcLang,
validatedTgtLang,
customPromptPath,
extraRules
);
const files = (await fs.readdir(tgtDir)).filter(
(f) => f.endsWith(".json") && f !== "_config.json"
);
if (files.length === 0) {
logger.skip("目标目录中没有找到可重试的 JSON 文件");
return;
}
logger.start(
`开始重试失败的翻译任务 (${validatedSrcLang} → ${validatedTgtLang})`
);
let totalRetried = 0;
let filesProcessed = 0;
for (const file of files) {
const srcPath = path.join(srcDir, file);
const tgtPath = path.join(tgtDir, file);
try {
logger.file("read", srcPath);
const srcObj = JSON.parse(await fs.readFile(srcPath, "utf8"));
let tgtObj = {};
try {
tgtObj = JSON.parse(await fs.readFile(tgtPath, "utf8"));
} catch {
logger.warn(`目标文件读取失败,跳过: ${file}`);
continue;
}
const allKeys = collectKeys(tgtObj);
const failed = allKeys.filter((k) => nestedUtils.get(tgtObj, k) === null);
if (!failed.length) {
logger.skip(`${file} 无失败条目`);
continue;
}
logger.progress(`处理 ${file}: 重试 ${failed.length} 条失败条目`);
const chunks = chunkArray(failed, CONFIG.TRANSLATE_BATCH_SIZE);
const progress = new ProgressTracker(logger, chunks.length);
for (const keys of chunks) {
const batch = {};
keys.forEach((k) => {
batch[k] = nestedUtils.get(srcObj, k);
});
const result = await translate(batch);
const successCount = Object.values(result).filter(
(v) => v !== null
).length;
Object.entries(result).forEach(([k, v]) =>
nestedUtils.set(tgtObj, k, v)
);
logger.file("write", tgtPath);
await fs.writeFile(tgtPath, JSON.stringify(tgtObj, null, 2));
progress.update(successCount === Object.keys(batch).length);
totalRetried += successCount;
}
progress.finish();
logger.success(`${file} 重试完成`);
filesProcessed++;
} catch (err) {
logger.error(`处理文件 ${file} 时出错: ${err.message}`);
await errorHandler.logError(err, { file, operation: "retry" });
}
}
logger.stats("重试任务完成统计", {
检查文件数: files.length,
处理文件数: filesProcessed,
重试成功条目: totalRetried,
源语言: validatedSrcLang,
目标语言: validatedTgtLang,
});
}
// ====================== 增量翻译函数 ======================
export async function incrementalTranslate(
srcLang,
tgtLang,
token,
customPromptPath,
extraRules
) {
// 参数验证
const validatedToken = Validator.validateApiKey(token);
const validatedSrcLang = Validator.validateLanguage(srcLang, "源语言");
const validatedTgtLang = Validator.validateLanguage(tgtLang, "目标语言");
// 验证自定义提示词文件(如果提供)
if (customPromptPath) {
try {
await Validator.validateFile(customPromptPath);
logger.info(`使用自定义提示词: ${path.basename(customPromptPath)}`);
} catch (err) {
throw new Error(`自定义提示词文件无效: ${err.message}`);
}
}
if (extraRules) {
logger.info("应用额外处理规则");
}
// 验证new和snapshot目录
const newDir = path.resolve("new", validatedSrcLang);
const snapshotDir = path.resolve("snapshot", validatedSrcLang);
const tgtDir = path.resolve("src", validatedTgtLang); // 默认翻译目标目录是src下的目标语言
try {
await Validator.validateDirectory(newDir);
} catch (err) {
throw new Error(`new目录不存在或无法访问: ${newDir}`);
}
let snapshotExists = true;
try {
await Validator.validateDirectory(snapshotDir);
} catch (err) {
logger.warn(`snapshot目录不存在,将进行全量翻译: ${snapshotDir}`);
snapshotExists = false;
}
// 创建目标目录
try {
await fs.mkdir(tgtDir, { recursive: true });
logger.info(`翻译目标目录已准备: ${tgtDir}`);
} catch (err) {
throw new Error(`无法创建翻译目标目录: ${err.message}`);
}
// 初始化 OpenAI 客户端
const openai = new AzureOpenAI({
endpoint: CONFIG.ENDPOINT,
apiKey: validatedToken,
apiVersion: CONFIG.API_VERSION,
});
const translate = createTranslator(
openai,
validatedSrcLang,
validatedTgtLang,
customPromptPath,
extraRules
);
// 获取新文件列表
const newFiles = (await fs.readdir(newDir)).filter(
(f) => f.endsWith(".json") && f !== "_config.json"
);
if (newFiles.length === 0) {
logger.skip("new目录中没有找到可翻译的 JSON 文件");
return;
}
logger.start(
`开始增量翻译 ${newFiles.length} 个文件从 ${validatedSrcLang} 到 ${validatedTgtLang}`
);
let totalAdded = 0;
let totalModified = 0;
let totalDeleted = 0;
let filesProcessed = 0;
for (const file of newFiles) {
const newPath = path.join(newDir, file);
const snapshotPath = path.join(snapshotDir, file);
const translationPath = path.join(tgtDir, file); // 翻译文件路径(src/目标语言/)
logger.file("read", newPath);
const newObj = JSON.parse(await fs.readFile(newPath, "utf8"));
let snapshotObj = {};
let translationObj = {};
// 读取snapshot文件(如果存在)
if (snapshotExists) {
try {
snapshotObj = JSON.parse(await fs.readFile(snapshotPath, "utf8"));
} catch {
logger.info(`snapshot文件不存在,视为新文件: ${file}`);
}
}
// 读取翻译文件(如果存在)
try {
translationObj = JSON.parse(await fs.readFile(translationPath, "utf8"));
} catch {
logger.info(`翻译文件不存在,将创建新文件: ${file}`);
}
// 比较new和snapshot的差异
const { added, deleted, modified } = compareJsonObjects(
newObj,
snapshotObj
);
if (added.length === 0 && deleted.length === 0 && modified.length === 0) {
logger.skip(`${file} 无变化`);
continue;
}
logger.progress(
`处理 ${file}: 新增 ${added.length}, 修改 ${modified.length}, 删除 ${deleted.length}`
);
// 处理删除的键
deleted.forEach((key) => {
deleteNestedKey(translationObj, key);
logger.info(`删除翻译条目: ${key}`);
});
// 处理新增和修改的键
const toTranslate = [...added, ...modified];
if (toTranslate.length > 0) {
const chunks = chunkArray(toTranslate, CONFIG.TRANSLATE_BATCH_SIZE);
const progress = new ProgressTracker(logger, chunks.length);
for (const keys of chunks) {
const batch = {};
keys.forEach((k) => {
batch[k] = nestedUtils.get(newObj, k);
});
const result = await translate(batch);
const successCount = Object.values(result).filter(
(v) => v !== null
).length;
Object.entries(result).forEach(([k, v]) => {
if (v !== null) {
nestedUtils.set(translationObj, k, v);
}
});
progress.update(successCount === Object.keys(batch).length);
}
progress.finish();
}
// 保存更新后的翻译文件
logger.file("write", translationPath);
await fs.writeFile(
translationPath,
JSON.stringify(translationObj, null, 2)
);
// 🆕 更新snapshot文件夹
try {
await fs.mkdir(path.dirname(snapshotPath), { recursive: true });
logger.file("update", `快照: ${snapshotPath}`);
await fs.writeFile(snapshotPath, JSON.stringify(newObj, null, 2));
} catch (err) {
logger.warn(`更新快照文件失败: ${err.message}`);
}
totalAdded += added.length;
totalModified += modified.length;
totalDeleted += deleted.length;
filesProcessed++;
logger.success(`${file} 增量翻译完成`);
}
logger.stats("增量翻译任务完成统计", {
处理文件数: filesProcessed,
新增翻译: totalAdded,
修改翻译: totalModified,
删除翻译: totalDeleted,
源语言: validatedSrcLang,
目标语言: validatedTgtLang,
已更新翻译目录: `src/${validatedTgtLang}/`,
已更新快照目录: `snapshot/${validatedSrcLang}/`,
});
// 增量翻译成功后删除new目录
try {
logger.info(`删除已处理的new目录: ${newDir}`);
await fs.rm(newDir, { recursive: true, force: true });
logger.success(`已删除new目录: ${newDir}`);
} catch (err) {
logger.warn(`删除new目录失败: ${err.message}`);
}
}
// ====================== 更新快照函数 ======================
export async function updateSnapshot(srcLang) {
const validatedSrcLang = Validator.validateLanguage(srcLang, "源语言");
const newDir = path.resolve("new", validatedSrcLang);
const snapshotDir = path.resolve("snapshot", validatedSrcLang);
try {
await Validator.validateDirectory(newDir);
} catch (err) {
throw new Error(`new目录不存在或无法访问: ${newDir}`);
}
// 创建snapshot目录
try {
await fs.mkdir(snapshotDir, { recursive: true });
logger.info(`快照目录已准备: ${snapshotDir}`);
} catch (err) {
throw new Error(`无法创建快照目录: ${err.message}`);
}
const files = (await fs.readdir(newDir)).filter(
(f) => f.endsWith(".json") && f !== "_config.json"
);
if (files.length === 0) {
logger.skip("new目录中没有找到可复制的 JSON 文件");
return;
}
logger.start(`更新快照文件: ${files.length} 个文件`);
for (const file of files) {
const srcPath = path.join(newDir, file);
const destPath = path.join(snapshotDir, file);
logger.file("copy", `${srcPath} → ${destPath}`);
const content = await fs.readFile(srcPath, "utf8");
await fs.writeFile(destPath, content);
}
logger.success(`快照更新完成: ${files.length} 个文件`);
}
// ====================== 导出命令函数 ======================
export async function translateCommand(options) {
const { source, target, token, customPrompt, extraRules } = options;
try {
logger.start(`开始翻译任务`);
// 解析 token(支持环境变量)
const { TokenResolver } = await import("./utils.js");
const resolvedToken = TokenResolver.resolveToken(token);
const tokenSource = TokenResolver.getTokenSource(token);
logger.info(`Token 来源: ${tokenSource}`);
await translateFiles(source, target, resolvedToken, customPrompt, extraRules);
logger.finish("翻译任务完成!");
} catch (err) {
logger.error(`翻译任务失败: ${err.message}`);
await errorHandler.logError(err, { command: "translate", options });
process.exit(1);
}
}
export async function retryFailedCommand(options) {
const { source, target, token, customPrompt, extraRules } = options;
try {
logger.start(`开始重试任务`);
// 解析 token(支持环境变量)
const { TokenResolver } = await import("./utils.js");
const resolvedToken = TokenResolver.resolveToken(token);
const tokenSource = TokenResolver.getTokenSource(token);
logger.info(`Token 来源: ${tokenSource}`);
await retryFailedTranslations(
source,
target,
resolvedToken,
customPrompt,
extraRules
);
logger.finish("重试任务完成!");
} catch (err) {
logger.error(`重试任务失败: ${err.message}`);
await errorHandler.logError(err, { command: "retry-failed", options });
process.exit(1);
}
}
export async function incrementalTranslateCommand(options) {
const { source, target, token, customPrompt, extraRules } = options;
try {
logger.start(`开始增量翻译任务`);
// 解析 token(支持环境变量)
const { TokenResolver } = await import("./utils.js");
const resolvedToken = TokenResolver.resolveToken(token);
const tokenSource = TokenResolver.getTokenSource(token);
logger.info(`Token 来源: ${tokenSource}`);
await incrementalTranslate(source, target, resolvedToken, customPrompt, extraRules);
logger.finish("增量翻译任务完成!");
} catch (err) {
logger.error(`增量翻译任务失败: ${err.message}`);
await errorHandler.logError(err, {
command: "incremental-translate",
options,
});
process.exit(1);
}
}
export async function updateSnapshotCommand(options) {
const { source } = options;
try {
logger.start(`开始更新快照任务`);
await updateSnapshot(source);
logger.finish("快照更新任务完成!");
} catch (err) {
logger.error(`快照更新任务失败: ${err.message}`);
await errorHandler.logError(err, { command: "update-snapshot", options });
process.exit(1);
}
}