DeepL / Google Translate API 机器翻译集成
核心问题:什么时候适合用机器翻译?DeepL 和 Google Translate 怎么集成?机器翻译的质量怎么评估?
真实场景
你的应用新增了 1000 个翻译 key,需要翻译成 6 种语言。纯人工翻译需要 3 周,但产品要求 1 周上线。策略:机器翻译快速填充,人工审查 30% 最关键的 key(关键按钮、错误提示、营销文案),其余机器翻译直接上线。
机器翻译(MT)适用场景
graph TD
A[翻译需求] --> B{内容类型?}
B -->|营销文案/品牌语言| C[纯人工 ❌ MT 质量不达标]
B -->|法律/合规内容| D[纯人工 ❌ MT 风险太高]
B -->|用户界面文字| E{量级?}
E -->|少量 < 100 key| F[纯人工 ✅]
E -->|中量 100-1000 key| G[MT + 人工审校 ✅]
E -->|大量 > 1000 key| H[MT 直接上线\n低优先级内容 ✅]
B -->|产品描述/帮助文档| I[MT + 人工后审 ✅]
B -->|错误信息| J[MT + 重要错误人工审 ✅]
DeepL API 集成
DeepL 在欧洲语言翻译质量普遍优于 Google Translate,中文翻译也表现良好。
安装
npm install deepl-node
批量翻译脚本
// scripts/machine-translate.ts
import * as deepl from 'deepl-node';
import fs from 'fs';
import path from 'path';
const translator = new deepl.Translator(process.env.DEEPL_API_KEY!);
// DeepL 支持的目标语言代码(与 BCP 47 略有不同)
const DEEPL_LOCALE_MAP: Record<string, deepl.TargetLanguageCode> = {
'en-US': 'EN-US',
'en-GB': 'EN-GB',
'de-DE': 'DE',
'fr-FR': 'FR',
'ja-JP': 'JA',
'ko-KR': 'KO',
'zh-TW': 'ZH', // DeepL 目前只有简体/繁体两种中文
'es-ES': 'ES',
'it-IT': 'IT',
'pt-BR': 'PT-BR',
'ru-RU': 'RU',
'nl-NL': 'NL',
'pl-PL': 'PL',
};
// 扁平化/反扁平化工具
function flatten(obj: Record<string, unknown>, prefix = ''): Record<string, string> {
return Object.entries(obj).reduce((acc, [key, value]) => {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'object' && value !== null) {
Object.assign(acc, flatten(value as Record<string, unknown>, fullKey));
} else {
acc[fullKey] = String(value);
}
return acc;
}, {} as Record<string, string>);
}
function unflatten(obj: Record<string, string>): Record<string, unknown> {
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
const keys = key.split('.');
let current = result;
for (let i = 0; i < keys.length - 1; i++) {
current[keys[i]] = current[keys[i]] ?? {};
current = current[keys[i]] as Record<string, unknown>;
}
current[keys[keys.length - 1]] = value;
}
return result;
}
// 保护占位符不被翻译
function protectPlaceholders(text: string): { protected: string; map: Record<string, string> } {
const map: Record<string, string> = {};
let counter = 0;
const protected_ = text.replace(/\{[^}]+\}/g, (match) => {
const placeholder = `__PH${counter++}__`;
map[placeholder] = match;
return placeholder;
});
return { protected: protected_, map };
}
function restorePlaceholders(text: string, map: Record<string, string>): string {
return Object.entries(map).reduce(
(result, [placeholder, original]) => result.replace(placeholder, original),
text
);
}
// 批量翻译(分批处理,避免超出 API 限制)
async function translateBatch(
texts: string[],
targetLang: deepl.TargetLanguageCode,
sourceLang: deepl.SourceLanguageCode = 'ZH' // 中文源
): Promise<string[]> {
const BATCH_SIZE = 50; // DeepL 单次最多 50 个字符串
const results: string[] = [];
for (let i = 0; i < texts.length; i += BATCH_SIZE) {
const batch = texts.slice(i, i + BATCH_SIZE);
// 保护占位符
const protectedBatch = batch.map(protectPlaceholders);
const translations = await translator.translateText(
protectedBatch.map(p => p.protected),
sourceLang,
targetLang,
{
preserveFormatting: true,
tagHandling: 'xml', // 处理 XML/HTML 标签
}
);
// 恢复占位符
results.push(
...translations.map((t, idx) =>
restorePlaceholders(t.text, protectedBatch[idx].map)
)
);
// API 速率限制保护
if (i + BATCH_SIZE < texts.length) {
await new Promise(resolve => setTimeout(resolve, 500));
}
}
return results;
}
// 主函数:翻译所有缺失的 key
async function translateMissingKeys(
sourceLocale: string,
targetLocale: string,
namespace: string
) {
const sourceFile = path.join('src/locales', sourceLocale, `${namespace}.json`);
const targetFile = path.join('src/locales', targetLocale, `${namespace}.json`);
if (!fs.existsSync(sourceFile)) return;
const source = JSON.parse(fs.readFileSync(sourceFile, 'utf-8'));
const target = fs.existsSync(targetFile)
? JSON.parse(fs.readFileSync(targetFile, 'utf-8'))
: {};
const sourceFlat = flatten(source);
const targetFlat = flatten(target);
// 找出需要翻译的 key
const missing = Object.entries(sourceFlat).filter(
([key]) => !targetFlat[key] || targetFlat[key] === ''
);
if (missing.length === 0) {
console.log(`[${targetLocale}/${namespace}] 已全部翻译`);
return;
}
console.log(`[${targetLocale}/${namespace}] 翻译 ${missing.length} 个缺失 key...`);
const deepLCode = DEEPL_LOCALE_MAP[targetLocale];
if (!deepLCode) {
console.warn(`不支持的目标语言: ${targetLocale}`);
return;
}
// 批量翻译
const texts = missing.map(([_, text]) => text);
const translations = await translateBatch(texts, deepLCode);
// 合并翻译结果(标记为机器翻译)
const updatedTarget = { ...targetFlat };
missing.forEach(([key], idx) => {
updatedTarget[key] = translations[idx];
// 在翻译文件旁边的 .mt-flag 文件中记录机器翻译的 key
});
// 保存翻译结果
fs.mkdirSync(path.dirname(targetFile), { recursive: true });
fs.writeFileSync(targetFile, JSON.stringify(unflatten(updatedTarget), null, 2));
// 记录机器翻译的 key(供后续人工审查)
const mtFlagFile = targetFile.replace('.json', '.mt-keys.json');
const mtKeys = missing.map(([key]) => key);
fs.writeFileSync(mtFlagFile, JSON.stringify(mtKeys, null, 2));
console.log(`[${targetLocale}/${namespace}] ✅ 完成,${missing.length} 个 key 已机器翻译`);
}
// 运行
const NAMESPACES = ['common', 'product', 'cart', 'checkout', 'errors'];
const TARGET_LOCALES = ['en-US', 'ja-JP', 'ko-KR'];
for (const locale of TARGET_LOCALES) {
for (const ns of NAMESPACES) {
await translateMissingKeys('zh-CN', locale, ns);
}
}
Google Cloud Translation API
npm install @google-cloud/translate
// Google Cloud Translation v3
import { TranslationServiceClient } from '@google-cloud/translate';
const client = new TranslationServiceClient();
const PROJECT_ID = process.env.GCP_PROJECT_ID!;
const LOCATION = 'global';
async function translateWithGoogle(
texts: string[],
targetLanguage: string, // BCP 47 格式:'en', 'ja', 'ko'
sourceLanguage = 'zh'
): Promise<string[]> {
const [response] = await client.translateText({
parent: `projects/${PROJECT_ID}/locations/${LOCATION}`,
contents: texts,
mimeType: 'text/plain',
sourceLanguageCode: sourceLanguage,
targetLanguageCode: targetLanguage,
});
return response.translations?.map(t => t.translatedText ?? '') ?? [];
}
DeepL vs Google Translate 对比
| 维度 | DeepL | Google Translate |
|---|---|---|
| 欧洲语言质量 | ⭐⭐⭐⭐⭐(业界最佳) | ⭐⭐⭐⭐ |
| 亚洲语言质量 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 语言支持数量 | 31 种语言 | 133+ 种语言 |
| 中文支持 | 简体+繁体 | 简体/繁体/文言文 |
| API 价格(每百万字符) | $25 | $20 |
| 免费额度 | 50 万字符/月 | $300 试用额度 |
| 占位符保护 | 原生支持(XML tag) | 需要自行处理 |
| 术语表 | ✅(Glossary API) | ✅(Glossary API) |
| 响应速度 | 快 | 快 |
混合使用策略
// 根据语言选择最佳 MT 引擎
async function smartTranslate(text: string, targetLocale: string): Promise<string> {
const DEEPL_LOCALES = ['en-US', 'en-GB', 'de-DE', 'fr-FR', 'es-ES', 'it-IT', 'pt-BR', 'ru-RU', 'nl-NL', 'pl-PL'];
const GOOGLE_LOCALES = ['ja-JP', 'ko-KR', 'th-TH', 'vi-VN', 'ar-SA', 'hi-IN'];
if (DEEPL_LOCALES.includes(targetLocale)) {
return translateWithDeepL(text, targetLocale);
} else if (GOOGLE_LOCALES.includes(targetLocale)) {
return translateWithGoogle(text, targetLocale.split('-')[0]);
}
// 默认使用 Google(语言支持更广)
return translateWithGoogle(text, targetLocale.split('-')[0]);
}
机器翻译质量评估
BLEU Score 自动评估
# 使用 sacrebleu 库评估机器翻译质量
from sacrebleu.metrics import BLEU
bleu = BLEU()
# 机器翻译结果(系统输出)
hypothesis = ["The product is out of stock", "Add to cart"]
# 人工参考翻译
references = [["The item is temporarily unavailable", "Add item to cart"]]
score = bleu.corpus_score(hypothesis, references)
print(f"BLEU score: {score.score:.2f}")
# BLEU 分数 0-100,越高越好
# < 10: 几乎无用
# 10-30: 可读,但需要大量修改
# 30-50: 较好,需要少量修改
# > 50: 接近人工翻译质量
人工抽样审查机制
// scripts/sample-for-review.js
// 从机器翻译结果中随机抽样,生成人工审查清单
const MT_SAMPLE_RATE = 0.3; // 审查 30% 的机器翻译
function sampleForReview(mtKeys, allTranslations, sampleRate) {
// 优先审查高频/高重要性的 key
const criticalPatterns = ['error.', 'auth.', 'checkout.', 'actions.'];
const critical = mtKeys.filter(k => criticalPatterns.some(p => k.startsWith(p)));
const others = mtKeys.filter(k => !criticalPatterns.some(p => k.startsWith(p)));
// 关键 key 100% 审查,其他按比例抽样
const sampled = [
...critical,
...others.sort(() => Math.random() - 0.5).slice(0, Math.ceil(others.length * sampleRate))
];
return sampled.map(key => ({
key,
source: sourceTranslations[key],
machine: allTranslations[key],
status: 'pending_review',
}));
}
机器翻译的常见错误类型
| 错误类型 | 示例 | 解决方案 |
|---|---|---|
| 占位符位置错误 | {name} 你好 → Hello You {name} | 使用 XML 标签保护占位符 |
| 专业术语翻译错误 | "购物车" → "shopping bag" | 使用术语表 API |
| 语气不一致 | 部分用"您"部分用"你" | 在 glossary 中指定语气 |
| 截断 | 长句子截断 | 检查翻译结果长度 |
| 品牌名被翻译 | "Apple Store" → "苹果商店" | 术语表标记为 DoNotTranslate |
// 翻译后自动校验
function validateTranslation(source: string, translated: string): string[] {
const issues: string[] = [];
// 检查占位符完整性
const sourcePlaceholders = source.match(/\{[^}]+\}/g) ?? [];
const translatedPlaceholders = translated.match(/\{[^}]+\}/g) ?? [];
const missingPlaceholders = sourcePlaceholders.filter(p => !translatedPlaceholders.includes(p));
if (missingPlaceholders.length > 0) {
issues.push(`缺少占位符: ${missingPlaceholders.join(', ')}`);
}
// 检查长度(机器翻译不应超过原文 3 倍)
if (translated.length > source.length * 3) {
issues.push(`翻译结果异常长 (${translated.length} vs ${source.length})`);
}
return issues;
}
下一节:了解了直接调用 MT API,下面看如何通过 TMS 平台(Crowdin、Phrase、Lokalise)实现更完整的翻译工作流管理,包括 GitHub 自动同步。