三行代碼完成國際化適配,妙~啊~
前言
國際化適配一直以來都是一個棘手的問題,尤其是在項目一開始沒有考慮的情況下,我們需要修改大量源碼,使用類似于 ${t.xxx} 的占位符去一一修改我們已經(jīng)寫好的文字(如最耳熟能詳?shù)膙ue-i18n)。這個工程量在項目后期是巨大的,令人無法接受的。
目前,網(wǎng)上有五花八門的國際化方案,但是大部分都只解決了基礎(chǔ)問題——能用,但是都存在這個痛點——太麻煩了。
好,那么有沒有一款插件,讓我們不用自己動手做這件事呢?
有的兄弟有的
auto-i18n-translation-plugins 簡介
wenps/auto-i18n-translation-plugins 正是這樣一款通用插件
它最少只需要三行參數(shù),像這樣:
const i18nPlugin = vitePluginsAutoI18n({
targetLangList: ['en', 'ko', 'ja'],
translator: new YoudaoTranslator({
appId: '4xxxx9xxxx66fef',
appKey: 'ONIxxxxGRxxxxw7UM730xxxxmB3j'
})
})
然后,在 vite 的 plugins 中填入 i18nPlugin 即可。
像這樣:
import { defineConfig } from 'vite'
?
export default defineConfig({
resolve: {},
plugins: [i18nPlugin] //上面的對象
})
當(dāng)插件運行成功后,會生成最終的語言包,在根目錄下的 lang 文件夾,然后我們需要在入口處引入,以 vue 為例,在 main.ts 中引入
// main.ts
import '../lang/index'
即可。
插件將在 localStorage 中獲取到當(dāng)前語言,所以切換語言時你只需:
window.localStorage.setItem('lang', value) // 你在 targetLangList 參數(shù)中傳入的字符串,如 'en'
window.location.reload()
當(dāng)然,此插件同樣支持 webpack、rollup
安裝
pnpm i vite-auto-i18n-plugin -D
pnpm i webpack-auto-i18n-plugin -D
上面提到 YoudaoTranslator ,你需要申請自己的有道翻譯 api key,或者使用代理使用免費的谷歌翻譯(詳見插件 readme.md)。
有道翻譯 api 申請地址:https://ai.youdao.com/product-fanyi-text.s
優(yōu)點
此插件的和目前市面上的插件的根本區(qū)別在于,將翻譯、文本替換這兩步都自動化了,翻譯是前置執(zhí)行的,而替換過程是在構(gòu)建過程中發(fā)生的,對于使用者來說是不可見且無需關(guān)心的,使用之后,項目中的任何文本都無需改動,且插件也不會去修改我們的代碼,看起來一切如舊!妙哉。
對于傳統(tǒng)方式來說,使用此插件之后,工作量將降低 90% 以上。
由于機器翻譯可能對于特定語境存在偏差,所以翻譯可能不是 100% 準確,這時候我們可以手動去修改少量的翻譯文本產(chǎn)物。
插件成功運行后,將在根目錄下生成一個 lang 文件夾,lang/index.json 就是生成后的翻譯。
tip: 由于 vite 的運行機制,使用 vite 時,需要先執(zhí)行 npm run build,這樣可以節(jié)省 api 用量。
它大概長這樣:
{
"qylb2": {
"zh-cn": "首頁",
"en": "Home page",
"ko": "? ???",
"ja": "トップページです"
},
"dud62": {
"zh-cn": "產(chǎn)品",
"en": "product",
"ko": "??",
"ja": "製品です"
},
"ea9n2": {
"zh-cn": "關(guān)于",
"en": "With regard to",
"ko": "?",
"ja": "についてです"
}
}
qylb2 等 key 值是源文本的一個 hash,只要源文本不變,就不會重新翻譯,所以我們可以自由修改語言的翻譯結(jié)果,而不會使插件自動重新翻譯。
當(dāng)此文件內(nèi)容不全,或源文本發(fā)生改變(hash發(fā)生改變),插件會在構(gòu)建階段重新補全(增量)。
auto-i18n-translation-plugins 已加入 auto-plugin 開源聯(lián)盟 ,我們致力于打造足夠 auto 的 JS 插件。
作者是 @wenps , Github主頁:https://github.com/wenps
項目Github鏈接:https://github.com/wenps/auto-i18n-translation-plugins
為了讓兄弟們用插件時足夠放心,接下來,將講解 auto-i18n-translation-plugins 的具體原理,你也可以點擊上方鏈接親自閱讀源碼。
auto-i18n-translation-plugins 原理解析
本章由作者 @wenps 親自編寫,內(nèi)容十分硬核,不想看的同學(xué)可以直接跳到文末查看示例。
它是如何找到要翻譯的文本的?
在開始討論如何定位需要翻譯的文本前,我們首先需要理解 Babel 的核心機制。Babel 是一款 JavaScript 編譯工具,它能夠通過以下流程將代碼轉(zhuǎn)化為可操作的中間表示:
文案標記
-
解析(Parse) Babel 將輸入的 JavaScript 代碼解析為抽象語法樹(AST) ,將代碼結(jié)構(gòu)分解為層級清晰的節(jié)點(Node)。例如,字符串字面量、模板字符串、JSX 元素等會轉(zhuǎn)換為對應(yīng)的 AST 節(jié)點類型(如
StringLiteral,TemplateLiteral,JSXText)以中文為例,一般中文就會出現(xiàn)在這些StringLiteral,TemplateLiteral,JSXTextast節(jié)點中,因此處理這些節(jié)點即可。 -
轉(zhuǎn)換(Transform) 通過
Babel.transform方法對來源語言可能出現(xiàn)的StringLiteral,TemplateLiteral,JSXText等AST節(jié)點進行深度遍歷,通過這種手段去掃描目標文案:- 定位目標文本:遍歷 AST,篩選出
StringLiteral,TemplateLiteral,JSXText等AST節(jié)點,如果來源語言是中文,那就匹配當(dāng)前節(jié)點的值當(dāng)前是否符合中文的正則,符合就往下走; - 過濾無需翻譯的內(nèi)容:排除路徑引用(
import '/path')、對象鍵名({ key: 'value' })、注釋等非內(nèi)容文本; - 標記文本:如果存在符合來源語言的正則,又不屬于無需翻譯的內(nèi)容,就會對需要翻譯的文本生成唯一哈希(為了保證不會出現(xiàn)重復(fù)翻譯),并將其替換為翻譯函數(shù)調(diào)用(如
$t('哈希值', '原始文本'))。 - 全部文件遍歷完之后,文案的標記就完成了
- 定位目標文本:遍歷 AST,篩選出
-
代碼生成(Generate) 將修改后的 AST 轉(zhuǎn)換回 JavaScript 代碼,最終輸出包含翻譯標記的源文件。
文案收集
標記完之后還需要去將標記的文案和hash收集起來,因此我們會先生成一個全局變量,這里有兩個方案:
-
遍歷時同步收集
- 流程:在遍歷 AST 標記文本時,將哈希值和原始文本實時存儲到全局對象。
- 優(yōu)點:僅需遍歷一次,節(jié)省時間;
-
分步處理
-
流程:
- 首次遍歷:將文本替換為
$t調(diào)用,不存儲數(shù)據(jù); - 二次遍歷:專門收集所有
$t調(diào)用中的參數(shù)(哈希和文本)存儲到全局對象。
- 首次遍歷:將文本替換為
-
優(yōu)點:邏輯分離清晰,避免副作用;
-
目前我們這里使用的就是方案二,完成這一步之后所有的待翻譯文字就已經(jīng)被存儲到了全局對象中,接下來我們需要將這些文案進行翻譯即可。
文案收集舉例
原始代碼:
<Div>按鈕文字</Div>
const message = '系統(tǒng)提示';
import 'styles.css'; // 路徑無需翻譯
經(jīng) Babel 插件處理后輸出:
_c('div', [$t('f8b7a1d', '按鈕文字')]);
const message = $t('2e3c9a7', '系統(tǒng)提示');
import 'styles.css'; // 路徑未被修改
最終遍歷函數(shù),讀取hash和文字,并收集的全局對象中,就會得到一個映射表:
{
f8b7a1d: '按鈕文字',
2e3c9a7: '系統(tǒng)提示'
}
到這一步就完成了目標文案的函數(shù)轉(zhuǎn)換和收集。
通過這一機制,開發(fā)者無需手動標記文本,Babel 能夠自動化識別和準備需要翻譯的內(nèi)容,同時確保結(jié)構(gòu)化代碼的準確性。
它是如何進行翻譯的?
在上面的描述中我們已經(jīng)通過babel 完成文案的收集了,那我們怎么完成翻譯呢?主要分成兩步。
這里我做了兩個翻譯器(class),它們負責(zé)接收用戶參數(shù),以及進行接下來的操作。
第一步:實例化翻譯器
插件默認暴露了兩個可用的翻譯器類和一個翻譯器基類,可用的翻譯器類分別包括有道翻譯器類和谷歌翻譯器類,這兩個類實例化即可使用,實例化代碼如下:
有道翻譯:(強烈推薦有道翻譯)
new YoudaoTranslator({
appId: '4xxxx9xxxx66fef',
appKey: 'ONIxxxxGRxxxxw7UM730xxxxmB3j'
})
谷歌翻譯:
new GoogleTranslator({
proxyOption: { // 國內(nèi)使用需要配置代理
host: '127.0.0.1',
port: 8899,
headers: {
'User-Agent': 'Node'
}
}
})
通過內(nèi)置的translate函數(shù)進行翻譯。
谷歌翻譯和有道翻譯都是繼承于翻譯器基類:
Translator 是一個封裝翻譯功能的核心類,用于通過配置好的翻譯 API(如機器翻譯服務(wù))將文本從源語言轉(zhuǎn)換為目標語言。其設(shè)計目標是標準化翻譯調(diào)用流程、管理 API 請求頻率并提供錯誤處理機制。(更詳細內(nèi)容可以去看github源碼,有相關(guān)的類型標識)
Translator 源碼
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. 找出需要翻譯的新文案
-
第一步:讀取兩份數(shù)據(jù):
- 全局對象:代碼中所有文案生成的全局對象(比如
"hash1" →"確定", "hash2" → "你好")。 - 已翻譯的舊文件(運行插件的時候會生成一個index.json, 里面存放的就是舊的翻譯內(nèi)容) :之前翻譯好的結(jié)果(比如 hash1 的中英文都有了,但 hash2 還沒翻譯)。
- 全局對象:代碼中所有文案生成的全局對象(比如
-
篩選規(guī)則:找出舊文件中沒有翻譯過的文案(如 hash2 的“你好”需要翻譯成英文等)并將這個沒翻譯的重新存儲在一個臨時對象中。
{ hash2: 你好 }
2. 合并文案方便一次性翻譯
-
操作:通過key去讀取臨時對象,把需要翻譯的原文用符號
\n┋┋┋\n連起來,變成一個長文本。-
例子:
-
原始文案列表:
["你好", "歡迎來到系統(tǒng)"] -
合并后的長文本:
你好\n┋┋┋\n歡迎來到系統(tǒng)
-
-
為什么這么做? :把多個短文案合并成一個長文本,可以一次批量翻譯,減少多次調(diào)用翻譯接口的時間,而且通過 key 去讀取,可以保證順序的一致性,因為 key 不會變。
-
3. 分語言翻譯并拆分結(jié)果
-
步驟:
-
選擇目標語言:比如要翻譯成英文、韓語。
-
逐個翻譯:
-
對合并后的長文本調(diào)用翻譯器實例的翻譯函數(shù),設(shè)置語言參數(shù)(如英文、韓語)。
-
結(jié)果示例:
- 英文翻譯 →
Hello\n┋┋┋\nWelcome to the system - 韓語翻譯 →
?????\n┋┋┋\n???? ?? ?? ?????
- 英文翻譯 →
-
-
拆分結(jié)果:根據(jù)
\n┋┋┋\n符號,把翻譯后的文本切回一個個單獨文案。例如:- 英文結(jié)果 →
[ "Hello", "Welcome..." ] - 韓語結(jié)果 →
[ "?????", ... ]值得注意的時候此時的數(shù)組順序,和我們生成的臨時對象變量key的順序是一致的
- 英文結(jié)果 →
-
4. 匹配原文順序,更新翻譯映射表
-
關(guān)鍵點:
-
因為合并時保持了原文檔的順序(如按哈希值
hash2排列),拆分后的翻譯結(jié)果也能按順序?qū)?yīng)原文。 -
操作:
-
新建臨時存儲對象,把翻譯結(jié)果按哈希值歸類。例如:
{ "hash2": { "zh": "你好", // 原文(不翻譯) "en": "Hello", // 新翻譯的英文 "ko": "?????", // 新翻譯的韓語 }, ... } -
合并到舊文件:把臨時對象中的新翻譯內(nèi)容,追加到已有的映射表中。
-
最終效果:
{ "hash1": { "zh": "確定", "en": "Confirm" }, // 已有的舊數(shù)據(jù) "hash2": { "zh": "你好", "en": "Hello", "ko": "?????" } // 新增翻譯 }3.合并完之后重新寫入
-
- 將合并完之后的對象重新寫入到配置文件中即可,完成文案的翻譯
-
-
通過這種流程,新增文案會自動被翻譯并整合到文件里,同時保證已翻譯的內(nèi)容不受影響,整個過程就像“把碎片拼成整張畫 → 一起翻譯 → 再分開展示”一樣簡單。
它是如何處理新增的語言和增量文案?
新加語言
當(dāng)需要新增目標語言(如從「中→英」擴展為「中→英→韓」),無需手動編輯映射表:
插件啟動時會自動檢查當(dāng)前配置語言列表(如 zh, en, ko)與映射表內(nèi)已有語言(如存在 zh 和 en)。若發(fā)現(xiàn)新增語言未初始化(如 ko),則觸發(fā)自動補全流程——
具體步驟:
-
提取源語言文案:直接從映射表中拉取原始語言(如中文)的所有純文本內(nèi)容(如
"確定","韓文")。 -
批量翻譯新增語言:通過合并符
\n┇┇┇\n拼接原始語言,(如確定\n┇┇┇\n韓文),將文案統(tǒng)一翻譯為目標語言(如韓語??\n┇┇┇\n??),重新按照合并符\n┇┇┇\n進行切割,就可以得到新增語言的翻譯如:??,??。 -
追加語言數(shù)據(jù):將新翻譯結(jié)果直接寫入映射表對應(yīng)位置,例如:
"確認按鈕": { "zh": "確定", "en": "Confirm", "ko": "??" }
此過程完全自動化,開發(fā)者只需在配置中添加目標語言代碼,插件即可無縫擴展語言包,無需手動維護映射表或擔(dān)憂變量錯位問題。
新加文案
- 每次代碼編譯時,插件會掃描所有文本(如
"新按鈕"),自動生成翻譯函數(shù)調(diào)用(如$t('哈希', '新按鈕'))。 - 這一過程完全無感,開發(fā)者無需手動標注新文案。
-
插件將新文本的哈希值與原始內(nèi)容寫入全局映射表時,會先判斷該哈希是否已存在:
- 若不存在:追加新條目(如
"哈希": "新按鈕")。 - 若已存在:跳過寫入,避免覆蓋已有翻譯記錄。
- 若不存在:追加新條目(如
-
在翻譯階段,插件會比對:
- 當(dāng)前代碼中的全局映射表(存儲文本和hash的全局變量)
- 已有翻譯配置文件(
index.json)。
-
自動篩選出未翻譯的新增文案,僅對它們觸發(fā)翻譯流程。
-
然后對翻譯結(jié)果進行切割,并重新寫入到翻譯配置文件中。
示例場景
原始映射表
{ "確定按鈕hash": { "zh": "確定", "en": "Confirm" } }新增代碼文案:
<button>重置</button>插件動作:
- 自動生成
$t('新哈希', '重置')。- 更新映射表:
{ "新哈希": { "zh": "重置" }, // 中文自動填充,其他語言待翻譯 }
- 翻譯提示:只需補充英文
"Reset"、日文"リセット"等,無需處理已存在的“確定”按鈕。
通過以上分步設(shè)計,開發(fā)者可專注于代碼開發(fā),翻譯工作僅聚焦于真正新增的內(nèi)容。
它如何使結(jié)果回顯到頁面上的?
通過上面的內(nèi)容我們已經(jīng)成功的將待翻譯的文本轉(zhuǎn)換成了翻譯函數(shù)調(diào)用, 并且通過翻譯實例將待翻譯的文本進行了翻譯,那么接下來我們需要將翻譯的結(jié)果回顯到頁面上。
不妨看看編譯后的內(nèi)容長什么樣:
_c('div', [$t('f8b7a1d', '按鈕文字')]);
const message = $t('2e3c9a7', '系統(tǒng)提示');
import 'styles.css'; // 路徑未被修改
因此為了使其回顯到頁面上,我們需要做的就是將全局的$t實現(xiàn)即可。
下面通過源碼來介紹:(這個文件要在項目首行引入)
// 導(dǎo)入插件生成的國際化JSON文件
import langJSON from './index.json'
(function () {
// 定義翻譯函數(shù)
let $t = function (key, val, nameSpace) {
// 獲取指定命名空間下的語言包
const langPackage = $t[nameSpace];
// 返回翻譯結(jié)果,如果不存在則返回默認值
return (langPackage || {})[key] || val;
};
// 定義設(shè)置語言包的方法
$t.locale = function (locale, nameSpace) {
// 將指定命名空間下的語言包設(shè)置為傳入的locale
$t[nameSpace] = locale || {};
};
// 將翻譯函數(shù)掛載到window對象上,如果已經(jīng)存在則使用已有的
window.$t = window.$t || $t;
// 將簡單翻譯函數(shù)掛載到window對象上
window.$$t = $$t;
// 定義從JSON文件中獲取指定鍵的語言對象的方法
window._getJSONKey = function (key, insertJSONObj = undefined) {
// 獲取JSON對象
const JSONObj = insertJSONObj;
// 初始化語言對象
const langObj = {};
// 遍歷JSON對象的所有鍵
Object.keys(JSONObj).forEach((value) => {
// 將每個語言的對應(yīng)鍵值添加到語言對象中
langObj[value] = JSONObj[value][key];
});
// 返回語言對象
return langObj;
};
})();
// 定義語言映射對象
const langMap = {
// 根據(jù)插件的配置來生成語言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)
};
// 從本地存儲中獲取當(dāng)前語言,如果不存在則使用源語言
const lang = window.localStorage.getItem('lang') || 'zhcn';
// 根據(jù)當(dāng)前語言設(shè)置翻譯函數(shù)的語言包
window.$t.locale(langMap[lang], 'lang');
通過閱讀上面的代碼可以看到,為了回顯到頁面上,我們會導(dǎo)入生成的翻譯json,通過window.$t.locale(langMap[lang], 'lang')將對應(yīng)的語言包設(shè)置到翻譯函數(shù)上,這樣就可以在頁面上使用$t函數(shù)進行翻譯了。
它為什么不用影響現(xiàn)有代碼?
插件基于編譯時AST語法樹分析實現(xiàn)無感化改造:在代碼構(gòu)建階段,通過解析源代碼的抽象語法樹(AST),精準定位需翻譯的文本內(nèi)容,智能替換為指定翻譯函數(shù)(如 $t('哈希','原始文本'))。 同時,該過程會動態(tài)歸集所有需翻譯的文案數(shù)據(jù),生成映射表(index.json)。這一處理完全運行于構(gòu)建流程之中,既不修改源代碼文件的原始結(jié)構(gòu),也不會對運行期JavaScript邏輯產(chǎn)生任何干擾,確保開發(fā)與生產(chǎn)環(huán)境的穩(wěn)定性。
(例如對 `<div>文本</div>` 自動轉(zhuǎn)譯為 `$t('hash','文本')`,但原始源代碼文件保持不變,開發(fā)調(diào)試時仍可直接查看原生字符串內(nèi)容)
它為什么可以兼容全部前端框架?
auto-i18n-translation-plugins 的設(shè)計建立在框架無關(guān)的后期處理原則之上。由于所有前端框架(如 Vue、React、Svelte 等)最終都會將其自定義語法(模板、組件、JSX 等)編譯為標準 JavaScript 代碼,而該插件的文本提取與翻譯函數(shù)替換邏輯被明確置于構(gòu)建流程的最后階段執(zhí)行。這一策略的核心在于:
- 開發(fā)者框架的各類解析和編譯操作(如 Vue 的模板編譯、React 的 JSX 轉(zhuǎn)義)均在插件運行前完成;
- 插件直接處理最終的純 JavaScript 代碼,無需理解具體框架的內(nèi)部語法或結(jié)構(gòu);
- 只要確保插件在構(gòu)建管線的最后階段生效(如 Webpack 的
loader排序、Vite 的plugin配置順序),即可兼容所有符合標準編譯流程的前端框架。
效果: 開發(fā)者只需將插件配置為構(gòu)建流程的收尾環(huán)節(jié),即可無感支持 Vue + TS、React + SWC、純 JS 項目等任意技術(shù)棧,無需為不同框架單獨配置適配層。
倉庫地址和案例
Github:wenps/auto-i18n-translation-plugins
NPM vite 版: https://www.npmjs.com/package/vite-auto-i18n-plugin
NPM webpack 版:https://www.npmjs.com/package/webpack-auto-i18n-plugin?activeTab=readme
案例:https://github.com/wenps/auto-i18n-translation-plugins/tree/main/example
TODO
- ssr 全自動支持,目前需要手動適配
- 自動引入 lang/index.js

浙公網(wǎng)安備 33010602011771號