三行代码完成国际化适配,妙~啊~

前言

国际化适配一直以来都是一个棘手的问题,尤其是在项目一开始没有考虑的情况下,我们需要修改大量源码,使用类似于 ${t.xxx} 的占位符去一一修改我们已经写好的文字(如最耳熟能详的vue-i18n)。这个工程量在项目后期是巨大的,令人无法接受的。

目前,网上有五花八门的国际化方案,但是大部分都只解决了基础问题------能用,但是都存在这个痛点------太麻烦了。

好,那么有没有一款插件,让我们不用自己动手做这件事呢?

有的兄弟有的

auto-i18n-translation-plugins 简介

wenps/auto-i18n-translation-plugins 正是这样一款通用插件

它最少只需要三行参数,像这样:

php 复制代码
const i18nPlugin = vitePluginsAutoI18n({   
    targetLangList: ['en', 'ko', 'ja'],
    translator: new YoudaoTranslator({     
        appId: '4xxxx9xxxx66fef',
        appKey: 'ONIxxxxGRxxxxw7UM730xxxxmB3j'
    })
})

然后,在 vite 的 plugins 中填入 i18nPlugin 即可。

像这样:

javascript 复制代码
import { defineConfig } from 'vite'
​
export default defineConfig({
    resolve: {},
    plugins: [i18nPlugin] //上面的对象
})

当插件运行成功后,会生成最终的语言包,在根目录下的 lang 文件夹,然后我们需要在入口处引入,以 vue 为例,在 main.ts 中引入

arduino 复制代码
// main.ts
import '../lang/index'

即可。

插件将在 localStorage 中获取到当前语言,所以切换语言时你只需:

javascript 复制代码
window.localStorage.setItem('lang', value) // 你在 targetLangList 参数中传入的字符串,如 'en'
window.location.reload()

当然,此插件同样支持 webpack、rollup

安装

arduino 复制代码
pnpm i vite-auto-i18n-plugin -D
arduino 复制代码
pnpm i webpack-auto-i18n-plugin -D

上面提到 YoudaoTranslator ,你需要申请自己的有道翻译 api key,或者使用代理使用免费的谷歌翻译(详见插件 readme.md)。

有道翻译 api 申请地址:ai.youdao.com/product-fan...

优点

此插件的和目前市面上的插件的根本区别在于,将翻译、文本替换这两步都自动化了,翻译是前置执行的,而替换过程是在构建过程中发生的,对于使用者来说是不可见且无需关心的,使用之后,项目中的任何文本都无需改动,且插件也不会去修改我们的代码,看起来一切如旧!妙哉

对于传统方式来说,使用此插件之后,工作量将降低 90% 以上。

由于机器翻译可能对于特定语境存在偏差,所以翻译可能不是 100% 准确,这时候我们可以手动去修改少量的翻译文本产物。

插件成功运行后,将在根目录下生成一个 lang 文件夹,lang/index.json 就是生成后的翻译。

tip: 由于 vite 的运行机制,使用 vite 时,需要先执行 npm run build,这样可以节省 api 用量。

它大概长这样:

json 复制代码
{
    "qylb2": {
        "zh-cn": "首页",
        "en": "Home page",
        "ko": "첫 페이지",
        "ja": "トップページです"
    },
    "dud62": {
        "zh-cn": "产品",
        "en": "product",
        "ko": "제품",
        "ja": "製品です"
    },
    "ea9n2": {
        "zh-cn": "关于",
        "en": "With regard to",
        "ko": "관",
        "ja": "についてです"
    }
}

qylb2 等 key 值是源文本的一个 hash,只要源文本不变,就不会重新翻译,所以我们可以自由修改语言的翻译结果,而不会使插件自动重新翻译。

当此文件内容不全,或源文本发生改变(hash发生改变),插件会在构建阶段重新补全(增量)。

auto-i18n-translation-plugins 已加入 auto-plugin 开源联盟 ,我们致力于打造足够 auto 的 JS 插件。

作者是 @wenps , Github主页:github.com/wenps

项目Github链接:github.com/wenps/auto-...

为了让兄弟们用插件时足够放心,接下来,将讲解 auto-i18n-translation-plugins 的具体原理,你也可以点击上方链接亲自阅读源码。

auto-i18n-translation-plugins 原理解析

本章由作者 @wenps 亲自编写,内容十分硬核,不想看的同学可以直接跳到文末查看示例。

它是如何找到要翻译的文本的?


