一個命令搞定 Web 國際化
背景
隨著出海的業務越來越多,web 應用面臨越來越多的國際化的工作。如何高效,高質量的完成 Web 前端國際化工作,已經是擺在 web 前端同學的急需解決的問題。
i18n-helper-cli 是什麼
i18n-helper-cli 是一個 Web 國際化整體解決方案,包含自動包裹詞條
,提取詞條
, 翻譯詞條
,詞條翻譯統計
,節省人力預估統計
,網頁多語言顯示異常檢測
(Coming soon)等功能。可以大大減低開發,測試,翻譯各個角色的人力成本,減少重複勞動,低階錯誤。
為什麼需要 i18n-helper-cli
Web 國際化流程
簡單來說可以分為以下 5 個步驟
- 【選型】多語言框架選型(這裡不深究,不在此篇範圍),我們選定
- 【開發 - 包裹詞條】從上面這步驟,我們知道需要把詞條包裹起來 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://babeljs.io/docs/en/babel-types
如何使用 i18n-helper-cli
例項
安裝
注意:請確保 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。