對國際化 i18n 專案的一點思考

熊的貓發表於2023-02-20

國際化是什麼?

國際化 對應的英文單詞為 Internationalization,又稱 i18n

  • i 為單詞的 【第一個】 字母
  • 18 為 【in 之間】 單詞的個數
  • n 代表這個單詞的 【最後一個】 字母

如果你的專案是 Vue,那麼相信你在實現國際化功能時,也必不可少的會使用到 vue-i18n 這個庫,接下來本文也是透過這個庫搭配 Vue 實現最基本的國際化功能,但關注點並不是如何使用這個庫,而是在實現的過程中思考 可最佳化的點

實現基本國際化功能

這裡就不再多餘演示 demo 專案的建立過程了,並且文中只演示最基本的 中英文 切換,好了現在直奔核心吧!

企業微信截圖_16764259952321.png

整合 vue-i18n

安裝依賴

熟悉的命令:npm install vue-i18n -S

配置 vue-i18n

image.png

  • src 目錄下建立 language 目錄用於保持和語言切換相關的內容
  • language 目錄下建立 lang 目錄用於儲存不同語言的對映關係,如中文對應 zh.js、英文對應 en.js
  • language 目錄下建立 index.js 作為預設匯出,並在其中建立 i18n 物件

     import { createI18n } from "vue-i18n";
      import zh from './lang/zh';
      import en from './lang/en';
      const i18n = createI18n({
        legacy: false,
        locale: "zh", // 初始化配置語言
        messages: {
          zh,
          en,
        },
      });
    
      export default i18n;

    main.js 註冊 i18n

    內容非常簡單,直接上程式碼:

    import { createApp } from "vue";
    import i18n from "./language";
    import store from "./store";
    import App from "./App.vue";
    
    createApp(App)
      .use(store)
      .use(i18n)
      .mount("#app");

    實際上在透過 use(i18n) 時,會呼叫 i18n.install() 方法,大概內容如下:

  • 透過 app.provide(app.__VUE_I18N_SYMBOL__, i18n)i18n 物件提供給應用中的所有後代元件可透過 inject 注入
  • 透過 app.config.globalProperties.xxx = xxx 的方式為應用新增 全域性屬性/方法,實際上是對 Vue2 中 Vue.prototype 使用方式的一種替代

    • 常見全域性屬性,如 $i18n 透過 app.config.globalProperties.$i18n = i18n 新增到全域性
    • 常見全域性方法,如:$t, $rt, $d, $n, $tm 透過 Object.defineProperty(app.config.globalProperties, `$${method}`, desc) 新增到全域性
  • 透過 aplly(...) 方法註冊常用的 全域性指令 v-t全域性元件 i18n
  • 在根元件解除安裝時移除/釋放 i18n 相關內容

    const unmountApp = app.unmount;
    app.unmount = () => {
        i18n.dispose();
        unmountApp();
    };
  • 註冊 vue-devtools 的相關外掛

    根據資料資訊填充國際化內容

    頁面渲染

    假設需要渲染如下資料對應的列表,並且要實現國際化:

    const data = [
    {
      url: vueImg,
      title: 'Vue',
      describe: '漸進式 JavaScript 框架'
    },
    {
      url: reactImg,
      title: 'React',
      describe: '用於構建使用者介面的 JavaScript 庫'
    },
    {
      url: angularImg,
      title: 'Angular',
      describe: '現代 Web 開發平臺'
    },
    {
      url: nodeImg,
      title: 'Node',
      describe: 'Node.js 是一個基於 Chrome V8 引擎的 JavaScript 執行時'
    },
    {
      url: webpackImg,
      title: 'Webpack',
      describe: 'webpack 是一個用於現代 JavaScript 應用程式的靜態模組打包工具'
    },
    ];

    對應 App.vue 元件模板內容如下:

    <template>
    <button class="btn" @click="changeLang">{{ $t("中/英") }}</button>
    <List :data="data" />
    </template>

對應的頁面效果如下:

image.png

填充 lang 目錄下對映關係

lang/zh.js 檔案中:

export default {
  "漸進式 JavaScript 框架": "漸進式 JavaScript 框架",

  "用於構建使用者介面的 JavaScript 庫": "用於構建使用者介面的 JavaScript 庫",

  "現代 Web 開發平臺": "現代 Web 開發平臺",

  "Node.js 是一個基於 Chrome V8 引擎的 JavaScript 執行時":
    "Node.js 是一個基於 Chrome V8 引擎的 JavaScript 執行時",

  "webpack 是一個用於現代 JavaScript 應用程式的靜態模組打包工具":
    "webpack 是一個用於現代 JavaScript 應用程式的靜態模組打包工具",

  "中/英": "中/英",
};

lang/zh.js 檔案中:

export default {
  "漸進式 JavaScript 框架": "Progressive JavaScript framework",

  "用於構建使用者介面的 JavaScript 庫":
    "JavaScript library for building user interface",

  "現代 Web 開發平臺": "Modern web development platform",

  "Node.js 是一個基於 Chrome V8 引擎的 JavaScript 執行時":
    "Node.js is a JavaScript runtime based on the chrome V8 engine",

  "webpack 是一個用於現代 JavaScript 應用程式的靜態模組打包工具":
    "Webpack is a static module packaging tool for modern JavaScript applications",

  "中/英": "Chinese / English",
};

<List /> 元件中進行翻譯處理

翻譯處理可透過如下方式處理:

  • 使用 $t(...) 方法
  • 使用 v-t 指令
  • 使用 <i18n-t></i18n-t> 元件

這裡選擇第一種,因為它更靈活,能使用的範圍也更廣,指令和元件形式限定在了 template 中:

image.png

效果演示

test.gif

最佳化 i18n 配置

基於以上簡單的例子,已經能夠實現了國際化切換功能,但其中需要考慮的最佳化點還不少,下面的內容僅屬於 拋轉引玉,不一定全面。

image.png

最佳化翻譯檔案中的 key

現在很明顯的一點,就是 zh.js、en.js 檔案中用於對映的 key 太長了,導致整個檔案看起來會很多、很亂,因此我們可以將對應的 key 進行精簡,如下:

// zh.js
export default {
  "Vue 簡介": "漸進式 JavaScript 框架",
  
  "React 簡介": "用於構建使用者介面的 JavaScript 庫",

  "Angular 簡介": "現代 Web 開發平臺",

  "Node 簡介": "Node.js 是一個基於 Chrome V8 引擎的 JavaScript 執行時",

  "Webpack 簡介":
    "webpack 是一個用於現代 JavaScript 應用程式的靜態模組打包工具",

  "中/英": "中/英",
};

// en.js
export default {
  "Vue 簡介": "Progressive JavaScript framework",

  "React 簡介": "JavaScript library for building user interface",

  "Angular 簡介": "Modern web development platform",

  "Node 簡介": "Node.js is a JavaScript runtime based on the chrome V8 engine",

  "Webpack 簡介":
    "Webpack is a static module packaging tool for modern JavaScript applications",

  "中/英": "Chinese / English",
};

那麼對應到外部傳入需要渲染的資料來源 data 就可以簡寫為:

const data = [
  {
    url: vueImg,
    title: "Vue",
    describe: "Vue 簡介",
  },
  {
    url: reactImg,
    title: "React",
    describe: "React 簡介",
  },
  {
    url: angularImg,
    title: "Angular",
    describe: "Angular 簡介",
  },
  {
    url: nodeImg,
    title: "Node",
    describe: "Node 簡介",
  },
  {
    url: webpackImg,
    title: "Webpack",
    describe: "Webpack 簡介",
  },
];
另一種精簡方式就是將 key 用對應的類似於 變數命名 的方式來定義,但我個人不是很喜歡這種方式,首先在語義上很難直接讀出相關資訊,而且很難用一兩個英文單詞去概括文字內容,而且在後期需要排查對應問題並需要定位 tmeplate 時是極其不方便.

轉換翻譯檔案型別

.js 轉 .json

上述的翻譯檔案是 .js 檔案,因此,為了能夠讓其能被其他檔案匯入,我們不得不在檔案中使用 export defualtexport 將對應檔案內容向外匯出,但其實我們可以將 .js 檔案轉換為 .json 檔案直接使用,如下:

image.png

excel 轉 .json

在實際專案中翻譯的內容通常是由業務專門找對應的翻譯人員提供的,而真正到了開發者手中往往就是一個 excel 型別的表格檔案,如果我們使用的是前面的純 json 方式,免不了要自己一個一個從表格中複製對應的內容到我們對應的 .json 檔案中,而且是分別填充到 zh.jsonen.json 中,值得注意的是現在才是僅支援兩個國家的語言,如果後續支援的國家變多,那麼手動複製的方式豈不是要 image.png

