[Vue Note] — 在 Vue3 優雅的引入 I18n 之餘,透過網站統一管理並一鍵更新

Introduction & 前言

I18n

還在找怎麼最簡單在 Vue3 引入 I18n 的使用方式嗎?看到這邊就對了,Vue2 也適用。

首先強調本篇絕對不是說 Vue3 結果使用 Vue Cli3.0,是確確實實的 Vue3 沒有錯,放心地看下去沒關係;至於會有這篇文章也是剛好最近的 Side Project 碰到多語系的功能,第一次實作後將過程記錄下來,如果中途或是內容有任何錯誤,歡迎下方留言告訴筆者。

先說一下筆者的環境(當然 Vue2 也可以參考):

  • nodejs v12.16.1
  • vue 3.0
  • vue-cli 4.5.12

首先如果不知道 I18n 的話這邊快速解釋一下,就是透過 I18n 這個套件可以做到一個網站多語系;在我們手動建立好各種語言的翻譯之後,只要將套件引入,在隨時都可以切換指定現在要使用什麼語系顯示文字,達到多語系網站的目的。

引用套件其實並不稀奇,但碰到了 Vue3 + TypeScript 對還不熟的筆者來說差點要我命3000,在確定引入成功後,想起前陣子在公司接觸到的專案,在厲害的其他前端同事處理之下可以達到指令更新最新的語言包,並且透過網站統一管理,各自新增並且不影響 commit,這邊就深深覺得一定要筆記了。

筆者目前還沒有在網路上看到相關的教學,如果有的話還請見諒,也歡迎互相切磋交流。

詳細解釋為麼叫 I18n, 沒有興趣可跳過。

I18n 的全名為 『Internationalization』,共有20個字母,去掉頭尾的 In 中間有 18個字,如果每次提到都要打或是說這麼長的一串文字其實很麻煩,所以就乾脆簡稱 『I18n』,筆者剛開始還以為是不是拿了18國當範例之類的,請見諒我是小菜雞。


Summary & 摘要

本次筆記將會記錄幾個摘要,如果你不想要透過網站統一管理,那其實做到安裝完套件並且引入即可,不會太麻煩。

  1. I18n 引入方式

  2. I18n 進階用法

  3. 線上網站統一管理語言包

  4. 一鍵更新語言包


I18n 引入方式

上網查詢了一下 VueI18n 引入方式有些都特別麻煩,這邊簡單的記錄一下該怎麼引入。

安裝套件

1
2
3
4
5
$npm i vue-i18n -S

or

$yarn add vue-i18n -S

安裝 vue-i18n 即可,有些教學會請你安裝類似 vue-i18n@next 這種的套件,其實不需要再另外安裝。

檔案架構

接著在專案 /src 下新增放語言包的資料夾及檔案,如下:

新增資料夾

有些教學會請你新增的語言包是分開的,例如 en.jsonch.json…等等也可以,但是在引入語言包的時候需要注意!

這邊 common.json 裡面請先打上下面的內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
// @/src/i18n/common.json

{
"zh-TW": {
"welcome": "歡迎"
},
"ja-JP": {
"welcome": "ようこそ"
},
"en-US": {
"welcome": "Welcome",
}
}

沒錯上面就是我們的語言包,如果拆成多支檔案,只需要鍵入內容即可(以下為範例):

1
2
3
4
5
// ./src/i18n/ch.json

{
"welcome": "歡迎"
}

index.ts(或是 index.js) 打上下列內容:

  1. 如果是合併再一起的語言包
1
2
3
4
5
6
7
8
9
10
11
12
// ./src/i18n/index.ts

import { createI18n } from "vue-i18n";
import message from "./common.json";

const i18n = new createI18n({
locale: "zh-TW",
messages: message,
fallbackLocale: "zh-TW",
});

export { i18n };
  1. 如果是分開的語言包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ./src/i18n/index.ts

import { createI18n } from "vue-i18n";
import ch from "./ch.json";
import en from "./en.json";

const i18n = new createI18n({
locale: "zh-TW",
messages: {
ch,
en
},
fallbackLocale: "zh-TW",
});

export { i18n };

