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