在开始讨论如何定位需要翻译的文本前,我们首先需要理解 Babel 的核心机制。Babel 是一款 JavaScript 编译工具,它能够通过以下流程将代码转化为可操作的中间表示:


文案标记

  1. 解析(Parse) Babel 将输入的 JavaScript 代码解析为抽象语法树(AST) ,将代码结构分解为层级清晰的节点(Node)。例如,字符串字面量、模板字符串、JSX 元素等会转换为对应的 AST 节点类型(如 StringLiteral, TemplateLiteral, JSXText)以中文为例,一般中文就会出现在这些StringLiteral, TemplateLiteral, JSXTextast节点中,因此处理这些节点即可。

  2. 转换(Transform) 通过 Babel.transform 方法对来源语言可能出现的StringLiteral, TemplateLiteral, JSXText等AST节点进行深度遍历,通过这种手段去扫描目标文案:

    • 定位目标文本 :遍历 AST,筛选出StringLiteral, TemplateLiteral, JSXText等AST节点,如果来源语言是中文,那就匹配当前节点的值当前是否符合中文的正则,符合就往下走;
    • 过滤无需翻译的内容 :排除路径引用(import '/path')、对象键名({ key: 'value' })、注释等非内容文本;
    • 标记文本 :如果存在符合来源语言的正则,又不属于无需翻译的内容,就会对需要翻译的文本生成唯一哈希(为了保证不会出现重复翻译),并将其替换为翻译函数调用(如 $t('哈希值', '原始文本'))。
    • 全部文件遍历完之后,文案的标记就完成了
  3. 代码生成(Generate) 将修改后的 AST 转换回 JavaScript 代码,最终输出包含翻译标记的源文件。


文案收集

标记完之后还需要去将标记的文案和hash收集起来,因此我们会先生成一个全局变量,这里有两个方案:

  1. 遍历时同步收集

    • 流程 :在遍历 AST 标记文本时,将哈希值和原始文本实时存储到全局对象
    • 优点:仅需遍历一次,节省时间;
  2. 分步处理

    • 流程

      1. 首次遍历 :将文本替换为 $t 调用,不存储数据;
      2. 二次遍历 :专门收集所有 $t 调用中的参数(哈希和文本)存储到全局对象
    • 优点:逻辑分离清晰,避免副作用;

目前我们这里使用的就是方案二,完成这一步之后所有的待翻译文字就已经被存储到了全局对象中,接下来我们需要将这些文案进行翻译即可。


文案收集举例

原始代码:

arduino 复制代码
<Div>按钮文字</Div>
const message = '系统提示';
import 'styles.css'; // 路径无需翻译

经 Babel 插件处理后输出:

php 复制代码
_c('div', [$t('f8b7a1d', '按钮文字')]);
const message = $t('2e3c9a7', '系统提示');
import 'styles.css'; // 路径未被修改

最终遍历函数,读取hash和文字,并收集的全局对象中,就会得到一个映射表:

css 复制代码
{
  f8b7a1d: '按钮文字',
  2e3c9a7: '系统提示'
}

到这一步就完成了目标文案的函数转换和收集。


通过这一机制,开发者无需手动标记文本,Babel 能够自动化识别和准备需要翻译的内容,同时确保结构化代码的准确性。


它是如何进行翻译的?

在上面的描述中我们已经通过babel 完成文案的收集了,那我们怎么完成翻译呢?主要分成两步。

这里我做了两个翻译器(class),它们负责接收用户参数,以及进行接下来的操作。

第一步:实例化翻译器

插件默认暴露了两个可用的翻译器类和一个翻译器基类,可用的翻译器类分别包括有道翻译器类和谷歌翻译器类,这两个类实例化即可使用,实例化代码如下:

有道翻译:(强烈推荐有道翻译)

arduino 复制代码
new YoudaoTranslator({     
	appId: '4xxxx9xxxx66fef',
	appKey: 'ONIxxxxGRxxxxw7UM730xxxxmB3j'
})

谷歌翻译:

yaml 复制代码
new GoogleTranslator({
  proxyOption: { // 国内使用需要配置代理
    host: '127.0.0.1',
    port: 8899,
    headers: {
      'User-Agent': 'Node'
    }
  }
})

通过内置的translate函数进行翻译。

谷歌翻译和有道翻译都是继承于翻译器基类:

