LLM 驱动的前端国际化方案:从文本提取到多语言代码生成的工程实践

一、前端国际化的工程痛点:手动维护翻译文件的噩梦
前端国际化(i18n)看似简单——把硬编码的中文替换为 i18n key,再提供各语言的翻译文件。但在大型项目中,这个流程迅速失控:新增功能时遗漏翻译 key、翻译文件与代码不同步、翻译质量无法自动化验证、文案变更需要跨团队协调。更严重的是,翻译文件往往成为"无人认领的代码",开发者在其中堆砌 key 却从不清理废弃条目。
LLM 为国际化流程提供了新的可能性:自动提取代码中的硬编码文本、根据上下文生成高质量翻译、检测翻译文件与代码的一致性。这并非替代专业翻译,而是将机械性的提取和初译工作自动化,让翻译人员专注于语义校准和文化适配。
二、LLM 辅助国际化的架构设计
2.1 国际化全流程自动化
flowchart TB
subgraph Extract["文本提取"]
E1[AST 扫描源码] –> E2[识别硬编码中文]
E2 –> E3[生成 i18n key]
E3 –> E4[替换源码引用]
end
subgraph Translate["翻译生成"]
T1[提取中文文案] –> T2[LLM 上下文翻译]
T2 –> T3[术语表约束]
T3 –> T4[翻译质量评分]
end
subgraph Validate["一致性校验"]
V1[代码引用扫描] –> V2[翻译文件扫描]
V2 –> V3[缺失 key 检测]
V3 –> V4[废弃 key 检测]
end
Extract –> Translate –> Validate
2.2 i18n key 的命名规范
规范的 key 命名是国际化可维护性的基础。推荐采用 模块.页面.元素.状态 的层级命名,如 user.profile.email.label、order.list.empty.hint。这种命名既便于定位代码位置,也便于翻译人员理解上下文。
三、LLM 辅助国际化的代码实现
3.1 硬编码文本提取器
import * as ts from 'typescript';
import * as fs from 'fs';
import * as path from 'path';
interface ExtractedText {
file: string;
line: number;
original: string;
key: string;
context: string; // 周围代码的上下文
}
// 从 TypeScript/TSX 文件中提取硬编码中文
function extractChineseText(filePath: string): ExtractedText[] {
const sourceCode = fs.readFileSync(filePath, 'utf-8');
const sourceFile = ts.createSourceFile(
filePath,
sourceCode,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TSX
);
const results: ExtractedText[] = [];
function visit(node: ts.Node) {
// 检测字符串字面量中的中文
if (ts.isStringLiteral(node) && containsChinese(node.text)) {
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
const context = getContext(sourceCode, node.getStart(), 200);
const key = generateKey(filePath, node.text);
results.push({
file: filePath,
line,
original: node.text,
key,
context,
});
}
// 检测 JSX 文本中的中文
if (ts.isJsxText(node) && containsChinese(node.text)) {
const text = node.text.trim();
if (text) {
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
const context = getContext(sourceCode, node.getStart(), 200);
const key = generateKey(filePath, text);
results.push({
file: filePath,
line,
original: text,
key,
context,
});
}
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
return results;
}
function containsChinese(text: string): boolean {
return /[\\u4e00-\\u9fa5]/.test(text);
}
// 根据文件路径和文本内容生成 i18n key
function generateKey(filePath: string, text: string): string {
const relativePath = path.relative(process.cwd(), filePath);
const parts = relativePath
.replace(/\\.(tsx?|jsx?)$/, '')
.split(path.sep)
.filter(p => p !== 'src' && p !== 'pages' && p !== 'components');
// 取路径的最后两级作为 key 前缀
const prefix = parts.slice(-2).join('.');
// 取文本的前几个字作为语义标识
const suffix = text.replace(/[^\\u4e00-\\u9fa5a-zA-Z0-9]/g, '').slice(0, 8);
return `${prefix}.${suffix}`;
}
function getContext(sourceCode: string, position: number, length: number): string {
const start = Math.max(0, position – length / 2);
const end = Math.min(sourceCode.length, position + length / 2);
return sourceCode.slice(start, end);
}
3.2 LLM 翻译生成器
interface TranslationRequest {
key: string;
chinese: string;
context: string;
glossary: Record<string, string>; // 术语表
}
interface TranslationResult {
key: string;
en: string;
ja: string;
ko: string;
qualityScore: number; // 0-1 翻译质量评分
}
// 使用 LLM 生成多语言翻译
async function generateTranslations(
requests: TranslationRequest[]
): Promise<TranslationResult[]> {
const glossaryStr = Object.entries(requests[0]?.glossary || {})
.map(([cn, en]) => `${cn} → ${en}`)
.join('\\n');
const prompt = `你是一个专业的前端翻译专家。请将以下中文文案翻译为英文、日文、韩文。
翻译规则:
1. 保持 UI 文案的简洁性,英文不超过中文长度的 1.5 倍
2. 遵循术语表中的固定翻译
3. 根据代码上下文推断文案的语义场景
4. 输出 JSON 数组格式
术语表:
${glossaryStr}
待翻译文案:
${requests.map(r => `Key: ${r.key}\\n中文: ${r.chinese}\\n上下文: ${r.context}`).join('\\n—\\n')}
输出格式:
[{"key": "…", "en": "…", "ja": "…", "ko": "…"}]`;
const response = await callLLM(prompt);
const results = JSON.parse(response);
// 为每条翻译添加质量评分
return results.map(r => ({
…r,
qualityScore: estimateQuality(r),
}));
}
// 简单的翻译质量评估
function estimateQuality(result: TranslationResult): number {
let score = 1.0;
// 英文过长扣分
if (result.en.length > result.key.length * 3) score -= 0.2;
// 包含未翻译的中文扣分
if (/[\\u4e00-\\u9fa5]/.test(result.en)) score -= 0.3;
return Math.max(0, score);
}
3.3 一致性校验工具
interface ValidationResult {
missingInCode: string[]; // 翻译文件中有但代码中未引用的 key
missingInTranslation: string[]; // 代码中引用但翻译文件中缺失的 key
emptyTranslations: string[]; // 翻译值为空的 key
}
function validateI18nConsistency(
codeKeys: Set<string>,
translationFiles: Record<string, Record<string, string>>
): ValidationResult {
const allTranslationKeys = new Set<string>();
const missingInCode: string[] = [];
const missingInTranslation: string[] = [];
const emptyTranslations: string[] = [];
// 收集所有翻译文件中的 key
for (const translations of Object.values(translationFiles)) {
for (const key of Object.keys(translations)) {
allTranslationKeys.add(key);
}
}
// 检查翻译文件中有但代码中未引用的 key(废弃 key)
for (const key of allTranslationKeys) {
if (!codeKeys.has(key)) {
missingInCode.push(key);
}
}
// 检查代码中引用但翻译文件中缺失的 key
for (const key of codeKeys) {
if (!allTranslationKeys.has(key)) {
missingInTranslation.push(key);
}
}
// 检查翻译值为空的 key
for (const [lang, translations] of Object.entries(translationFiles)) {
for (const [key, value] of Object.entries(translations)) {
if (!value.trim()) {
emptyTranslations.push(`${lang}/${key}`);
}
}
}
return { missingInCode, missingInTranslation, emptyTranslations };
}
四、LLM 辅助国际化的架构权衡
4.1 LLM 翻译 vs 专业翻译
LLM 翻译适合初译和批量翻译,但在文化适配、品牌调性和法律合规方面仍需专业翻译人员审核。建议将 LLM 翻译作为"初稿",标记为 status: draft,经人工审核后才标记为 status: approved。
4.2 自动提取的准确率
AST 扫描能准确识别字符串字面量和 JSX 文本中的中文,但无法识别动态拼接的文案(如 '欢迎' + name)。对于动态文案,仍需人工标注。建议在代码规范中禁止字符串拼接,统一使用模板函数(如 t('welcome', { name }))。
4.3 翻译文件的格式选择
JSON 格式最常见但不支持注释,YAML 支持注释但解析复杂。建议使用 JSON 格式,将注释信息放在 _comment 字段中。对于大型项目,按模块拆分翻译文件,避免单文件过大。
五、总结
前端国际化的工程化需要覆盖文本提取、翻译生成和一致性校验三个环节。AST 扫描实现硬编码文本的自动提取,LLM 根据代码上下文生成多语言初译,一致性校验工具检测缺失和废弃的翻译 key。LLM 翻译的价值在于将初译的机械劳动自动化,而非替代专业翻译。落地时建议从文本提取和一致性校验这两个确定性工具开始,再逐步引入 LLM 翻译辅助,确保每一步都有质量门禁。
