hyperchat/scripts/translate.mts

737 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
fs,
argv,
path,
$,
usePowerShell,
os,
} from "zx";
import { fileURLToPath } from "url";
import "dotenv/config";
import { z } from "zod";
import { generateObject, streamObject } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
import { createAnthropic } from '@ai-sdk/anthropic';
import { createGoogleGenerativeAI } from '@ai-sdk/google';
if (os.platform() === "win32") {
usePowerShell();
}
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootDir = path.resolve(__dirname, "../");
const i18nPath = path.resolve(rootDir, "./packages/shared/src/i18n/i18n.json");
// 🎯 HyperChat 多语言专业术语词典
const TERMINOLOGY_DICT = {
zh: {
"Agent": "代理",
"Agents": "代理",
"MCP": "MCP",
"MCP Server": "MCP 服务器",
"MCP Client": "MCP 客户端",
"Workspace": "工作区",
"Workspaces": "工作区",
"Chat": "聊天",
"Model": "模型",
"Provider": "提供商",
"API Key": "API 密钥",
"Task": "任务",
"Tasks": "任务",
"Schedule": "调度",
"Scheduler": "调度器",
"CLI": "命令行工具",
"Command": "命令",
"Template": "模板",
"Gateway": "网关",
"Token": "令牌",
"Prompt": "提示词",
"Temperature": "温度",
"Tool": "工具",
"Tools": "工具",
"Resource": "资源",
"System Prompt": "系统提示词",
"Chat Log": "聊天记录",
"History": "历史记录",
"Session": "会话",
"Configuration": "配置",
"Settings": "设置"
},
ja: {
"Agent": "エージェント",
"Agents": "エージェント",
"MCP": "MCP",
"MCP Server": "MCPサーバー",
"MCP Client": "MCPクライアント",
"Workspace": "ワークスペース",
"Workspaces": "ワークスペース",
"Chat": "チャット",
"Model": "モデル",
"Provider": "プロバイダー",
"API Key": "APIキー",
"Task": "タスク",
"Tasks": "タスク",
"Schedule": "スケジュール",
"Scheduler": "スケジューラー",
"CLI": "コマンドラインツール",
"Command": "コマンド",
"Template": "テンプレート",
"Gateway": "ゲートウェイ",
"Token": "トークン",
"Prompt": "プロンプト",
"Temperature": "温度",
"Tool": "ツール",
"Tools": "ツール",
"Resource": "リソース",
"System Prompt": "システムプロンプト",
"Chat Log": "チャット履歴",
"History": "履歴",
"Session": "セッション",
"Configuration": "設定",
"Settings": "設定"
},
ko: {
"Agent": "에이전트",
"Agents": "에이전트",
"MCP": "MCP",
"MCP Server": "MCP 서버",
"MCP Client": "MCP 클라이언트",
"Workspace": "워크스페이스",
"Workspaces": "워크스페이스",
"Chat": "채팅",
"Model": "모델",
"Provider": "프로바이더",
"API Key": "API 키",
"Task": "작업",
"Tasks": "작업",
"Schedule": "스케줄",
"Scheduler": "스케줄러",
"CLI": "명령줄 도구",
"Command": "명령",
"Template": "템플릿",
"Gateway": "게이트웨이",
"Token": "토큰",
"Prompt": "프롬프트",
"Temperature": "온도",
"Tool": "도구",
"Tools": "도구",
"Resource": "리소스",
"System Prompt": "시스템 프롬프트",
"Chat Log": "채팅 기록",
"History": "히스토리",
"Session": "세션",
"Configuration": "구성",
"Settings": "설정"
},
fr: {
"Agent": "Agent",
"Agents": "Agents",
"MCP": "MCP",
"MCP Server": "Serveur MCP",
"MCP Client": "Client MCP",
"Workspace": "Espace de travail",
"Workspaces": "Espaces de travail",
"Chat": "Chat",
"Model": "Modèle",
"Provider": "Fournisseur",
"API Key": "Clé API",
"Task": "Tâche",
"Tasks": "Tâches",
"Schedule": "Planification",
"Scheduler": "Planificateur",
"CLI": "Outil en ligne de commande",
"Command": "Commande",
"Template": "Modèle",
"Gateway": "Passerelle",
"Token": "Jeton",
"Prompt": "Invite",
"Temperature": "Température",
"Tool": "Outil",
"Tools": "Outils",
"Resource": "Ressource",
"System Prompt": "Invite système",
"Chat Log": "Journal de chat",
"History": "Historique",
"Session": "Session",
"Configuration": "Configuration",
"Settings": "Paramètres"
},
de: {
"Agent": "Agent",
"Agents": "Agenten",
"MCP": "MCP",
"MCP Server": "MCP-Server",
"MCP Client": "MCP-Client",
"Workspace": "Arbeitsbereich",
"Workspaces": "Arbeitsbereiche",
"Chat": "Chat",
"Model": "Modell",
"Provider": "Anbieter",
"API Key": "API-Schlüssel",
"Task": "Aufgabe",
"Tasks": "Aufgaben",
"Schedule": "Zeitplan",
"Scheduler": "Planer",
"CLI": "Kommandozeilen-Tool",
"Command": "Befehl",
"Template": "Vorlage",
"Gateway": "Gateway",
"Token": "Token",
"Prompt": "Eingabeaufforderung",
"Temperature": "Temperatur",
"Tool": "Werkzeug",
"Tools": "Werkzeuge",
"Resource": "Ressource",
"System Prompt": "System-Eingabeaufforderung",
"Chat Log": "Chat-Protokoll",
"History": "Verlauf",
"Session": "Sitzung",
"Configuration": "Konfiguration",
"Settings": "Einstellungen"
}
};
// 🎯 语言配置映射
const LANGUAGE_CONFIG = {
zh: { name: '中文(简体)', nativeName: '中文', region: '中国大陆' },
ja: { name: '日语', nativeName: '日本語', region: '日本' },
ko: { name: '韩语', nativeName: '한국어', region: '韩国' },
fr: { name: '法语', nativeName: 'Français', region: '法国' },
de: { name: '德语', nativeName: 'Deutsch', region: '德国' }
};
// 🎯 软件背景信息和翻译上下文
const HYPERCHAT_CONTEXT = `
HyperChat 是一个多平台的 AI 聊天应用,具有以下特点:
- 支持多种 AI 模型OpenAI、Claude、Gemini、Qwen、Deepseek 等)
- 完整的 MCP模型上下文协议支持
- 工作区概念:每个项目可以有独立的配置和代理
- AI 代理系统:可创建专门用途的 AI 助手
- 任务调度系统:支持定时任务执行
- 多平台支持Web、桌面Electron、命令行CLI
- 支持多语言界面(中文/英文)
UI 组件包括:表单、按钮、菜单、设置面板、聊天界面、文件管理等。
用户群体开发者、AI 研究人员、需要 AI 辅助工作的专业人士。
`;
// 🎯 翻译结果 Schema - 批量翻译
const BatchTranslationSchema = z.object({
translations: z.array(z.object({
index: z.number(),
original: z.string(),
translation: z.string(),
confidence: z.number().min(0).max(1).optional().describe("翻译置信度0-1之间")
})).describe("按原始顺序排列的翻译结果数组")
});
// 🎯 翻译结果 Schema - 单个翻译
const SingleTranslationSchema = z.object({
translation: z.string().describe("翻译后的中文文本"),
confidence: z.number().min(0).max(1).optional().describe("翻译置信度0-1之间"),
reasoning: z.string().optional().describe("翻译推理过程")
});
// 🎯 AI 提供商配置
interface AIConfig {
provider: 'openai' | 'anthropic' | 'google';
model: string;
apiKey: string;
baseURL?: string;
}
// 🎯 获取 AI 模型
function getAIModel(config: AIConfig) {
// 如果使用 OpenRouter统一使用 OpenAI 兼容接口
if (config.baseURL && config.baseURL.includes('openrouter.ai')) {
const openai = createOpenAI({
apiKey: config.apiKey,
baseURL: config.baseURL
});
return openai(config.model);
}
switch (config.provider) {
case 'openai':
const openai = createOpenAI({
apiKey: config.apiKey,
baseURL: config.baseURL
});
return openai(config.model);
case 'anthropic':
const anthropic = createAnthropic({
apiKey: config.apiKey,
baseURL: config.baseURL
});
return anthropic(config.model);
case 'google':
const google = createGoogleGenerativeAI({
apiKey: config.apiKey,
baseURL: config.baseURL
});
return google(config.model);
default:
throw new Error(`不支持的 AI 提供商: ${config.provider}`);
}
}
// 🎯 创建翻译提示词 - 批量
function createBatchTranslationPrompt(items: string[], targetLang: string): string {
const langConfig = LANGUAGE_CONFIG[targetLang];
const terminologyDict = TERMINOLOGY_DICT[targetLang] || {};
const terminologyList = Object.entries(terminologyDict)
.map(([en, translated]) => `- ${en}${translated}`)
.join('\n');
const itemsList = items.map((text, index) => `${index + 1}. "${text}"`).join('\n');
return `作为专业的软件本地化翻译专家,请将以下 HyperChat 软件界面文本从英文翻译成${langConfig.name}
【软件背景】${HYPERCHAT_CONTEXT}
【专业术语词典】
${terminologyList}
【翻译原则】
1. 保持用户界面的简洁性和一致性
2. 使用${langConfig.region}用户习惯的表达方式
3. 技术术语保持准确性,优先使用词典中的对应翻译
4. 保持原文的格式、标点符号和特殊字符
5. 错误信息要清晰易懂,帮助用户快速定位问题
6. 按钮和菜单项要简洁有力
7. 如果原文是空字符串,返回空字符串
8. 确保翻译质量高且专业,符合软件界面标准
【待翻译文本列表】
${itemsList}
请按照相同的顺序和索引返回结构化的翻译结果。每个翻译都应该准确、自然且符合${langConfig.nativeName}表达习惯。`;
}
// 🎯 创建翻译提示词 - 单个
function createSingleTranslationPrompt(text: string, targetLang: string): string {
const langConfig = LANGUAGE_CONFIG[targetLang];
const terminologyDict = TERMINOLOGY_DICT[targetLang] || {};
const terminologyList = Object.entries(terminologyDict)
.map(([en, translated]) => `- ${en}${translated}`)
.join('\n');
return `作为专业的软件本地化翻译专家,请将以下 HyperChat 软件界面文本从英文翻译成${langConfig.name}
【软件背景】${HYPERCHAT_CONTEXT}
【专业术语词典】
${terminologyList}
【翻译原则】
1. 保持用户界面的简洁性和一致性
2. 使用中国大陆用户习惯的表达方式
3. 技术术语保持准确性,优先使用词典中的对应翻译
4. 保持原文的格式、标点符号和特殊字符
5. 错误信息要清晰易懂,帮助用户快速定位问题
6. 按钮和菜单项要简洁有力
7. 如果原文是空字符串,返回空字符串
【待翻译文本】"${text}"
请提供高质量的${langConfig.nativeName}翻译,确保准确性和专业性。`;
}
// 🎯 批量翻译函数
export async function translateBatch(items: string[], config: AIConfig, targetLang: string = 'zh'): Promise<string[]> {
if (items.length === 0) return [];
try {
const model = getAIModel(config);
const prompt = createBatchTranslationPrompt(items, targetLang);
console.log(`🤖 使用 ${config.provider}/${config.model} 进行批量翻译...`);
const result = await generateObject({
model,
schema: BatchTranslationSchema,
prompt,
temperature: 0.1, // 低温度确保一致性
} as any) as any;
// 按索引排序并提取翻译结果
const sortedTranslations = result.object.translations
.sort((a, b) => a.index - b.index)
.map(t => t.translation);
if (sortedTranslations.length !== items.length) {
console.warn(`⚠️ 翻译数量不匹配,期望 ${items.length},实际 ${sortedTranslations.length}`);
return await translateFallback(items, config, targetLang);
}
// 质量检查
let lowConfidenceCount = 0;
result.object.translations.forEach(t => {
if (t.confidence && t.confidence < 0.8) {
lowConfidenceCount++;
console.warn(`⚠️ 低置信度翻译: "${t.original}" -> "${t.translation}" (${t.confidence})`);
}
});
if (lowConfidenceCount > 0) {
console.warn(`⚠️ 发现 ${lowConfidenceCount} 个低置信度翻译`);
}
return sortedTranslations;
} catch (error) {
console.warn('批量翻译失败,回退到单个翻译模式:', error);
return await translateFallback(items, config, targetLang);
}
}
// 🎯 单个翻译函数
export async function translateSingle(text: string, config: AIConfig, targetLang: string = 'zh'): Promise<string> {
try {
const model = getAIModel(config);
const prompt = createSingleTranslationPrompt(text, targetLang);
const result = await generateObject({
model,
schema: SingleTranslationSchema,
prompt,
temperature: 0.1,
} as any) as any;
// 质量检查
if (result.object.confidence && result.object.confidence < 0.8) {
console.warn(`⚠️ 低置信度翻译: "${text}" -> "${result.object.translation}" (${result.object.confidence})`);
if (result.object.reasoning) {
console.warn(` 推理: ${result.object.reasoning}`);
}
}
return result.object.translation;
} catch (error) {
console.error(`单个翻译失败: ${text}`, error);
throw error;
}
}
// 🎯 回退翻译方法
async function translateFallback(items: string[], config: AIConfig, targetLang: string = 'zh'): Promise<string[]> {
console.log('使用单个翻译回退模式...');
const results: string[] = [];
for (const item of items) {
try {
const translation = await translateSingle(item, config, targetLang);
results.push(translation);
await new Promise(resolve => setTimeout(resolve, 200)); // 增加延迟避免限速
} catch (error) {
console.error(`翻译失败: ${item}`, error);
results.push(item); // 翻译失败时保持原文
}
}
return results;
}
// 🎯 翻译质量验证
function validateTranslation(original: string, translation: string, targetLang: string = 'zh'): { isValid: boolean; issues: string[] } {
const issues: string[] = [];
const terminologyDict = TERMINOLOGY_DICT[targetLang] || {};
// 检查是否为空
if (!translation.trim()) {
if (original.trim()) {
issues.push("翻译结果为空但原文不为空");
}
}
// 检查是否包含英文(可能翻译不完整)
if (/[a-zA-Z]{3,}/.test(translation) && translation !== original) {
// 排除专业术语
const hasValidTerminology = Object.values(terminologyDict).some(term =>
translation.includes(term as string)
);
if (!hasValidTerminology) {
issues.push("翻译中包含较长英文单词");
}
}
// 检查长度是否合理
if (translation.length > original.length * 4) {
issues.push("翻译长度异常过长");
}
// 检查专业术语使用
for (const [en, translated] of Object.entries(terminologyDict)) {
if (original.includes(en) && !translation.includes(translated as string) && !translation.includes(en)) {
issues.push(`专业术语 "${en}" 可能翻译不准确`);
}
}
return {
isValid: issues.length === 0,
issues
};
}
// 🎯 处理单个语言的翻译
async function processLanguage(json: any, aiConfig: AIConfig, targetLang: string): Promise<boolean> {
const langConfig = LANGUAGE_CONFIG[targetLang];
let hasChanges = false;
// 仅验证模式
if (argv.validate) {
console.log(`🔍 验证 ${langConfig.name} 翻译质量...`);
let issues = 0;
for (const [key, value] of Object.entries(json)) {
const entry = value as { [key: string]: string };
if (entry[targetLang]) {
const validation = validateTranslation(key, entry[targetLang], targetLang);
if (!validation.isValid) {
console.warn(`⚠️ [${key}]: ${validation.issues.join(', ')}`);
issues++;
}
}
}
console.log(issues === 0 ? `${langConfig.name} 所有翻译质量良好` : `⚠️ ${langConfig.name} 发现 ${issues} 个质量问题`);
return false;
}
// 统计需要翻译的条目
const needTranslation = Object.keys(json).filter(key => {
if (argv.force) {
return true;
}
const entry = json[key] as { [key: string]: string | null };
const translation = entry[targetLang];
return translation == null || translation === "";
});
if (needTranslation.length === 0) {
console.log(`${langConfig.name} 所有条目都已翻译,无需处理`);
return false;
}
console.log(`📊 ${langConfig.name} 找到 ${needTranslation.length} 个需要翻译的条目`);
// 如果是 dry-run 模式,只显示需要翻译的条目
if (argv['dry-run']) {
console.log(`\n📋 ${langConfig.name}翻译预览:`);
needTranslation.forEach((key, index) => {
const entry = json[key] as { [key: string]: string | null };
const translation = entry[targetLang];
const status = translation ? '(重新翻译)' : '(新翻译)';
console.log(`${index + 1}. ${key} ${status}`);
});
console.log(`\n📈 ${langConfig.name} 总计: ${needTranslation.length} 个条目`);
return false;
}
// 🚀 批量翻译处理
const batchSize = parseInt(argv['batch-size']) || 8;
const batches = [];
for (let i = 0; i < needTranslation.length; i += batchSize) {
batches.push(needTranslation.slice(i, i + batchSize));
}
console.log(`${langConfig.name} 使用批量翻译模式,${batches.length} 个批次,每批 ${batchSize} 个条目`);
for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
const batch = batches[batchIndex];
console.log(`\n🔄 ${langConfig.name} 处理批次 ${batchIndex + 1}/${batches.length} (${batch.length} 个条目)`);
try {
const translations = await translateBatch(batch, aiConfig, targetLang);
// 应用翻译结果并验证质量
for (let i = 0; i < batch.length; i++) {
const key = batch[i];
if (translations[i]) {
const entry = json[key] as { [key: string]: string | null };
entry[targetLang] = translations[i];
hasChanges = true;
// 翻译质量验证
const validation = validateTranslation(key, translations[i], targetLang);
if (validation.isValid) {
console.log(`${key}`);
} else {
console.warn(`⚠️ ${key} (质量警告: ${validation.issues.join(', ')})`);
}
}
}
// 批次间延迟,避免 API 限速
if (batchIndex < batches.length - 1) {
console.log('⏳ 等待 1 秒避免 API 限速...');
await new Promise(resolve => setTimeout(resolve, 1000));
}
} catch (error) {
console.error(`${langConfig.name} 批次 ${batchIndex + 1} 翻译失败:`, error);
console.log('继续处理下一批次...');
}
}
return hasChanges;
}
// 🎯 解析 AI 配置
function parseAIConfig(): { aiConfig: AIConfig; targetLang: string } {
const baseURL = process.env.baseURL;
// 如果使用 OpenRouter默认使用 OpenAI 兼容模式
let provider: AIConfig['provider'];
let model: string;
if (baseURL && baseURL.includes('openrouter.ai')) {
provider = 'openai';
model = argv.model || "google/gemini-2.5-flash";
} else {
provider = (argv.provider || 'google') as AIConfig['provider'];
model = argv.model || "google/gemini-2.5-flash";
}
const apiKey = process.env.apiKey || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.GOOGLE_API_KEY;
const targetLang = argv.lang || argv.language || 'all';
if (!apiKey) {
console.error("❌ 错误: 缺少 API 密钥");
console.error("请设置环境变量: apiKey 或对应的提供商 API 密钥");
process.exit(1);
}
if (targetLang !== 'all' && !LANGUAGE_CONFIG[targetLang]) {
console.error(`❌ 错误: 不支持的语言: ${targetLang}`);
console.error(`支持的语言: ${Object.keys(LANGUAGE_CONFIG).join(', ')}, all`);
process.exit(1);
}
return {
aiConfig: {
provider,
model,
apiKey,
baseURL
},
targetLang
};
}
// 🎯 主函数
async function main() {
if (argv.help || argv.h) {
console.log(`
🌐 HyperChat 智能翻译工具 (基于 AI SDK)
用法: npx tsx scripts/translate.mts [选项]
选项:
--help, -h 显示帮助信息
--dry-run 仅显示需要翻译的条目,不执行翻译
--force 强制重新翻译所有条目(包括已有翻译的)
--batch-size N 批量翻译大小(默认: 8
--validate 仅验证现有翻译质量
--provider NAME AI 提供商 (openai|anthropic|google, 默认: google)
--model MODEL 指定模型 (默认: google/gemini-2.0-flash-exp)
--lang, --language 目标语言 (zh|ja|ko|fr|de|all, 默认: all)
环境变量:
apiKey API 密钥(通用)
OPENAI_API_KEY OpenAI API 密钥
ANTHROPIC_API_KEY Anthropic API 密钥
GOOGLE_API_KEY Google API 密钥
baseURL API 基础 URL可选
特性:
✅ 基于独立 AI SDK轻量高效
✅ 支持多个 AI 提供商 (OpenAI/Anthropic/Google)
✅ JSON Schema 确保翻译结构化和一致性
✅ 针对 HyperChat 软件的专业术语词典
✅ 智能批量翻译减少 API 调用
✅ 置信度评估和质量验证
✅ 智能回退机制
示例:
npx tsx scripts/translate.mts # 翻译所有语言
npx tsx scripts/translate.mts --lang zh # 仅翻译中文
npx tsx scripts/translate.mts --lang ja # 翻译为日语
npx tsx scripts/translate.mts --lang ko # 翻译为韩语
npx tsx scripts/translate.mts --lang fr # 翻译为法语
npx tsx scripts/translate.mts --lang de # 翻译为德语
npx tsx scripts/translate.mts --provider anthropic --lang ja # 使用 Claude 翻译日语
npx tsx scripts/translate.mts --dry-run --lang ko # 预览韩语翻译计划
npx tsx scripts/translate.mts --validate --lang fr # 验证法语翻译质量
npx tsx scripts/translate.mts --dry-run # 预览所有语言翻译计划
`);
process.exit(0);
}
try {
const { aiConfig, targetLang } = parseAIConfig();
// 检查 i18n.json 文件是否存在
if (!fs.existsSync(i18nPath)) {
console.error(`❌ 错误: i18n.json 文件不存在: ${i18nPath}`);
console.error(`请先运行 'node scripts/extract-i18n.mjs' 生成 i18n.json 文件`);
process.exit(1);
}
console.log("🌐 开始 HyperChat 智能翻译 (基于 AI SDK)...");
console.log(`🤖 使用: ${aiConfig.provider}/${aiConfig.model}`);
const json = JSON.parse(fs.readFileSync(i18nPath).toString());
let hasChanges = false;
// 如果选择翻译所有语言
if (targetLang === 'all') {
console.log("🌍 翻译所有支持的语言...");
for (const lang of Object.keys(LANGUAGE_CONFIG)) {
console.log(`\n📍 开始翻译 ${LANGUAGE_CONFIG[lang].name} (${lang})...`);
const result = await processLanguage(json, aiConfig, lang);
if (result) hasChanges = true;
}
} else {
const langConfig = LANGUAGE_CONFIG[targetLang];
console.log(`📍 翻译目标语言: ${langConfig.name}`);
const result = await processLanguage(json, aiConfig, targetLang);
if (result) hasChanges = true;
}
// 如果是 dry-run 或 validate 模式,提前返回
if (argv['dry-run'] || argv.validate) {
if (argv['dry-run'] && targetLang === 'all') {
console.log(`⚡ 预计使用批量翻译,减少 API 调用`);
console.log(`🤖 使用模型: ${aiConfig.provider}/${aiConfig.model}`);
}
return;
}
if (hasChanges) {
// 备份原文件
const backupPath = i18nPath + '.backup-' + Date.now();
fs.copyFileSync(i18nPath, backupPath);
console.log(`\n💾 已备份原文件到: ${backupPath}`);
fs.writeFileSync(i18nPath, JSON.stringify(json, null, 2));
console.log(`✅ 已更新 i18n.json 文件`);
// 重新构建 shared 包
console.log('\n🔧 重新构建 shared 包...');
try {
await $`npm run build --workspace=@dadigua/hyperchat-shared`;
console.log('✅ shared 包构建完成i18n.json 已同步到 dist 目录');
} catch (error) {
console.warn('⚠️ shared 包构建失败,可能需要手动运行: npm run build --workspace=@dadigua/hyperchat-shared');
}
}
console.log(`\n🎉 翻译完成!使用 ${aiConfig.provider}/${aiConfig.model} 完成智能翻译`);
} catch (error) {
console.error("❌ 脚本执行失败:", error);
process.exit(1);
}
}
// 运行主函数
await main();