如何使用 react-intl + babel plugin 自動化產出多國語系檔?

ErrorBaker

找麵包

搜尋烘焙師、標題、內文或口味

今天想吃什麼?

試試技術名稱、口味,或烘焙師的名字。

# 誰可能適合閱讀這篇文章?

如果你沒有上述需求,你可以考慮去閱讀其他夥伴們的 優秀作品

# react-intl 簡介

官方文件可以得知, react-intl 是一套可以協助我們做多國語系的 Library

# react-intl 基本操作

在 React 專案中執行指令 yarn react-intl or npm install react-intl 可以按照下面的程式碼做最基本的配置

  1. 在元件的最外層包一個 react-intl 內建的 Context:IntlProvider,這樣被包裹的元件才能透過 useIntl 這個 custom hook 取得 formatMessage

  2. formatMessage 內放的是一個 object 包含 id 和 defaultMessage,當 IntlProvider messages 中的 key 和 formateMessage 中 object 的 id 對應時,就會使用對應的詞作為翻譯。

import { IntlProvider, useIntl } from "react-intl"

function App() {
  return (
    <IntlProvider messages={{ test: "測試" }}>
      {/* props: messages 的 test 對應到 formatMessage({ id: "test" }) */}
      <TranslationBlock />
    </IntlProvider>
  )
}
function TranslationBlock() {
  const { formatMessage } = useIntl()
  return (
    <div>
      <span>
        {formatMessage({ id: "test", defaultMessage: "預設訊息" })}
        {/* 顯示為 測試 */}
      </span>
    </div>
  )
}
import { IntlProvider, useIntl } from "react-intl";

function App() {
  return (
    <IntlProvider messages={}>
      {/* messages 沒有翻譯 */}
      <TranslationBlock />
    </IntlProvider>
  )
}
function TranslationBlock() {
  const { formatMessage } = useIntl()
  return (
    <div>
      <span>
        {formatMessage({ id: "test", defaultMessage: "預設訊息" })}
        {/* messages 沒有翻譯 顯示 defaultMessage 為 預設訊息 */}
      </span>
    </div>
  )
}

隨著翻譯字詞數量的增加我們會額外獨立出一個翻譯檔,再引入到 IntlProvider 中,方便集中管理

locale/zh.json

{
  "test1": "測試1",
  "test2": "測試2",
  "test3": "測試3",
  "test4": "測試4"
}

App.jsx

import { IntlProvider, useIntl } from "react-intl";
import zh from "../locale/zh";
import jp from "../locale/jp";
import en from "../locale/en";

function App() {
  return (
    <IntlProvider messages={zh}>
      <TranslationBlock />
    </IntlProvider>
  )
}

function TranslationBlock() {
  const { formatMessage } = useIntl()
  return (
    <div>
      <span>{formatMessage({ id: "test1", defaultMessage: "預設訊息1" })}</span>{/* 測試1 */}
      <span>{formatMessage({ id: "test2", defaultMessage: "預設訊息2" })}</span>{/* 測試2 */}
      <span>{formatMessage({ id: "test3", defaultMessage: "預設訊息3" })}</span>{/* 測試3 */}
      <span>{formatMessage({ id: "test4", defaultMessage: "預設訊息4" })}</span>{/* 測試4 */}
    </div>
  )
}

但很快的,隨著翻譯的語系(zh, en, jp...)和字詞的增加,就會開始遇到問題了,每一次新增翻譯和語系的時候,都必須 手動 的將語系的 translation key 複製出來貼到語系檔裡面才能進行翻譯,這樣不僅麻煩也很容易漏,
所以我希望能採用某種自動化的機制,讓我只需要在 formatMessage 中定義好 id 和 defaultMessage,就來自動的產出語系檔,每個不同語系檔都包含對應 id 的 translation key,這樣我就只需要將注意力放在定 id 和翻譯。

在查了很多資料之後發現,使用 react-intl 中的 defineMessage + babel plugin 可以幫我們做到這件事情。

再加上 defineMessage 之後,翻譯不會並不會發生什麼變化,但使用 defineMessage 可以讓我們講使用螢光筆一樣,將待會需要 extract 的翻譯字詞標記起來。

import { defineMessage, useIntl } from "react-intl";

function TranslationBlock() {
  const { formatMessage } = useIntl();
  return (
    <div>
      <span>
        {formatMessage({ id: "test1", defaultMessage: "預設訊息 1" })}
      </span>{" "}
      {/* 預設訊息 1 */}
      <span>
        {/* defineMessage 回傳的值等同於 { id: "test2", defaultMessage: "預設訊息 2" } */}
        /{formatMessage(
          defineMessage({ id: "test2", defaultMessage: "預設訊息 2" })
        )} {/* 預設訊息 2 */}
      </span>
    </div>
  );
}