所以其實分開合再一起結果都一樣的,但因為後面方便使用網站管理,這邊筆者使用合併的方式。

引入套件

下一步到我們的入口 main.tsmain.js,引入剛剛使用的 I18n 套件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ./src/main.ts

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";

import { i18n } from "@/i18n"; // 引入 I18n 套件

createApp(App)
.use(store)
.use(router)
.use(i18n) // <-- 這樣就全局引入了
.mount("#app");

使用套件

到此地你已經可以使用多語系了,到隨便一個頁面,新增選項,就可以更改多語系:

1
2
3
4
5
6
7
8
9
10
11
12
// ./src/App.vue

<template lang="pug">
button(
@click="$i18n.locale = 'ch'"
) 繁體中文
button(
@click="$i18n.locale = 'en'"
) English

h2 {{ $t("-welcome") }}
</template>

也許你查看了許多文章,會叫你使用 import { useI18n } from "vue-i18n"; 引入套件,然後透過 const { t, locale } = useI18n(); 來渲染文字或者更改語言,其實 t 是可以省略的,如果有另外寫 methods 才需要用到引入套件並且使用 locale 去更改語系。

到這邊你已經成功的將多語系引入進來了,下一步將會講解如何在使用者關閉網頁之後也持續記錄著使用者所選擇的語系。


I18n 進階用法

其實也不算進階用法,這一步只是為了要紀錄使用者所選擇的語系,在關閉網頁之後不會再讓語系跳回來,所以我們要做的事情有幾件。

  1. 將使用者語系紀錄到 LocalStorage

  2. 使用 VueX 去更改使用者選擇的語系,也順便將語系紀錄在 VueX(非必要,如果想在任何一頁單獨修改也可以)

修改 VueX

為了達到上面的目的,我們必須先改寫 VueX 這邊,在這邊我們統一透過 localStorage.setItem("i18nLang", value); 來紀錄目前使用者所選擇的語系 localStorage,這樣下次我們只要撈取 localStorage 便可以知道使用者上次有沒有選擇了什麼語系。

打開 ./src/store/index.ts 這隻檔案並且修改為:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// ./src/store/index.ts

import { createStore } from "vuex";

// Prototype[State]
type stateType = {
langsOption: lang.langsOptionType[];
lang: string | null;
};

const stateInit: stateType = {
langsOption: [
{
id: 0,
key: "zh-TW",
title: "繁體中文",
},
{
id: 1,
key: "en-US",
title: "English(United States)",
},
{
id: 2,
key: "ja-JP",
title: "日本語",
},
],
lang: null,
};

export default createStore({
state: stateInit,
mutations: {
// 切換語系設定
setI18nLang(state, value) {
state.lang = value;
localStorage.setItem("i18nLang", value);
},
},
actions: {},
modules: {},
});

如果不是 TypeScript 的話直接使用下面的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// ./src/store/index.js

import { createStore } from "vuex";

export default createStore({
state: {
langsOption: [
{
id: 0,
key: "zh-TW",
title: "繁體中文",
},
{
id: 1,
key: "en-US",
title: "English(United States)",
},
{
id: 2,
key: "ja-JP",
title: "日本語",
},
],
lang: null,
},
mutations: {
// 切換語系設定
setI18nLang(state, value) {
state.lang = value;
localStorage.setItem("i18nLang", value);
},
},
actions: {},
modules: {},
});

改寫套件引入檔案

接著改寫 ./src/i18n/index.ts 這隻檔案,改寫成下面的內容(聰明的你一定知道這邊要去讀取 localStorage 拿取上次使用者是否有選擇語系):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ./src/i18n/index.ts

import store from "@/store";
import { createI18n } from "vue-i18n";
import message from "./common.json"; // 這邊之後統一示範全部檔案集中在同一支 JSON 的方式

let locale = "zh-TW";

// 判斷之前是否有改過語系, 讀取紀錄
if (localStorage.getItem("i18nLang")) {
// 必須先賦予一個變數, 不然 TS 會出現 "類型 'string | null' 不可指派給類型 'string'。類型 'null' 不可指派給類型 'string'。" 的錯誤
const localeData = localStorage.getItem("i18nLang");
locale = localeData !== null ? localeData : "zh-TW";
} else {
localStorage.setItem("i18nLang", locale);
}

