AI 生成前端国际化翻译:从 Key 到多语言文案的全流程
一、前端国际化翻译的工程化挑战
前端国际化(i18n)需要维护多语言翻译文件,传统方案依赖人工翻译或第三方翻译服务。随着产品功能迭代,翻译文件同步成为负担,容易出现遗漏或翻译不准确。
AI 辅助翻译能够通过大语言模型(LLM),自动生成多语言翻译,并保持术语一致性。核心挑战包括:上下文理解、术语统一、复数形式处理、变量插值支持。
// 传统国际化文件维护方式(存在问题) // en-US.json { "welcome.message": "Welcome to our app", "button.save": "Save", "user.profile.title": "User Profile", "notification.unread_count": "You have {count} unread notifications" } // zh-CN.json(需要人工翻译,容易遗漏或翻译不一致) { "welcome.message": "欢迎使用我们的应用", "button.save": "保存", "user.profile.title": "用户资料", "notification.unread_count": "您有 {count} 条未读通知" } // 新增键时,需要手动添加到所有语言文件 // 删除键时,需要手动从所有语言文件中删除 // 这导致维护成本高、容易出错二、AI 翻译的技术架构与流程
AI 驱动的国际化翻译系统包含以下核心模块:
- Key 提取模块:从源代码中自动提取国际化 Key
- 上下文分析模块:理解 Key 的使用场景和语义
- 翻译生成模块:调用 LLM API 生成多语言翻译
- 术语一致性检查:确保同一术语在不同地方翻译一致
- 翻译文件更新:自动更新 JSON/YAML 翻译文件
graph TD A[源代码扫描] --> B[提取 i18n Keys] B --> C{翻译文件是否存在} C -->|否| D[创建新翻译条目] C -->|是| E[检查是否需要更新] D --> F[上下文分析] E --> F F --> G[调用 LLM 生成翻译] G --> H[术语一致性检查] H --> I[人工审核(可选)] I --> J[更新翻译文件] J --> K[生成 TypeScript 类型] K --> L[集成到构建流程] style A fill:#e1f5fe style G fill:#fff3e0 style J fill:#e8f5e9 style L fill:#f3e5f5技术选型:
- Key 提取:使用 AST 分析工具(Babel Parser)扫描代码中的
t()、i18n.t()等调用 - LLM 选择:OpenAI GPT-4(翻译质量高)、Claude 3(上下文理解强)、或开源模型(成本低)
- 术语库:使用 JSON 或数据库存储产品特定术语的翻译映射
- 质量控制:使用 BLEU 评分、人工审核、A/B 测试
场景描述:在一次实际项目中,我们用 Babel Parser 做 Key 提取时发现了一个隐蔽问题:动态 Key 无法被 AST 静态分析捕获。比如t(\user.status.${status}`)` 这种模板字符串方式的调用,AST 看到的不是一个 StringLiteral 而是一个 TemplateLiteral,导致提取器漏掉了大量 Key。等到日语版本的翻译文件上线后,大量未翻译的 Key 直接以英文原文显示在页面上。
// Babel Parser 只能静态分析,这种动态 key 会漏掉 const statusText = t(`user.status.${status}`); // ❌ 不会被提取 // 解决方案:使用枚举替换模板字符串 const statusMap = { active: 'user.status.active', inactive: 'user.status.inactive', banned: 'user.status.banned' } as const; const statusText = t(statusMap[status]); // ✅ 可被静态分析这个案例的教训是:AST 提取不能 100% 覆盖所有场景,团队需要建立规范,限制动态 Key 的使用。同时应该在 CI 中加入覆盖度检查——运行时收集所有实际调用过的t()key,与静态提取的结果做 diff,发现遗漏及时补充。
// i18n Key 提取器实现 // services/i18nKeyExtractor.ts import * as babelParser from '@babel/parser'; import * as t from '@babel/types'; import traverse from '@babel/traverse'; import * as fs from 'fs'; import * as path from 'path'; export interface I18nKeyInfo { key: string; defaultValue?: string; filePath: string; lineNumber: number; context?: string; // 周围的代码上下文 } export class I18nKeyExtractor { private patterns: Array<{ name: string; detect: (node: t.Node) => I18nKeyInfo | null; }>; constructor() { this.patterns = [ { name: 'react-i18next t()', detect: (node) => { if ( t.isCallExpression(node) && t.isIdentifier(node.callee) && node.callee.name === 't' && node.arguments.length >= 1 && t.isStringLiteral(node.arguments[0]) ) { return { key: node.arguments[0].value, defaultValue: node.arguments[1]?.type === 'StringLiteral' ? node.arguments[1].value : undefined, filePath: '', // 需要在遍历时设置 lineNumber: node.loc?.start.line || 0 }; } return null; } }, { name: 'i18next t()', detect: (node) => { if ( t.isCallExpression(node) && t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.object) && node.callee.object.name === 'i18next' && t.isIdentifier(node.callee.property) && node.callee.property.name === 't' && node.arguments.length >= 1 && t.isStringLiteral(node.arguments[0]) ) { return { key: node.arguments[0].value, filePath: '', lineNumber: node.loc?.start.line || 0 }; } return null; } }, { name: 'FormattedMessage (react-intl)', detect: (node) => { if ( t.isJSXElement(node) && t.isJSXOpeningElement(node.openingElement) && t.isJSXIdentifier(node.openingElement.name) && node.openingElement.name.name === 'FormattedMessage' ) { const idAttr = node.openingElement.attributes.find( attr => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === 'id' ) as t.JSXAttribute | undefined; if (idAttr?.value && t.isStringLiteral(idAttr.value)) { return { key: idAttr.value.value, filePath: '', lineNumber: node.loc?.start.line || 0 }; } } return null; } } ]; } /** * 从文件中提取 i18n Keys */ extractFromFile(filePath: string): I18nKeyInfo[] { const code = fs.readFileSync(filePath, 'utf-8'); const keys: I18nKeyInfo[] = []; try { const ast = babelParser.parse(code, { sourceType: 'module', plugins: ['jsx', 'typescript', 'decorators-legacy'] }); traverse(ast, { enter: (path) => { for (const pattern of this.patterns) { const result = pattern.detect(path.node); if (result) { result.filePath = filePath; // 获取更多上下文(前 3 行代码) const lines = code.split('\n'); const startLine = Math.max(0, result.lineNumber - 4); result.context = lines.slice(startLine, result.lineNumber).join('\n'); keys.push(result); break; // 一个节点只匹配一个模式 } } } }); } catch (error) { console.error(`Failed to parse file ${filePath}:`, error); } return keys; } /** * 扫描目录,提取所有 i18n Keys */ extractFromDirectory(dirPath: string, extensions: string[] = ['.tsx', '.ts', '.jsx', '.js']): I18nKeyInfo[] { const allKeys: I18nKeyInfo[] = []; const scanDirectory = (currentPath: string) => { const items = fs.readdirSync(currentPath); for (const item of items) { const itemPath = path.join(currentPath, item); const stat = fs.statSync(itemPath); if (stat.isDirectory()) { // 跳过 node_modules、dist 等目录 if (!['node_modules', 'dist', 'build', '.git'].includes(item)) { scanDirectory(itemPath); } } else if (stat.isFile()) { const ext = path.extname(itemPath); if (extensions.includes(ext)) { const keys = this.extractFromFile(itemPath); allKeys.push(...keys); } } } }; scanDirectory(dirPath); return allKeys; } }三、实战:AI 翻译生成与术语一致性保障
以下实现完整的 AI 翻译生成流程,包括术语库管理、LLM 调用、翻译质量控制。
1. 术语库管理
// services/terminologyManager.ts export interface TermEntry { term: string; // 源语言术语 translations: Record<string, string>; // 各语言的翻译 context?: string; // 使用场景说明 approved: boolean; // 是否已审核 } export class TerminologyManager { private terms: Map<string, TermEntry> = new Map(); private filePath: string; constructor(filePath: string = './i18n/terminology.json') { this.filePath = filePath; this.loadTerms(); } /** * 加载术语库 */ private loadTerms(): void { try { const content = fs.readFileSync(this.filePath, 'utf-8'); const data = JSON.parse(content); for (const [term, entry] of Object.entries(data)) { this.terms.set(term, entry as TermEntry); } console.log(`Loaded ${this.terms.size} terminology entries`); } catch (error) { console.log('Terminology file not found, starting with empty terminology'); } } /** * 保存术语库 */ private saveTerms(): void { const data = Object.fromEntries(this.terms); fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2), 'utf-8'); } /** * 添加或更新术语 */ addTerm(term: string, translations: Record<string, string>, context?: string): void { const existing = this.terms.get(term); this.terms.set(term, { term, translations: { ...existing?.translations, ...translations }, context: context || existing?.context, approved: false }); this.saveTerms(); } /** * 查找术语翻译 */ findTranslation(term: string, language: string): string | null { const entry = this.terms.get(term); return entry?.translations[language] || null; } /** * 构建 LLM Prompt 的术语部分 */ buildTerminologyPrompt(languages: string[]): string { if (this.terms.size === 0) { return ''; } let prompt = '\n\n## Terminology (必须严格遵守)\n'; for (const [term, entry] of this.terms) { prompt += `- ${term}:`; for (const lang of languages) { const translation = entry.translations[lang]; if (translation) { prompt += ` [${lang}] ${translation}`; } } if (entry.context) { prompt += ` (上下文: ${entry.context})`; } prompt += '\n'; } return prompt; } }2. AI 翻译生成服务
// services/aiTranslationService.ts import OpenAI from 'openai'; export interface TranslationRequest { keys: Array<{ key: string; defaultValue?: string; context?: string; }>; sourceLanguage: string; targetLanguages: string[]; terminologyPrompt?: string; } export interface TranslationResult { [key: string]: { [language: string]: string; }; } export class AITranslationService { private openai: OpenAI; private terminologyManager: TerminologyManager; constructor(apiKey: string, terminologyManager: TerminologyManager) { this.openai = new OpenAI({ apiKey }); this.terminologyManager = terminologyManager; } /** * 批量生成翻译 */ async generateTranslations(request: TranslationRequest): Promise<TranslationResult> { const { keys, sourceLanguage, targetLanguages, terminologyPrompt } = request; // 构建 LLM Prompt const prompt = this.buildPrompt( keys, sourceLanguage, targetLanguages, terminologyPrompt ); try { const response = await this.openai.chat.completions.create({ model: 'gpt-4', messages: [ { role: 'system', content: 'You are a professional translator specializing in software UI translation. Provide accurate, concise, and contextually appropriate translations.' }, { role: 'user', content: prompt } ], temperature: 0.3, // 低温度确保一致性 response_format: { type: 'json_object' } }); const content = response.choices[0]?.message?.content; if (!content) { throw new Error('Empty response from LLM'); } const translations = JSON.parse(content) as TranslationResult; // 验证翻译结果 this.validateTranslations(translations, keys, targetLanguages); return translations; } catch (error) { console.error('Failed to generate translations:', error); throw error; } } /** * 构建翻译 Prompt */ private buildPrompt( keys: TranslationRequest['keys'], sourceLanguage: string, targetLanguages: string[], terminologyPrompt?: string ): string { let prompt = `# Task: Translate UI strings for a software application\n\n`; prompt += `## Source Language: ${sourceLanguage}\n`; prompt += `## Target Languages: ${targetLanguages.join(', ')}\n\n`; prompt += `## Strings to Translate\n\n`; for (const item of keys) { prompt += `### Key: ${item.key}\n`; if (item.defaultValue) { prompt += `Default Value: "${item.defaultValue}"\n`; } if (item.context) { prompt += `Context: ${item.context}\n`; } prompt += `\n`; } // 添加术语库约束 if (terminologyPrompt) { prompt += terminologyPrompt; } prompt += `\n## Instructions\n`; prompt += `1. Provide translations for ALL target languages.\n`; prompt += `2. Keep translations concise and appropriate for UI elements.\n`; prompt += `3. Preserve variables in the format {variableName} unchanged.\n`; prompt += `4. For plural forms, use the format: "one item | {count} items".\n`; prompt += `5. Return the result as a JSON object with the following structure:\n`; prompt += `{\n`; prompt += ` "key_name": {\n`; prompt += ` "language_code": "translation"\n`; prompt += ` }\n`; prompt += `}\n`; return prompt; } /** * 验证翻译结果 */ private validateTranslations( translations: TranslationResult, keys: TranslationRequest['keys'], targetLanguages: string[] ): void { const errors: string[] = []; for (const keyInfo of keys) { const translation = translations[keyInfo.key]; if (!translation) { errors.push(`Missing translation for key: ${keyInfo.key}`); continue; } for (const lang of targetLanguages) { if (!translation[lang]) { errors.push(`Missing ${lang} translation for key: ${keyInfo.key}`); } } // 检查变量是否保留 if (keyInfo.defaultValue) { const variables = keyInfo.defaultValue.match(/\{(\w+)\}/g) || []; for (const lang of targetLanguages) { const translated = translation[lang]; if (translated) { for (const variable of variables) { if (!translated.includes(variable)) { errors.push(`Variable ${variable} not preserved in ${lang} translation for key: ${keyInfo.key}`); } } } } } } if (errors.length > 0) { console.warn('Translation validation warnings:'); errors.forEach(err => console.warn(` - ${err}`)); } } }3. 翻译文件自动更新
场景描述:自动更新翻译文件虽然方便,但是 AI 翻译偶尔会把占位符{variable}翻译成目标语言的等价表达,比如把{count} items的日语翻译写成{count} アイテム就对了,但有时模型会把{count}当成原文的一部分翻译成{個数},导致运行时变量不再被识别为插值点,直接显示为原始字符串。我们的做法是在updateTranslations方法中增加校验步骤,检查翻译后占位符数量和原始值的占位符数量是否一致:
// 占位符完整性校验 function validatePlaceholders(original: string, translated: string): boolean { const regex = /\{(\w+)\}/g; const origMatches = original.match(regex) || []; const transMatches = translated.match(regex) || []; return origMatches.every(m => transMatches.includes(m)); }如果校验失败,对应的 key 标记为需要人工复审,不直接写入文件。这个简单的校验避免了多次线上"文案出现{個数}而不是实际数字"的事故。
// services/translationFileManager.ts export class TranslationFileManager { private basePath: string; constructor(basePath: string = './public/locales') { this.basePath = basePath; } /** * 更新翻译文件 */ async updateTranslations( translations: TranslationResult, targetLanguages: string[] ): Promise<void> { for (const lang of targetLanguages) { const filePath = path.join(this.basePath, lang, 'translation.json'); // 读取现有翻译 let existing: Record<string, string> = {}; try { const content = fs.readFileSync(filePath, 'utf-8'); existing = JSON.parse(content); } catch (error) { console.log(`Creating new translation file: ${filePath}`); } // 更新翻译 let updated = false; for (const [key, translation] of Object.entries(translations)) { if (translation[lang] && existing[key] !== translation[lang]) { existing[key] = translation[lang]; updated = true; } } // 写回文件 if (updated) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(existing, null, 2), 'utf-8'); console.log(`Updated translation file: ${filePath}`); } } } /** * 检测缺失的翻译 */ findMissingTranslations( keys: string[], targetLanguages: string[] ): Record<string, string[]> { const missing: Record<string, string[]> = {}; for (const lang of targetLanguages) { const filePath = path.join(this.basePath, lang, 'translation.json'); try { const content = fs.readFileSync(filePath, 'utf-8'); const translations = JSON.parse(content); const missingKeys = keys.filter(key => !translations[key]); if (missingKeys.length > 0) { missing[lang] = missingKeys; } } catch (error) { console.error(`Failed to read translation file for ${lang}:`, error); missing[lang] = keys; // 文件不存在,所有 key 都缺失 } } return missing; } }4. 完整工作流集成
// scripts/generate-translations.ts - CLI 工具 import { I18nKeyExtractor } from '../services/i18nKeyExtractor'; import { TerminologyManager } from '../services/terminologyManager'; import { AITranslationService } from '../services/aiTranslationService'; import { TranslationFileManager } from '../services/translationFileManager'; async function main() { // 1. 提取 i18n Keys console.log('Extracting i18n keys...'); const extractor = new I18nKeyExtractor(); const keys = extractor.extractFromDirectory('./src'); console.log(`Found ${keys.length} i18n keys`); // 2. 加载术语库 console.log('Loading terminology...'); const termManager = new TerminologyManager(); const terminologyPrompt = termManager.buildTerminologyPrompt(['zh-CN', 'ja-JP', 'ko-KR']); // 3. 检测缺失的翻译 console.log('Checking for missing translations...'); const fileManager = new TranslationFileManager(); const missing = fileManager.findMissingTranslations( keys.map(k => k.key), ['zh-CN', 'ja-JP', 'ko-KR'] ); console.log('Missing translations:', missing); // 4. 生成翻译 if (Object.keys(missing).length > 0) { console.log('Generating translations with AI...'); const translationService = new AITranslationService( process.env.OPENAI_API_KEY!, termManager ); const keysToTranslate = keys.filter(k => Object.values(missing).some(langKeys => langKeys.includes(k.key)) ); const translations = await translationService.generateTranslations({ keys: keysToTranslate.map(k => ({ key: k.key, defaultValue: k.defaultValue, context: k.context })), sourceLanguage: 'en-US', targetLanguages: ['zh-CN', 'ja-JP', 'ko-KR'], terminologyPrompt }); // 5. 更新翻译文件 console.log('Updating translation files...'); await fileManager.updateTranslations(translations, ['zh-CN', 'ja-JP', 'ko-KR']); console.log('✅ Translation generation complete!'); } else { console.log('✅ All translations are up to date!'); } } main().catch(error => { console.error('❌ Translation generation failed:', error); process.exit(1); });四、AI 翻译的工程化最佳实践
1. 翻译质量评估:使用 BLEU 评分和人工审核结合的方式确保质量。
// 翻译质量评估 export async function evaluateTranslationQuality( sourceTexts: string[], translatedTexts: string[], referenceTranslations: string[] ): Promise<{ bleuScore: number; suggestions: string[] }> { // 简化版 BLEU 评分计算 let totalScore = 0; const suggestions: string[] = []; for (let i = 0; i < sourceTexts.length; i++) { const translated = translatedTexts[i]; const reference = referenceTranslations[i]; // 计算 n-gram 重叠度(简化逻辑) const translatedWords = translated.split(/\s+/); const referenceWords = reference.split(/\s+/); const commonWords = translatedWords.filter(w => referenceWords.includes(w)); const score = commonWords.length / Math.max(translatedWords.length, 1); totalScore += score; if (score < 0.5) { suggestions.push(`Low quality translation for: ${sourceTexts[i]}`); } } const bleuScore = totalScore / sourceTexts.length; return { bleuScore, suggestions }; }2. 增量翻译:只翻译新增或修改的 Keys,避免重复翻译。
// 增量翻译支持 export class IncrementalTranslationManager { private cachePath: string; constructor(cachePath: string = './.i18n-cache.json') { this.cachePath = cachePath; } /** * 计算需要翻译的 Keys(新增或修改后的) */ async getKeysToTranslate(keys: I18nKeyInfo[]): Promise<I18nKeyInfo[]> { const cache = this.loadCache(); const keysToTranslate: I18nKeyInfo[] = []; for (const key of keys) { const cached = cache[key.key]; const currentHash = this.hashCode(key.defaultValue || key.key); if (!cached || cached.hash !== currentHash) { keysToTranslate.push(key); } } return keysToTranslate; } /** * 更新缓存 */ async updateCache(keys: I18nKeyInfo[]): Promise<void> { const cache = this.loadCache(); for (const key of keys) { cache[key.key] = { hash: this.hashCode(key.defaultValue || key.key), lastTranslated: new Date().toISOString() }; } fs.writeFileSync(this.cachePath, JSON.stringify(cache, null, 2), 'utf-8'); } private loadCache(): Record<string, { hash: number; lastTranslated: string }> { try { return JSON.parse(fs.readFileSync(this.cachePath, 'utf-8')); } catch { return {}; } } private hashCode(str: string): number { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return hash; } }3. CI/CD 集成:在 CI/CD 流程中自动检测并更新翻译。
场景描述:将 AI 翻译集成到 CI/CD 中有一个风险点值得注意——如果 CI 每次 push 都自动生成并提交翻译,但 AI 翻译结果偶然出现了严重错误(比如把否定句翻译成了肯定句),这个错误会被自动合入代码库并可能随下一次部署上线。因此,我们增加了一个缓冲区:AI 翻译的 PR 只自动创建,不自动合入。由人工或简单的规则检查(如关键术语一致性校验)通过后再合入。
# 关键:自动创建 PR 但不自动合入 - name: Create Translation PR uses: peter-evans/create-pull-request@v5 with: title: 'Auto-generated i18n translations' body: 'AI 自动生成的翻译更新,请检查后合入' branch: auto/i18n-update labels: 'i18n, auto-generated' # 不自动合入,等待人工或规则检查这样既保证了翻译同步的效率,又避免了一次 AI 的幻觉直接把错误翻译推到线上。
# .github/workflows/translations.yml name: Update Translations on: push: branches: [main] paths: - 'src/**' - 'public/locales/**' jobs: update-translations: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: 18 - name: Install dependencies run: npm ci - name: Extract i18n keys run: npm run i18n:extract - name: Generate translations (AI) env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} run: npm run i18n:translate - name: Commit updated translations uses: stefanzweifel/git-auto-commit-action@v4 with: commit_message: 'Auto-update i18n translations [skip ci]' file_pattern: 'public/locales/**'五、总结
AI 生成前端国际化翻译能够显著降低多语言维护成本。通过自动提取 i18n Keys、调用 LLM 生成翻译、保障术语一致性,可以实现翻译文件的自动更新。
关键实践包括:建立术语库确保一致性、增量翻译避免重复成本、翻译质量评估、CI/CD 自动化集成。这些手段能够确保翻译质量,并减少人工介入。
对于大型项目,建议建立翻译审核流程,AI 生成初稿后由人工审核关键翻译,持续提升翻译质量。