因此,最好的做法是我們根據業務方提供的表格自動轉成 json 格式的資料,避免不必要的手動操作,用命令幫我們處理這個內容:

  • 安裝依賴 npm install xlsx-to-json
  • 將對應的轉換操作封裝 excel2json.js 檔案中,基於第三方庫簡單封裝即可

    const xlsx2json = require("xlsx-to-json");
    const path = require("path");
    
    xlsx2json(
      {
        input: path.join(__dirname, "./i18n.xlsx"),
        output: path.join(__dirname, "./i18n.json"),
      },
      function (err, result) {
        if (err) {
          console.error(err);
        } else {
          console.log(result);
        }
      }
    );
    • 修改 i18n 配置的入口檔案 src\language\index.js

        import { createI18n } from "vue-i18n";
        import i18njson from "./i18n.json";
      
        // 動態獲取 message 資訊
        function getMessage() {
       const messages = {
         zh: {},
         en: {}
       };
       i18njson.forEach(({Short, Chinese, English}) => {
         messages.zh[Short] = Chinese;
         messages.en[Short] = English;
       });
       return messages;
        }
      
        const i18n = createI18n({
       legacy: false,
       locale: "zh", // 初始化配置語言
       messages: getMessage(),
        });
      
        export default i18n;
    • 提供 i18n.xlsx 檔案作為資料來源

      image.png

    • package.json 問檔案中新增對應轉換命令

        "scripts": {
            "dev": "vite",
            "build": "vite build",
            "preview": "vite preview",
            "i18n": "node ./src/language/excel2json.js"
          }

      具體效果如下:

      test.gif

升級為 i18n 系統

上面已經將對應的翻譯包從 *.xlsx 轉成了 *.json 的形式,這樣一定程度上能減少重複勞動力,但在 複用/協作 方面還是不夠理想,因此可以從更高的維度將這整個內容升級到 i18n 系統,並提供對應的翻譯包上傳、自動解析、去重、新增名稱空間等功能,再加上對應的列表管理功能,重點是可供多人員、多系統進行 複用/協作,對前端來講就可以透過 介面 獲取對應的翻譯包資料,也能減少前端最終構建產物的體積。

處理翻譯檔案中重複的內容

93ADD769.gif

什麼叫重複的內容呢?其實很簡單,比如有個文字內容為 確認 按鈕,它在 A 頁面需要翻譯為 Confirm,它在 B 頁面需要翻譯為 OK,而它的中文內容就是 確認,意味著它對應的資料內容為:

[
  { "Short": "確認", "Chinese": "確認", "English": "Confirm" },
  { "Short": "確認", "Chinese": "確認", "English": "OK" }
]

但這樣其實是不行的,這樣最終會被後面的內容覆蓋掉,即 A、B 頁面最終的翻譯內容都為 OK,因為資料中的 Short 其實就是最終的不同語言對映的 key,如下:

const messages = {
    zh: {
     "確認": "確認",
     "確認": "確認"
    },
    en: {
     "確認": "Confirm",
     "確認": "OK"
    }
};

既然這樣,那麼其實我們只要為不同的翻譯內容設定不同的 Short 值即可,如下:

[
  { "Short": "確認1", "Chinese": "確認", "English": "Confirm" },
  { "Short": "確認2", "Chinese": "確認", "English": "OK" }
]

這樣變動小,並且也沒有丟失掉原本的語義。

考慮不同語言的樣式

由於不同語言的表現形式不同,內容長度也不一致,因此在前端進行展示時,就必須要考慮到最終的顯式問題,否則一旦切換語言環境那麼一定會導致頁面的佈局展示出現問題,處理方式無非幾種:

  • 允許文字內容換行展示,在文字發生換行時,要透過 CSS 設定按完整詞換行
  • 不允許換行的,就要控制固定寬度,超出部分打點展示,滑鼠移入展示全部內容等
  • 單獨為不同語言環境設定樣式,具體還是得看展示需求,如需要考慮不同語言環境下文字的對齊方式、文字間距等
  • 針對難以處理的翻譯內容,可以透過和業務溝通是否可以替換翻譯內容、縮減文字長度等等

    image.png

    考慮後端介面語言環境變更

    一個專案的國際化不可能都是前端來實現的,一些介面動態返回的內容也是需要後端去處理的,通常介面的請求頭中會儲存一個用於標識當前頁面語言環境的欄位,然後再決定返回給前端頁面的具體內容。

    image.png

基於前面處理的國家化切換功能,本身是會基於 vue 的響應式來切換翻譯內容的,即不會重新整理頁面,因此在切換對應翻譯內容後,同樣需要修改後續介面請求頭中的語言環境。

但這樣還是有問題的,已經透過介面返回的資料內容,此時沒有辦法切換成對應的翻譯內容,因為當前的國際化切換是基於頁面的變動,但基於介面變動的部分還沒重新請求獲取新的內容,那怎麼處理呢?

  • 前端切換語言環境後,重新重新整理頁面,讓介面也重新獲取新的內容
  • 後端在返回資料時,將對應的不同語言環境的翻譯內容一起返回,由前端根據語言環境決定如何渲染
  • 將所有的翻譯內容全部交由前端管理,一開始就初始化好各個語言環境對應的翻譯內容

    最後

    以上內容是基於國際化功能的一點思考,文中對應的思考點是筆者自己在專案中遇到的點,並不一定適合所有專案,當然也期待評論區給出更多、更好的方案。

06D85B74.gif

本文參與了SegmentFault 思否寫作挑戰賽,歡迎正在閱讀的你也加入。

相關文章