// VueX 更改
store.commit("setI18nLang", locale);

const i18n = new createI18n({
locale,
messages: message,
fallbackLocale: "zh-TW",
});

export { i18n };

這邊題外話一下, const localeData = localStorage.getItem("i18nLang"); 那邊一定要這樣使用,不然會噴錯,如果有哪路大神知道為什麼的歡迎在下方留言告訴筆者,感激不盡。

最後修改要讓使用者選擇的頁面

這邊就依照個人喜好修改,附上使用者覺得很方便的 select 使用 v-model 綁定 VueX 的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 某支 .vue 檔

<template lang="pug">
select(
v-model="lang"
)
option(
v-for="lang in langsOption",
:key="lang.id",
:value="lang.key"
) {{ lang.title }}
</template>

<script>
import { defineComponent, ref, computed } from "vue";
import { useStore } from "vuex";
import { useI18n } from "vue-i18n"; // 這邊必須要透過 methods 去修改語系,所以必須要引入 vue-i18n,另外也可寫在 VueX 看個人喜好

export default defineComponent({
name: "index",
setup() {
// Vue Tools
const store = useStore();

// i18n
const { locale } = useI18n();

return {
langsOption: computed(() => store.state.langsOption),
lang: computed({
get: () => store.state.lang,
set: (val) => {
if (val === store.state.lang) return; // 如果和原本一樣就不更改
locale.value = val; // 修改 i18n 套件的語系設定, 這邊也可改寫在 VueX, 依個人喜好
store.commit("setI18nLang", val); // 修改 VueX 的值
},
}),
};
},
});
</script>

透過上面方法即可達到快速更改語系,也可記錄使用者語系至 localStorage,到這邊基本的用法都差不多了,如果你想再繼續下去學習透過網站統一管理語言包就接著下一步吧 Go~


Localise.biz

線上網站統一管理語言包

其實本地新增編輯語言包真的不太方便,如果多人開發的時候又會碰到需要解 Git 衝突的情況,今天如果有幾十個專案,就又要一個一個比對並且複製貼上去修改,太不人性化了。

因為前陣子筆者接觸公司的專案,發現強大的前端同事使用了一個方便的網站,索性便在這次的 Side Project 使用上,這邊也一起記錄下來。

註冊網站帳號

Plan

沒錯,因為需要透過網站去申請帳號密碼,才能把語言包紀錄在該網站的資料庫,但放心,免費的額度還是有的。

『每一個字串一種語言為一個項目,免費額度基本上的項目為 2000』,其實很夠用了,因為該網站為專案式管理語言包,所以如果公司專案覺得不錯的話,其實可以小花 $19.59/mo(美金) 購買商業方案,筆者覺得很划算。

建立專案

申請結束後請建立專案,然後輸入專案名稱。

Create New Project

這邊可以看見我已經有使用一個專案,因為筆者裡面設定了三種語言,所以每一個項目都會*3,目前有九個翻譯,就是佔用了 27Translation

另外這個網站還有個好處是語言包除了可以下載之外,也可以匯入,所以其實是很安全的,不怕語系檔案不見。

建立第一個翻譯

第一個翻譯

之後點擊綠色的按鈕新增第一個翻譯,然後在 Asset ID 輸入要設定的語系 Key,這邊先測試輸入 welcome

目前語系

這邊你會發現目前只有中文,查看右邊的地方有一個地球的 Icon,點擊後下面會出現 New locale,再度點擊後會彈出一個視窗,這時候就可以輸入你想新增的語系了,但記得,語系越多,佔用的 Translation 就會越多!

新增語系

最後在大框框那邊輸入你想翻譯的文字,然後再透過下拉去切換語言,把你設定的語言都新增好,右邊地球那邊會顯示你的百分比,如果某些語言沒有翻譯,就不會 **100%**。

拿取 API Key

記好你的Key

與大多的網站差不多,通常要打那個網站的 API,都會需要帶 Key,一樣看到剛剛右邊的地球,點擊地球右邊的板手 Icon,下方有一個 API Keys,點擊後又會彈出一個視窗,記下你的 API Keys,記得請使用上方的,下方的會包含可以修改的權限,這邊我們不需要。

