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 生成初稿后由人工审核关键翻译,持续提升翻译质量。

3000

被折叠的 条评论
为什么被折叠?



