作者:qiangwu,騰訊CSIG Web開發(fā)工程師
i18n-helper-cli 是什么
i18n-helper-cli 是一個(gè) Web 國際化整體解決方案,包含自動(dòng)包裹詞條,提取詞條, 翻譯詞條,詞條翻譯統(tǒng)計(jì),節(jié)省人力預(yù)估統(tǒng)計(jì),網(wǎng)頁多語言顯示異常檢測(cè)(Coming soon)等功能。可以大大減低開發(fā),測(cè)試,翻譯各個(gè)角色的人力成本,減少重復(fù)勞動(dòng),低級(jí)錯(cuò)誤。
為什么需要 i18n-helper-cli
Web 國際化流程
簡(jiǎn)單來說可以分為以下 5 個(gè)步驟
- 【選型】多語言框架選型(這里不深究,不在此篇范圍),我們選定 i18next,18n-helper-cli 對(duì)多語言框架并不限制
- 【開發(fā) - 包裹詞條】從上面這步驟,我們知道需要把詞條包裹起來 e.g 你好 => t('你好')
- 【開發(fā) - 提取詞條】把上一步中包裹的詞條 copy 到翻譯文件中
- 【翻譯 - 翻譯】翻譯把詞條翻譯好,填入翻譯文件
- 【測(cè)試 - 測(cè)試頁面】開發(fā)提交測(cè)試后,對(duì)多語言頁面進(jìn)行測(cè)試
問題
通過上面 5 步,可以完成站點(diǎn)國際化。大多數(shù)場(chǎng)景大家就是這么做的,但這里充斥著大量人工勞動(dòng),大量人工勞動(dòng)意味著重復(fù)低效,出錯(cuò)幾率提高。讓我們從以下三個(gè)階段分析下這些問題
- 【開發(fā)階段】 人工操作包裹和提取詞條耗時(shí)長(zhǎng),但對(duì)個(gè)人無任何成長(zhǎng)。如果是【全新開發(fā)】的站點(diǎn),大家還可以耐著性子包裹詞條,提取詞條,但如果是【存量修改】及對(duì)已有的站點(diǎn)做國際化,而且這里的頁面幾十上百,甚至更多,這里的包裹詞條,提取詞條的工作量會(huì)讓人崩潰 遺漏包裹,提取詞條(代碼多,詞條隱藏在各個(gè)文件的各個(gè)角落里。。。) 提取詞條后,運(yùn)行多語言界面無法看到效果,需要等到翻譯返回
- 【翻譯階段】 翻譯耗時(shí)長(zhǎng)遺漏翻譯
- 【測(cè)試階段】 多語言頁面測(cè)試每個(gè)都要測(cè),耗費(fèi)大量時(shí)間遺漏測(cè)試某個(gè)多語言頁面
所以這里最大的問題是上面這些工作都需人工操作,問題清楚了,那接下來我們要做的就是把這些人工操作能夠交給機(jī)器,實(shí)現(xiàn)自動(dòng)化,提高效率,降低出錯(cuò)幾率。
解決方案
先上結(jié)論,i18n-helper-cli 可以很好的解決上述問題。
原理
整體方案
- 【詞條包裹】通過對(duì)代碼進(jìn)行編譯,得到AST,找到符合條件(中文,或者其他語言,可配置)的 Node,根據(jù)配置創(chuàng)建新 Node,替換老的 Node
- 【詞條提取】同上,也是AST, 找到的符合條件的詞條以及原代碼已經(jīng)包裹的詞條會(huì)被一起提取,根據(jù)配置寫入文件
- 【詞條翻譯】 從源文件翻譯:如果有一份翻譯詞庫(這里有常見的翻譯),我們提取出來的未翻譯詞條在這里有,我們就可以直接從這里翻譯機(jī)器翻譯:未翻譯詞條調(diào)用云服務(wù)實(shí)現(xiàn)翻譯(這里我們用的是騰訊云的翻譯服務(wù))
- 【網(wǎng)頁多語言顯示異常檢測(cè)】提供一份頁面 url 列表,用 Cypress 進(jìn)行截圖,調(diào)用騰訊云 OCR 服務(wù)提取圖片文字,進(jìn)行對(duì)比,假設(shè)我們有個(gè)叫你好的詞條翻譯成 en 為Hello,如果我們通過 OCR 得到的是Hel,那么我們可以認(rèn)為這個(gè)頁面有問題(Coming soon)
- 【統(tǒng)計(jì)】 翻譯詞條統(tǒng)計(jì):根據(jù)當(dāng)前語言下未翻譯詞條數(shù) / 詞條總數(shù)減低人工耗時(shí)預(yù)估:根據(jù)包裹,提取,翻譯詞條數(shù)預(yù)估
包裹詞條方案詳解
接下來我們?cè)敿?xì)分析下詞條包裹的方案。我們要實(shí)現(xiàn)的是類似你好 => t('你好'),所以:
- 找到你好
- 替換成t('你好')
哈哈,剛說的就像網(wǎng)上的經(jīng)典問題: 如何把大象放到冰箱?
回答:先打冰箱門,然后把大象放進(jìn)去,在關(guān)上冰箱門
聽起來沒問題,好像很有道理的樣子,但沒有任何實(shí)際價(jià)值。言歸正傳,我們來探討下實(shí)際解決方案:
方案 1 - 正則
針對(duì)匹配到中文,這里我們第一個(gè)想法應(yīng)該就是正則表達(dá)式了。/[u4e00-u9fa5]可以匹配中文。至此,我們完成了第一步找到需要包裹的文字。接下來就是把包裹上了'你好'.replace(/([u4e00-u9fa5]+)/gi,'t('$1')'),搞定。慢著,真的這么簡(jiǎn)單嗎,想想我們真實(shí)代碼的情況,注釋,各種復(fù)雜的模板字符串,換行等等。這意味著這個(gè)正則到后面會(huì)巨復(fù)雜,到后面會(huì)面對(duì)一個(gè)晦澀難懂,難于維護(hù)的正則表達(dá)式。
另外還有個(gè)很蛋疼的事情,比方說我們的調(diào)試,上報(bào)等等代碼,如console.log('不需要提取'),怎么辦?感覺腦子不夠用了。
方案 2 - AST
我們希望只匹配我們想要的詞條。比如下如下代碼,我們預(yù)期匹配你好(注釋和 console.log 的里的都不需要),如果有個(gè)方式只給到我們你好,然后我再判斷它是不是包含中文,再包裹就再好不過了。
// 這是一段注釋
const word = '你好';
console.log('世界');
有沒有這樣的好事?答案是還真有。是時(shí)候上這張神圖了
Babel 的工作流程主要分為以下 3 個(gè)階段:
- Parse階段:詞法分析 & 語法分析
- Transform階段:生成AST,抽象語法樹
- Generate階段:生成代碼
這里我們著重說下Transform階段,AST 處理的核心要素:
- babel-core 通過 transform 將代碼字符串轉(zhuǎn)換為 AST 樹;
- babel-types 一個(gè)強(qiáng)大的用于處理 AST 節(jié)點(diǎn)的工具庫,它包含了構(gòu)造、驗(yàn)證以及變換 AST 節(jié)點(diǎn)的方法;
- visitor 當(dāng) Babel 處理 Node 時(shí),以訪問者的形式獲取節(jié)點(diǎn)信息,并進(jìn)行相關(guān)操作。這種方式是通過一個(gè) visitor 對(duì)象來完成,在 visitor 對(duì)象中定義了對(duì)于各種節(jié)點(diǎn)類型函數(shù),我們可以通過不同類型節(jié)點(diǎn)做出相應(yīng)處理。
通過上述要素,我們既可以完成對(duì) AST 的修改。下面我們看下這里的核心代碼:
return {
visitor: {
StringLiteral(path: NodePath<tt.StringLiteral>) {
let { value } = path.node;
value = replaceLineBreak(value);
if (needWrap(wrapCharacter, value)) {
let newNode = t.CallExpression(t.Identifier(T_WRAppER), [
combine(value),
]);
path.replaceWith(newNode);
}
},
CallExpression(path: NodePath<tt.CallExpression>) {
switch (path.node.callee.type) {
case 'MemberExpression': {
const excludeFuncName = i18nConf.parsedExcludeWrapperFuncName;
if (excludeFuncName.length > 0) {
const names: string[] = [];
const me = path.node.callee as tt.MemberExpression;
getName(me, names);
const MEName = names.reverse().join('.');
if (excludeFuncName.includes(MEName)) {
path.skip();
}
}
break;
}
default:
break;
}
},
}
針對(duì)上面我們?cè)V求的例子,當(dāng)我們得到 AST 后
- // 這是一段注釋 - 實(shí)際上會(huì)被解析成 CommentLine 類型,我們的代碼不處理,所以該什么樣還是什么樣
- const word = '你好' - 你好被解析為StringLiteral,判斷是中文,這時(shí)候我們?cè)僦匦聵?gòu)造一個(gè)新的節(jié)點(diǎn),替換老的及完成了包裹
- console.log('世界') - console.log被解析為CallExpression,我們可以通過在配置文件中配置需要忽略的包裹的方法,如果解析到的方法名在配置中,則忽略掉,這樣就不會(huì)出來這里的世界
至此,我們即可完成我們的訴求,完美的對(duì)符合我們需要的詞條就行包裹。
題外話 - 如何編寫自己的 babel 插件
通過上面 AST 的方案,我們可以看得出這里的功能很強(qiáng)大,業(yè)界eslint,prettier,webpack等等都是通過對(duì)源碼進(jìn)行分析,轉(zhuǎn)換,生成實(shí)現(xiàn)各種各樣的功能。
我們可以開發(fā)自己的插件,去做各種有意思的事情,比如說代碼埋點(diǎn),國際化方案等等。看到這里我想大家一定會(huì)有個(gè)問題:
- 上面說的代碼轉(zhuǎn) AST 時(shí)的各種類型,我們?cè)趺粗擂D(zhuǎn)成什么類型了呢?
答:https://astexplorer.net/
2.另外這些類型如何構(gòu)造新的節(jié)點(diǎn)?
答:https://babeljs.io/docs/en/babel-types
如何使用 i18n-helper-cli
實(shí)例
請(qǐng)參考 example
安裝
注意:請(qǐng)確保 Nodejs 版本大于 14!!!
# npm 安裝
npm install i18n-helper-cli -D
# yarn 安裝
yarn add i18n-helper-cli —dev
快捷使用
- 在項(xiàng)目根目錄下生成 i18n.config.json 文件
# 交互式命令行
i18n-helper init
# 生成默認(rèn)配置文件,具體參見【配置說明】( 推薦大家用這個(gè)哈,交互方式的的后面加了不少配置還沒來得及補(bǔ)齊)
i18n-helper init -y
- 包裹 & 提取 & 翻譯 & 統(tǒng)計(jì)
# 包裹 & 提取 & 翻譯 & 統(tǒng)計(jì) i18n.config.json 中 srcPath 文件中的中文詞條
i18n-helper scan -wetc
- 切換 Cli 語言
# cli 默認(rèn)為中文,支持語言切換,目前支持zh & en
i18n-helper switch en
命令詳情
# 包裹 & 提取 & 翻譯 & 統(tǒng)計(jì) i18n.config.json 中 srcPath 文件中的中文詞條
# w:wrap e:extract t:translate tm: translate machine c:count
# l:language
# 這 5 個(gè)操作可以隨意組合 e.g. i18n-helper scan -we 則只會(huì)翻譯 & 提取
i18n-helper scan -wetc
i18n-helper scan -we -tm -c
# 包裹 & 提取 & 翻譯 & 統(tǒng)計(jì) 指定路徑,指定語言內(nèi)符合規(guī)則的詞條
# e.g i18n-helper scan -wetc -l en ./src/test/index.js
i18n-helper scan -wetc -l [language] [filepath]
i18n-helper scan -we -tm -c -l [language] [filepath]
# 包裹 i18n.config.json 中 srcPath 文件中的中文詞條
i18n-helper wrap
i18n-helper scan -w
# 包裹指定文件中的中文詞條
i18n-helper wrap [filepath]
i18n-helper scan -w [filepath]
# 提取 i18n.config.json 中 srcPath 文件中的中文詞條到所有配置語言文件
i18n-helper extract
i18n-helper scan -e
# 提取指定文件中文詞條到指定語言文件
# e.g i18n-helper extract -l en ./src/test/index.js
i18n-helper extract -l [language] [filepath]
i18n-helper scan -e -l [language] [filepath]
# 翻譯 i18n.config.json 中配置翻譯文件詞條, -m 騰訊翻譯君機(jī)器翻譯
# 從翻譯源文件文件中翻譯
i18n-helper translate
i18n-helper scan -t
# 騰訊翻譯君自動(dòng)翻譯
i18n-helper translate -m
i18n-helper scan -tm
# 翻譯指定語言
# 從翻譯源文件文件中翻譯
i18n-helper translate [language]
i18n-helper scan -t -l [language]
# 騰訊翻譯君自動(dòng)翻譯指定語言文件
i18n-helper translate -m [language]
i18n-helper scan -tm -l [language]
# 統(tǒng)計(jì) i18n.config.json 中翻譯文件的翻譯情況
i18n-helper count
i18n-helper scan -c
# 統(tǒng)計(jì)指定語言翻譯文件的翻譯情況,多個(gè)語言用,分隔
i18n-helper count [language]
i18n-helper scan -c -l [language]
配置詳情
module.exports = {
// cli 語言
cliLang: 'zh',
// 項(xiàng)目類型:react | vue | js
projectType: '[react]',
// 默認(rèn)包裹和提取詞條的目錄
srcPath: './',
// 掃描文件格式
fileExt: 'js,ts,tsx',
// 包裹的字符集,下面是中文
wrapCharacter: '[u4e00-u9fa5]',
// 包裹詞條的名字
wrapperFuncName: 't',
// 忽略掉包裹的方法,多個(gè)用,分隔
excludeWrapperFuncName: 'console.log,console.error',
// jsx中的文字包裹方式,true用<trans></trans>, false用【wrapperFuncName】的value包裹
jsx2Trans: false,
// 當(dāng)文件需要翻譯時(shí)引入的文件
importStr: `import {t} from './i18n;';n`,
// 排除目錄,此目錄下的不會(huì)不會(huì)執(zhí)行包裹和提取詞條操作
exclude: 'node_modules,dist,git',
// 翻譯詞條目錄
localeDir: './locales',
// 翻譯語種
languages: 'zh,en',
// 源語言
sourceLanguage: 'zh',
// 翻譯詞條文件名
transFileName: 'translation',
// 翻譯詞條文件格式: json, po
transFileExt: 'json',
// 翻譯詞庫目錄(自動(dòng)翻譯目錄)
targetTransDir: './translations',
// 翻譯詞庫文件名
targetTransFile: 'sourceTranslation.json',
// 騰訊云 secretId
secretId: '',
// 騰訊云 secretKey
secretKey: '',
};
未來規(guī)劃
- [ ] 網(wǎng)頁多語言顯示異常檢測(cè)
- [ ] 豐富提取文件(po, csv, Excel 等等)
- [ ] 增加 git 模式,針對(duì)當(dāng)前改動(dòng)的文件才轉(zhuǎn) AST 包裹,提取
- [ ] 詞條提取 cleanMode,目前如果代碼中沒有這個(gè)詞條了,提取后的文件依然會(huì)有
其他
源碼
https://github.com/wuqiang1985/i18n-helper
NPM 包
https://www.npmjs.com/package/i18n-helper-cli
目前還在完善中,歡迎大家試用,大家有問題可以提 issue。