Translator 是一个封装翻译功能的核心类,用于通过配置好的翻译 API(如机器翻译服务)将文本从源语言转换为目标语言。其设计目标是标准化翻译调用流程管理 API 请求频率 并提供错误处理机制。(更详细内容可以去看github源码,有相关的类型标识)

Translator 源码

typescript 复制代码
export class Translator {
    protected option: TranslatorOption

    constructor(option: TranslatorOption) {
        this.option = option
        if (this.option.interval) {
            this.option.fetchMethod = interval(this.option.fetchMethod, this.option.interval)
        }
    }

    protected getErrorMessage(error: unknown) {
        if (error instanceof Error) {
            return error.message
        } else {
            return String(error)
        }
    }

    async translate(text: string, fromKey: string, toKey: string) {
        let result = ''
        try {
            result = await this.option.fetchMethod(text, fromKey, toKey)
        } catch (error) {
            const name = this.option.name
            console.error(
                `翻译api${name ? `【${name}】` : ''}请求异常:${this.getErrorMessage(error)}`
            )
        }
        return result
    }
}

所以,好像你也可以做一个自定义的 CustomTranslator

第二步:翻译目标语言

实例化翻译器之后就需要对我们扫描出来的文案进行翻译了,下面是具体的步骤


1. 找出需要翻译的新文案
  • 第一步:读取两份数据:

    • 全局对象 :代码中所有文案生成的全局对象(比如 "hash1" →"确定", "hash2" → "你好")。
    • 已翻译的旧文件(运行插件的时候会生成一个index.json, 里面存放的就是旧的翻译内容) :之前翻译好的结果(比如 hash1 的中英文都有了,但 hash2 还没翻译)。
  • 筛选规则 :找出旧文件中没有翻译过 的文案(如 hash2 的"你好"需要翻译成英文等)并将这个没翻译的重新存储在一个临时对象中。

css 复制代码
{
	hash2: 你好
}

2. 合并文案方便一次性翻译
  • 操作 :通过key去读取临时对象,把需要翻译的原文用符号 \n┋┋┋\n 连起来,变成一个长文本。

    • 例子

      • 原始文案列表: ["你好", "欢迎来到系统"]

      • 合并后的长文本:

        你好\n┋┋┋\n欢迎来到系统  
        
    • 为什么这么做? :把多个短文案合并成一个长文本,可以一次批量翻译,减少多次调用翻译接口的时间,而且通过 key 去读取,可以保证顺序的一致性,因为 key 不会变。


3. 分语言翻译并拆分结果
  • 步骤

    1. 选择目标语言:比如要翻译成英文、韩语。

    2. 逐个翻译

      • 对合并后的长文本调用翻译器实例的翻译函数,设置语言参数(如英文、韩语)。

      • 结果示例

        • 英文翻译 → Hello\n┋┋┋\nWelcome to the system
        • 韩语翻译 → 안녕하세요\n┋┋┋\n시스템에 오신 것을 환영합니다
    3. 拆分结果 :根据 \n┋┋┋\n 符号,把翻译后的文本切回一个个单独文案。例如:

      • 英文结果 → [ "Hello", "Welcome..." ]
      • 韩语结果 → [ "안녕하세요", ... ] 值得注意的时候此时的数组顺序,和我们生成的临时对象变量key的顺序是一致的

4. 匹配原文顺序,更新翻译映射表
  • 关键点

    • 因为合并时保持了原文档的顺序(如按哈希值 hash2排列),拆分后的翻译结果也能按顺序对应原文。

    • 操作

      1. 新建临时存储对象,把翻译结果按哈希值归类。例如:

        json 复制代码
        {  
          "hash2": {  
            "zh": "你好",    // 原文(不翻译)  
            "en": "Hello",   // 新翻译的英文  
            "ko": "안녕하세요", // 新翻译的韩语  
          },  
          ...  
        }  
      2. 合并到旧文件:把临时对象中的新翻译内容,追加到已有的映射表中。

        • 最终效果

          json 复制代码
          {  
            "hash1": { "zh": "确定", "en": "Confirm" },  // 已有的旧数据  
            "hash2": { "zh": "你好", "en": "Hello", "ko": "안녕하세요" } // 新增翻译  
          }  

          3.合并完之后重新写入

      • 将合并完之后的对象重新写入到配置文件中即可,完成文案的翻译

通过这种流程,新增文案会自动被翻译并整合到文件里,同时保证已翻译的内容不受影响,整个过程就像"把碎片拼成整张画 → 一起翻译 → 再分开展示"一样简单。