放心這個專案在示範完之後就會砍掉了,所以上面的 Key 是沒用的。

查閱 Document

接著到網站的 API 文件 可以看見許多 API 的介紹,這邊我們只需要找到 Export API 就可以了,進去後我們需要用到的是 GET /api/export/all.{ext} 這支 API;有些人會使用 GET /api/export/archive/{ext}.zip,但這個需要再解壓縮並且引入方式也要分開,筆者採用最暴力且簡單的方式,透過 API 取得語言包的 JSON 然後載入套件中。

測試 API

這邊推薦可以先使用該網站的 API Explorer 去查看你的 API 打不打得過,類似 Swagger,很方便。


一鍵更新語言包

這邊就要開始來講最後的重頭戲了,怎麼透過指令去直接抓取最新的語言包呢?首先到專案目錄新增 loadi18nFile.js 這支檔案,然後在裡面打上下列的內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 至 Loco 網站抓取語言檔(這邊直接抓取 JSON 不需再分資料夾)
const https = require('https');
const fs = require('fs')
const path = require('path');
const bl = require('bl');
const colors = require('colors');
const i18nPath = path.resolve(__dirname, './src/i18n');
const locoApiKey = "p90GaanWIslvbDG01a41e72qyown4xZC";
const locoGetJSONApiUrl = "https://localise.biz/api/export/all?key=";
const localesDownloadUrl = `${locoGetJSONApiUrl}${locoApiKey}`;

function downloadLocoJson() {
https
.get(localesDownloadUrl, (response) => {
response.setEncoding('utf8');
response.pipe(bl((err, data) => {
if (err) {
reject(err);
console.log('Loco JSON pipe failed'.red);
}
let i18nJSON = data.toString();
if (i18nJSON) {
fs.writeFile(`${i18nPath}/common.json`, i18nJSON, (err) => {
if (err) {
console.log('Loco JSON writeFile failed'.red);
}
console.log('Loco JSON writeFile success'.green);
});
}
console.log('Loco JSON downloading success'.green);
}));
})
.on('error', err => {
// Handle errors
console.log('Loco JSON downloading failed'.red);
console.error(err);
});
};

downloadLocoJson();

如果想謹慎一點,也可以將 API Key 放在 .env 中,再透過套件 require('dotenv').config(); 去取得 process.env.VUE_API_KEY 也會比較安全。

關於上面的內容也可以自行研究一下,例如 setEncodingwriteFile 都可以在網路上查看該用法是什麼,這個比較偏向 Node.js 故這邊不多做解釋,有興趣可以參考 [NodeJS Become A Full Stack Developer] — 從0開始 NodeJS 小試身手

大功告成

最後的最後我們只需要修改 package.json

1
2
3
4
5
6
7
8
9
10
11
{
"name": "i18n-Test-project",
"version": "0.1.0",
"scripts": {
"serve": "node loadI18nFile && vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"loadI18n": "node loadI18nFile"
},
// ... 略
}

之後就可以透過 yarn run loadI18nnpm run loadI18n 來直接更新語言包啦!或是在 yarn serve 之前先去更新一波語言包,這樣多人開發起來更方便了。


Conclusion & 結論

這次在做的 Side Project 其實是從去年底開始因為家人剛好有需求,拿來一邊練習一邊開發的,原本前台的頁面其實都做好了,因為想挑戰看看開始實作後台,這時候才了解後端工程師真的辛苦啊(原本就深感辛苦,別砲我QQ)。

透過這次的練習其實又收穫滿滿,除了上一篇的 [Vue Notes] — 在 Vue 優雅的引入 SVG 幾種方式 之外,這次又學到怎麼使用 I18n。對了,還有被 Vue3 + TS 摧殘得不成人形…

希望之後能學到更多優雅的使用方式再繼續上來筆記,如果各位有什麼更好的方式也請不要吝設在下方留言讓筆者知道,感謝各位。

結論的最後也再次希望近期的疫情可以快點結束,願那些因為疫情而辛苦或是受傷的人也可以事事順利或早日康復,願台灣會更好願你我都會更好,一起加油!


參考網站