通用基础项目

    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[] =
    会员专享

    订阅后解锁完整文章

    支持创作、解锁全文,未来更新也会第一时间送达。

    评论

    加入讨论

    0 条评论
    登录后评论

    还没有评论,来占个沙发吧。