它是如何处理新增的语言和增量文案?


新加语言

当需要新增目标语言(如从「中→英」扩展为「中→英→韩」),无需手动编辑映射表

插件启动时会自动检查当前配置语言列表 (如 zh, en, ko)与映射表内已有语言 (如存在 zhen)。若发现新增语言未初始化 (如 ko),则触发自动补全流程------

具体步骤

  1. 提取源语言文案 :直接从映射表中拉取原始语言 (如中文)的所有纯文本内容(如 "确定","韩文")。

  2. 批量翻译新增语言 :通过合并符 \n┇┇┇\n 拼接原始语言,(如确定\n┇┇┇\n韩文),将文案统一翻译为目标语言(如韩语 확인\n┇┇┇\n한글),重新按照合并符 \n┇┇┇\n进行切割,就可以得到新增语言的翻译如:확인, 한글

  3. 追加语言数据 :将新翻译结果直接写入映射表对应位置,例如:

    json 复制代码
    "确认按钮": {  
      "zh": "确定",  
      "en": "Confirm",  
      "ko": "확인"  
    }  

此过程完全自动化,开发者只需在配置中添加目标语言代码,插件即可无缝扩展语言包,无需手动维护映射表或担忧变量错位问题。

新加文案

  • 每次代码编译时,插件会扫描所有文本(如 "新按钮"),自动生成翻译函数调用(如 $t('哈希', '新按钮'))。
  • 这一过程完全无感,开发者无需手动标注新文案。

  • 插件将新文本的哈希值原始内容写入全局映射表时,会先判断该哈希是否已存在:

    • 若不存在 :追加新条目(如 "哈希": "新按钮")。
    • 若已存在:跳过写入,避免覆盖已有翻译记录。

  • 在翻译阶段,插件会比对:

    • 当前代码中的全局映射表(存储文本和hash的全局变量)
    • 已有翻译配置文件( index.json)。
  • 自动筛选出未翻译的新增文案,仅对它们触发翻译流程。

  • 然后对翻译结果进行切割,并重新写入到翻译配置文件中。


示例场景

原始映射表

json 复制代码
{
	"确定按钮hash": { "zh": "确定", "en": "Confirm" }  
}

新增代码文案<button>重置</button> 插件动作

  1. 自动生成 $t('新哈希', '重置')
  2. 更新映射表:
json 复制代码
  {
	"新哈希": { "zh": "重置" }, // 中文自动填充,其他语言待翻译  
  }
  1. 翻译提示 :只需补充英文 "Reset"、日文 "リセット" 等,无需处理已存在的"确定"按钮。

通过以上分步设计,开发者可专注于代码开发,翻译工作仅聚焦于真正新增的内容。


它如何使结果回显到页面上的?

通过上面的内容我们已经成功的将待翻译的文本转换成了翻译函数调用, 并且通过翻译实例将待翻译的文本进行了翻译,那么接下来我们需要将翻译的结果回显到页面上。

不妨看看编译后的内容长什么样:

php 复制代码
_c('div', [$t('f8b7a1d', '按钮文字')]);
const message = $t('2e3c9a7', '系统提示');
import 'styles.css'; // 路径未被修改

因此为了使其回显到页面上,我们需要做的就是将全局的$t实现即可

下面通过源码来介绍:(这个文件要在项目首行引入)

javascript 复制代码
	// 导入插件生成的国际化JSON文件
    import langJSON from './index.json'
    (function () {
    // 定义翻译函数
    let $t = function (key, val, nameSpace) {
      // 获取指定命名空间下的语言包
      const langPackage = $t[nameSpace];
      // 返回翻译结果,如果不存在则返回默认值
      return (langPackage || {})[key] || val;
    };
    // 定义设置语言包的方法
    $t.locale = function (locale, nameSpace) {
      // 将指定命名空间下的语言包设置为传入的locale
      $t[nameSpace] = locale || {};
    };
    // 将翻译函数挂载到window对象上,如果已经存在则使用已有的
    window.$t = window.$t || $t;
    // 将简单翻译函数挂载到window对象上
    window.$$t = $$t;
    // 定义从JSON文件中获取指定键的语言对象的方法
    window._getJSONKey = function (key, insertJSONObj = undefined) {
        // 获取JSON对象
        const JSONObj = insertJSONObj;
        // 初始化语言对象
        const langObj = {};
        // 遍历JSON对象的所有键
        Object.keys(JSONObj).forEach((value) => {
            // 将每个语言的对应键值添加到语言对象中
            langObj[value] = JSONObj[value][key];
        });
        // 返回语言对象
        return langObj;
    };
    })();
    // 定义语言映射对象
    const langMap = {
		// 根据插件的配置来生成语言map
        'en': window?.lang?.en || window._getJSONKey('en', langJSON),
		'ko': window?.lang?.ko || window._getJSONKey('ko', langJSON),
		'zhcn': window?.lang?.zhcn || window._getJSONKey('zhcn', langJSON)
    };
    // 从本地存储中获取当前语言,如果不存在则使用源语言
    const lang = window.localStorage.getItem('lang') || 'zhcn';
    // 根据当前语言设置翻译函数的语言包
    window.$t.locale(langMap[lang], 'lang');
  

