737 lines
24 KiB
TypeScript
737 lines
24 KiB
TypeScript
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(); |