1. 程式人生 > 其它 >一個命令搞定 Web 國際化

一個命令搞定 Web 國際化

背景

隨著出海的業務越來越多,web 應用面臨越來越多的國際化的工作。如何高效,高質量的完成 Web 前端國際化工作,已經是擺在 web 前端同學的急需解決的問題。

i18n-helper-cli 是什麼

i18n-helper-cli 是一個 Web 國際化整體解決方案,包含自動包裹詞條提取詞條翻譯詞條詞條翻譯統計節省人力預估統計網頁多語言顯示異常檢測(Coming soon)等功能。可以大大減低開發,測試,翻譯各個角色的人力成本,減少重複勞動,低階錯誤。

為什麼需要 i18n-helper-cli

Web 國際化流程

簡單來說可以分為以下 5 個步驟

  1. 【選型】多語言框架選型(這裡不深究,不在此篇範圍),我們選定
    i18next
    ,18n-helper-cli 對多語言框架並不限制
  2. 【開發 - 包裹詞條】從上面這步驟,我們知道需要把詞條包裹起來 e.g 你好 => t('你好')
  3. 【開發 - 提取詞條】把上一步中包裹的詞條 copy 到翻譯檔案中
  4. 【翻譯 - 翻譯】翻譯把詞條翻譯好,填入翻譯檔案
  5. 【測試 - 測試頁面】開發提交測試後,對多語言頁面進行測試

問題

通過上面 5 步,可以完成站點國際化。大多數場景大家就是這麼做的,但這裡充斥著大量人工勞動,大量人工勞動意味著重複低效出錯機率提高。讓我們從以下三個階段分析下這些問題

  • 【開發階段】

    1. 人工操作包裹和提取詞條耗時長,但對個人無任何成長。如果是【全新開發】的站點,大家還可以耐著性子包裹詞條

      提取詞條,但如果是【存量修改】及對已有的站點做國際化,而且這裡的頁面幾十上百,甚至更多,這裡的包裹詞條提取詞條的工作量會讓人崩潰

    2. 遺漏包裹,提取詞條(程式碼多,詞條隱藏在各個檔案的各個角落裡。。。)

    3. 提取詞條後,執行多語言介面無法看到效果,需要等到翻譯返回

  • 【翻譯階段】

    1. 翻譯耗時長
    2. 遺漏翻譯
  • 【測試階段】

    1. 多語言頁面測試每個都要測,耗費大量時間
    2. 遺漏測試某個多語言頁面

所以這裡最大的問題是上面這些工作都需人工操作,問題清楚了,那接下來我們要做的就是把這些人工操作能夠交給機器,實現自動化,提高效率降低出錯機率

解決方案

先上結論,i18n-helper-cli

可以很好的解決上述問題。

原理

整體方案

  • 【詞條包裹】通過對程式碼進行編譯,得到AST,找到符合條件(中文,或者其他語言,可配置)的 Node,根據配置建立新 Node,替換老的 Node
  • 【詞條提取】同上,也是AST, 找到的符合條件的詞條以及原始碼已經包裹的詞條會被一起提取,根據配置寫入檔案
  • 【詞條翻譯】
    1. 從原始檔翻譯:如果有一份翻譯詞庫(這裡有常見的翻譯),我們提取出來的未翻譯詞條在這裡有,我們就可以直接從這裡翻譯
    2. 機器翻譯:未翻譯詞條呼叫雲服務實現翻譯(這裡我們用的是騰訊雲的翻譯服務)
  • 【網頁多語言顯示異常檢測】提供一份頁面 url 列表,用 Cypress 進行截圖,呼叫騰訊雲 OCR 服務提取圖片文字,進行對比,假設我們有個叫你好的詞條翻譯成 en 為Hello,如果我們通過 OCR 得到的是Hel,那麼我們可以認為這個頁面有問題(Coming soon)
  • 【統計】
    1. 翻譯詞條統計:根據當前語言下未翻譯詞條數 / 詞條總數
    2. 減低人工耗時預估:根據包裹,提取,翻譯詞條數預估

包裹詞條方案詳解

接下來我們詳細分析下詞條包裹的方案。我們要實現的是類似你好 => t('你好'),所以:

  1. 找到你好
  2. 替換成t('你好')

哈哈,剛說的就像網上的經典問題: 如何把大象放到冰箱?

回答:先打冰箱門,然後把大象放進去,在關上冰箱門

聽起來沒問題,好像很有道理的樣子,但沒有任何實際價值。言歸正傳,我們來探討下實際解決方案:

方案 1 - 正則

針對匹配到中文,這裡我們第一個想法應該就是正則表示式了。/[\u4e00-\u9fa5]可以匹配中文。至此,我們完成了第一步找到需要包裹的文字。接下來就是把包裹上了'你好'.replace(/([\u4e00-\u9fa5]+)/gi,'t(\'$1\')'),搞定。慢著,真的這麼簡單嗎,想想我們真實程式碼的情況,註釋,各種複雜的模板字串,換行等等。這意味著這個正則到後面會巨複雜,到後面會面對一個晦澀難懂,難於維護的正則表示式。

另外還有個很蛋疼的事情,比方說我們的除錯,上報等等程式碼,如console.log('不需要提取'),怎麼辦?感覺腦子不夠用了。

方案 2 - AST

我們希望只匹配我們想要的詞條。比如下如下程式碼,我們預期匹配你好(註釋和 console.log 的裡的都不需要),如果有個方式只給到我們你好,然後我再判斷它是不是包含中文,再包裹就再好不過了。

// 這是一段註釋
const word = '你好';
console.log('世界');

有沒有這樣的好事?答案是還真有。是時候上這張神圖了

Babel 的工作流程主要分為以下 3 個階段:

  1. Parse階段:詞法分析 & 語法分析
  2. Transform階段:生成AST,抽象語法樹
  3. 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,prettierwebpack等等都是通過對原始碼進行分析,轉換,生成實現各種各樣的功能。

我們可以開發自己的外掛,去做各種有意思的事情,比如說程式碼埋點,國際化方案等等。看到這裡我想大家一定會有個問題:

  1. 上面說的程式碼轉 AST 時的各種型別,我們怎麼知道轉成什麼型別了呢?

答:https://astexplorer.net/

  1. 另外這些型別如何構造新的節點?

答: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

快捷使用

  1. 在專案根目錄下生成 i18n.config.json 檔案
# 互動式命令列
i18n-helper init
# 生成預設配置檔案,具體參見【配置說明】( 推薦大家用這個哈,互動方式的的後面加了不少配置海內來得及補齊)
i18n-helper init -y
  1. 包裹 & 提取 & 翻譯 & 統計
# 包裹 & 提取 & 翻譯 & 統計 i18n.config.json 中 srcPath 檔案中的中文詞條
i18n-helper scan -wetc
  1. 切換 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。