通过阅读上面的代码可以看到,为了回显到页面上,我们会导入生成的翻译json,通过window.$t.locale(langMap[lang], 'lang')将对应的语言包设置到翻译函数上,这样就可以在页面上使用$t函数进行翻译了。

它为什么不用影响现有代码?

插件基于编译时AST语法树分析 实现无感化改造:在代码构建阶段,通过解析源代码的抽象语法树(AST),精准定位需翻译的文本内容,智能替换为指定翻译函数(如 $t('哈希','原始文本'))。 同时,该过程会动态归集 所有需翻译的文案数据,生成映射表(index.json)。这一处理完全运行于构建流程之中,既不修改源代码文件的原始结构,也不会对运行期JavaScript逻辑产生任何干扰,确保开发与生产环境的稳定性。

bash 复制代码
(例如对 `<div>文本</div>` 自动转译为 `$t('hash','文本')`,但原始源代码文件保持不变,开发调试时仍可直接查看原生字符串内容)

它为什么可以兼容全部前端框架?


auto-i18n-translation-plugins 的设计建立在框架无关的后期处理 原则之上。由于所有前端框架(如 Vue、React、Svelte 等)最终都会将其自定义语法(模板、组件、JSX 等)编译为标准 JavaScript 代码 ,而该插件的文本提取与翻译函数替换逻辑被明确置于构建流程的最后阶段执行。这一策略的核心在于:

  • 开发者框架的各类解析和编译操作(如 Vue 的模板编译、React 的 JSX 转义)均在插件运行前完成;
  • 插件直接处理最终的纯 JavaScript 代码,无需理解具体框架的内部语法或结构;
  • 只要确保插件在构建管线的最后阶段生效 (如 Webpack 的 loader 排序、Vite 的 plugin 配置顺序),即可兼容所有符合标准编译流程的前端框架。

效果 : 开发者只需将插件配置为构建流程的收尾环节,即可无感支持 Vue + TS、React + SWC、纯 JS 项目等任意技术栈,无需为不同框架单独配置适配层。


仓库地址和案例

Github:wenps/auto-i18n-translation-plugins

NPM vite 版: www.npmjs.com/package/vit...

NPM webpack 版:www.npmjs.com/package/web...

案例:github.com/wenps/auto-...

TODO

  • ssr 全自动支持,目前需要手动适配
  • 自动引入 lang/index.js
相关推荐
Moment1 小时前
从方案到原理,带你从零到一实现一个 前端白屏 检测的 SDK ☺️☺️☺️
前端·javascript·面试
鱼樱前端2 小时前
Vue3 + TypeScript 整合 MeScroll.js 组件
前端·vue.js
拉不动的猪2 小时前
刷刷题29
前端·vue.js·面试
野生的程序媛2 小时前
重生之我在学Vue--第5天 Vue 3 路由管理(Vue Router)
前端·javascript·vue.js
鱼樱前端2 小时前
Vue 2 与 Vue 3 响应式原理详细对比
javascript·vue.js
codingandsleeping2 小时前
前端工程化之模块化
前端·javascript
CodeCraft Studio2 小时前
报表控件stimulsoft操作:使用 Angular 应用程序的报告查看器组件
前端·javascript·angular.js
阿丽塔~3 小时前
面试题之vue和react的异同
前端·vue.js·react.js·面试
Liigo3 小时前
初次体验Tauri和Sycamore(3)通道实现
javascript·rust·electron·tauri·channel·sycamore
烛阴4 小时前
JavaScript 性能提升秘籍:WeakMap 和 WeakSet 你用对了吗?
前端·javascript