通用基础项目
6. I18n完善与SEO
会员专享 · 非会员仅可阅读 30% 的正文。
- 发布时间
- May 17, 2025
- 阅读时间
- 5 min read
- 作者
- Felix
- 访问
- 会员专享
这是预览内容
非会员仅可阅读 30% 的正文。
在上章中,我们讲了一下I18n的一些配置。那么这一章,我们需要继续完善I18n的自动化脚本,以及完成I18n的SEO配置。
自动化翻译的问题
那我们先来讲讲问题吧。这一节的版本是 github tag v2.2.1。
full模式
首先问题最大的是full模式,也许它不应该叫full,应该叫reset, 因为 missing 模式已经能够高效地处理增量翻译需求。保留 full 模式的唯一理由是在需要完全重置翻译时使用,这种情况应该很少见,且要避免使用,因为在后期这样做所需的上下文实在是太大了,并且有风险。
但最现实的问题是AI输出这么多的全量内容,根本无法使用JSON.parse去转成Json,内容越多,要输出固定格式化的内容就越难,因此我直接把full模式去掉了,默认就是 missing模式。
"i18n:translate": "tsx scripts/18n/cli.ts --mode=missing",
"i18n:keys": "tsx scripts/18n/cli.ts --mode=keys --keys",
"i18n:list": "tsx scripts/18n/cli.ts --list-locales"
```
### missing模式的token消耗
在使用 AI 模型进行翻译时,每个请求都会消耗 token。Token 是文本的基本单位,通常一个单词会被分解为一个或多个 token。
当前的翻译实现存在严重的 token 消耗问题,主要体现在:
1. 重复的提示模板:对每种语言都使用相同的冗长提示
2. 单语言单次请求:每种语言单独发送一个请求,无法共享上下文
3. 多次 API 调用:每个语言一次 API 调用,增加了延迟和成本
解法是使用所有语言缺失键的并集作为源文本进行翻译:
1. **批量处理翻译请求**:通过将所有语言的缺失键合并为一个集合,我们可以在一次API调用中请求多种语言的翻译,而不是为每种语言单独发送请求。
2. **减少重复内容**:当多种语言都缺少相同的键时,我们只需要将这些键的英文原文发送一次,而不是每种语言都重复发送。
3. **共享上下文**:AI模型可以在一次请求中理解所有需要翻译的内容,这有助于保持翻译的一致性,特别是对于相关术语。
4. **降低API调用成本**:减少API调用次数不仅降低了延迟,还显著减少了token消耗,因为我们不再需要为每种语言重复发送相同的提示模板。
例如,假设我们有三种语言(中文、日语和韩语),每种语言都缺少5个相同的键。使用旧方法,我们需要发送3个独立请求,每个请求都包含完整的提示模板和5个键的内容。而使用新方法,我们只需发送1个请求,包含一次提示模板和5个键的内容,然后请求AI模型同时生成所有三种语言的翻译。
> 那有没有更好的做法那?显然有的,就是添加一张key + 语言的对照表,一个key对应所有语言的翻译,每次以key跟en.json进行对照,始终以对照表为主,删除对照表的一行,就删除所有key的一行,对照表的一行中有任意一列为空,就重新以AI生成。现在我的主项目就是这么做的,前后端+本土的小伙伴一起去运营这张key+语言的对照表。但这套方案是太复杂了,不适合单人开发。
#### 优化后的翻译流程
1. 收集所有目标语言的缺失键
2. 计算所有缺失键的并集
3. 从英文消息中提取这些键的值
4. 构建一个包含所有缺失键和目标语言的单一请求
5. 发送请求到AI模型,获取所有语言的翻译结果
6. 解析结果并更新各语言的翻译文件
这种方法不仅提高了效率,还大大降低了API调用的成本,特别是在处理多语言项目时。主要的核心改动代码:
```typescript
case 'missing':
// 收集所有语言的缺失键
for (const locale of localesToTranslate) {
let existingTranslations = {}
const localeFilePath = path.join(messagesDir, `${locale.code}.json`)
try {
const existingContent = await fs.readFile(localeFilePath, 'utf-8')
existingTranslations = JSON.parse(existingContent)
} catch (err) {
console.log(`未找到 ${locale.code} 的现有翻译,将创建新文件。`)
}
const missingKeys = findMissingKeys(englishMessages, existingTranslations)
if (missingKeys.length > 0) {
missingKeysByLocale[locale.code] = missingKeys
}
}
// 如果所有语言都没有缺失键,则提前返回
if (Object.keys(missingKeysByLocale).length === 0) {
return localesToTranslate.map((locale) => ({
success: true,
locale: locale.code,
message: `${locale.name} 没有发现缺失的键`,
translatedKeys: []
}))
}
// 使用所有缺失键的并集作为源
const allMissingKeys = [...new Set(Object.values(missingKeysByLocale).flat())]
sourceToTranslate = extractKeys(englishMessages, allMissingKeys)
break
```
> 在当前的项目中,不要写大量的翻译文案后,再去跑`pnpm i18n:translate`,如果遇见了大量翻译不能保持JSON结构的情况,可以采用分批跑的方式。
### 不需要翻译的Key
显然我们还需要维护一个不需要翻译的Key列表,因为类似品牌名、公司信息等值其实是不需要翻译的,只需要把`en`的值填入到其他的json翻译文件中。
### 上下文问题
在做AI翻译的时候,有一个没办法避免的问题,那就是`max tokens`超限,这是一次响应中最多可以生成的`token`限制,大家其实如果经常问题AI问题,有时候也会遇到,而这种问题在面临长文本翻译的时候格外的明显。
而解决方案是采用批处理的方式,每一批设置几个`key`,使用`key`模式,循环往下,直到翻译完成,大家可以执行`pnpm i18n:sequential`。
```typescript
/**
* 顺序翻译所有键
* @param options 翻译选项
*/
export async function sequentialTranslate(options: Omit<TranslationOptions, 'mode' | 'keys'> = {}): Promise<void> {
try {
// 读取英文消息文件(作为基准)
const messagesDir = path.join(process.cwd(), 'messages')
const englishMessagesPath = path.join(messagesDir, 'en.json')
const englishMessagesText = await fs.readFile(englishMessagesPath, 'utf-8')
const englishMessages = JSON.parse(englishMessagesText)
// 确定要翻译的目标语言
const { targetLocales } = options
const localesToTranslate = targetLocales
? locales.filter((l) => targetLocales.includes(l.code) && l.code !== 'en')
: locales.filter((l) => l.code !== 'en')
if (localesToTranslate.length === 0) {
console.log('没有找到要翻译的目标语言')
return
}
// 提取英文文件中的所有键
const allKeys = extractAllKeys(englishMessages)
console.log(`英文文件中共有 ${allKeys.length} 个键`)
let allMissingKeys: string[] =会员专享
订阅后解锁完整文章
支持创作、解锁全文,未来更新也会第一时间送达。
评论
加入讨论
登录后评论
还没有评论,来占个沙发吧。