export default App;

隨著翻譯字詞的增多,不想寫那麼多次 defineMessage ,我們還可以用有加 s 的 defineMessages 進行標注

const messages = defineMessages({
  test3: { id: "test3", defaultMessage: "預設訊息 3" },
  test4: { id: "test4", defaultMessage: "預設訊息 4" },
})

// defineMessages 回傳的 messages 等同於
// const messages = {
//   test3: { id: "test3", defaultMessage: "預設訊息 3" },
//   test4: { id: "test4", defaultMessage: "預設訊息 4" },
// }

function TranslationBlock() {
  const { formatMessage } = useIntl()
  return (
    <div>
      <span>
        {formatMessage({ id: "test1", defaultMessage: "預設訊息 1" })} {/* 預設訊息 1 */}
      </span>
      <span>
        {formatMessage(
          defineMessage({ id: "test2", defaultMessage: "預設訊息 2" }) {/* 預設訊息 2 */}
        )}
      </span>
      <span>{formatMessage(messages.test4)}</span> {/* 預設訊息 3 */}
      <span>{formatMessage(messages.test4)}</span> {/* 預設訊息 4 */}
    </div>
  )
}

# Babel 簡介

Babel 是一個 JavaScript 的轉譯器,通常拿來將瀏覽器的尚未支援的 ES6+ 語法轉成 ES5,這樣就可以讓開發者用最新的語法來寫 Code。

Babel 背後的原理是抽象語法樹 Abstract Syntax Tree,簡稱 AST,對 AST 有興趣的話,推薦看這篇

# Babel 設置

可以參考官方文件安裝,還需要加上 Plugin babel-plugin-react-intl

babel.config.json

{
  "presets": ["@babel/preset-env", "@babel/preset-react"],
  "plugins": [
    [
      // 這個 Plugin 會將定義好的 id 和 defaultMessage extract 出來
      "react-intl",
      {
        "messagesDir": "./src/translations/extract" // extract 出來的檔案要放到那個資料夾下面
      }
    ]
  ]
}

在專案中執行 babel ./src 就會再目標資料夾看到 extract 出來的 json 檔,資料格式會像是這樣

[
  { "id": "test1", "defaultMessage": "預設訊息 1" }
  { "id": "test2", "defaultMessage": "預設訊息 2" }
  { "id": "test3", "defaultMessage": "預設訊息 3" },
  { "id": "test4", "defaultMessage": "預設訊息 4" },
]

但這不是我們要的格式,我希望的語系檔格式會像是這樣

{
  "test1": "預設訊息 1",
  "test2": "預設訊息 2",
  "test3": "預設訊息 3", 
  "test4": "預設訊息 4",
}

聰明如你,一定想到了可以自己寫一個腳本做轉換,但正所謂不要重複造輪子,社群上的大大們已經把工具都做好,在這裡我們可以直接使用就好了,

我們要使用的是這個 react-intl-translations-manager

使用這個工具甚至可以直接幫我們把 Extract 的文字轉換成各種不同的語系檔

manageTranslation.js

const manageTranslations = require("react-intl-translations-manager").default

manageTranslations({
  messagesDirectory: "src/translations/extract", // 剛剛 extract 出來的檔案
  translationsDirectory: "src/translations/locales/", // 輸出語系檔的資料夾
  languages: ["en", "zh", "jp", 'alien'], // any language you need
  whitelistsDirectory: "src/translations/locales/whitelists",
})

在所有的設置都安排好之後我們要做的事就是:

  1. 利用 react-intl 的 defineMessage 將需要 extract 的字詞定義好
  2. 使用 babel-plugin-react-intl 將 id 和 defaultMessage extract 出來
  3. 使用 react-intl-translations-manager 將 extract 出來的翻譯轉換成語系檔
  4. 最後將 extract 出來,已經轉換成語系檔的檔案案刪除

寫在 package.json 的 script 會像這樣

{
  "scripts": {
    "trans": "yarn trans:extract && yarn trans:manage && yarn trans:clean",
    "trans:extract": "babel ./src",
    "trans:manage": "node manageTranslation.js",
    "trans:clean": "rm -rf ./src/translations/extract"
  },
}

執行 yarn trans 之後你就會看到你需要的語法出 locale 資料夾下面,你就可以在 IntlProvider 的 messages 切換不同的語系檔來轉換語系

可參考 repo

# 參考資料


認識這位烘焙師

紀錄那些我笨拙可愛的時刻