[ Vue3.0 ] 使用 Vue 3.0 / Vue-cli 4 開發 i18n 國際化 多國語言功能

Photo by Anastasia Dulgier on Unsplash

經歷了無數的崩潰,甚至後悔幹嘛使用 Vue 3.0 這個還尚未成熟的框架,要說 Vue2 網路上資源是一大堆,Vue 3.0 則少之又少,甚至有些根本就是 Vue2,為求加入 I18n 支援多國語言的功能吃了不少苦頭,網路上資源極其稀少,如今順利解決問題,踩坑的我也來小小做點貢獻。

Preface

開始之前要來大抱怨一下,網路上一堆文章,翻找翻到令人火大,主要原因是 vue-cli3 !== vue 3.0,但…查到一堆資料都是 vue-cli3 然後寫 vue2,又或者是 Vue3.0 的標題,內容程式碼語法都還是 Vue2.0,其次都是 Vue-cli3 拿來當標題,然後貼心的 Google 就認為你找 Vue3.0 就是在找 Vue-cli3,各種崩潰。

廢話說的也差不多了,會有這個坑主要是之前開發的 Django 效能太差,還記得當時也解決了 S3 檔案時效的問題「[ python3 ] via Django-Storages upload to S3 with Date path and ContentDisposition.」,為求使用者體驗更好更加更棒,所以希望做到前後端分離,於是看上了當紅的前端框架 Vue,身為前端苦手,在約莫六年前(大一),我碰到當紅的前端框架
Angular 碰壁的經驗,覺得前端是盡量能不碰就不碰的領域,但…看到這篇文章你就知道了…。

Environment

  • nodejs v10.19.0
  • vue 3.0
  • vue-cli 4.5.6

Start Project

由於重點放在 i18n,怎麼從頭開始創建專案就不廢話,如果你需要找教學,可以參考 MIS 腳印,但我不會說他就是把 vue3-cli 當標題的其中一位作者,但不得不說他的文章寫得很詳細,很值得參考。

假設專案已經創建好,目錄大概會長得像下面這樣,自行創建的部分不用緊張,該文章會教大家從複製貼上內容學習。

src/                # Vue Cli 專案目錄
...
├── assets          # 存放靜態檔案
├── components      # 各組件檔案
│   └── Menu.vue    # 選單組件(自行創建)
├── router          # 路由檔案
├── i18n            # 各語系目錄
│   ├── en.json     # 英文語系(自行創建)
│   └── tw.json     # 中文語系(自行創建)
├── store           # 儲存狀態用(自行創建)
│   └── index.js     
├── i18nPlugin.js   # i18n 核心檔案(自行創建)
├── App.vue         # 項目入口
└── main.js         # 項目的核心

Preceding Operation

首先有個必要的套件,但不曉得是內建、還是需額外安裝,已經搞混了,還是提供一下安裝指令。

npm install @vue/composition-api
# or
yarn add @vue/composition-api

GitHub: https://github.com/vuejs/composition-api

Start I18n (Debug)

首先我也是照著 MIS 腳印 的教學文一路走下來,但最後噴了一大堆 export 'default' (imported as 'Vue') was not found in 'vue' ,至於怎麼解的呢,這部分參考範例網路上範例只會一直撞牆,可以從官方文件的解決該問題,由於 Vue 3.0 不是像 Vue2 使用以下方式進行引用套件。

// src/main.js
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
import VueI18n from 'vue-i18n'

Vue.config.productionTip = false;
Vue.use(VueI18n)
new Vue({
  VueI18n,
  router,
  store,
  render: (h) => h(App),
}).$mount('#app');

所以 Vue 3.0 的 Code 需要改成像以下這樣。

// src/main.js
import { createApp } from 'vue'
import App from './App.vue';
import router from './router';
import { store } from "./store";

const myapp = createApp(App)
myapp.use(store)
myapp.use(router)
myapp.mount("#app");

眼尖的網友應該會發現 I18n 不見,沒錯!因為 Vue 3.0 的 I18n 不是寫在 main.js 裡,而是使用一種 Provide 與 Inject 的方式。

Vue 3.0 I18n init

先創立一個文件 src/i18nPlugin.js(其實名子怎麼取隨便),內容下方複製貼上。

// src/i18nPlugin.js
import { ref, provide, inject } from "vue";

const createI18n = config => ({
  locale: ref(config.locale),
  messages: config.messages,
  $t(key) {
    return this.messages[this.locale.value][key];
  }
});

const i18nSymbol = Symbol();

export function provideI18n(i18nConfig) {
  const i18n = createI18n(i18nConfig);
  provide(i18nSymbol, i18n);
}

export function useI18n() {
  const i18n = inject(i18nSymbol);
  if (!i18n) throw new Error("No i18n provided!!!");

  return i18n;
}

這部分的程式碼是參考 Articles Newsletter Create a i18n Plugin with Composition API in Vue.js 3,但最上頭的 import { ref, provide, inject } from "@vue/composition-api"; 做了點調整,改為 import { ref, provide, inject } from "vue";,其原因是該篇文章也混雜了 Vue2 的寫法,多次碰壁以後,在 composition-api 的官方文件上看到以下這段。

💡 When you migrate to Vue 3, just replacing @vue/composition-api to vue and your code should just work.

差點沒有把桌子翻過去,小小短短一行,還不是 Highlight,都編譯浪費多少人的秒數過去,才在文件上看到,而且參考的文章標題明明就寫著 in Vue.js 3,真好奇他的 Code 會不會 Work。

Language Packs

建立你要設置多國語言的語言包,名稱可自行定義,然後 Key 設置為變數、 Value 為對應的值,可以直接參考下方範例。

TW

{
  "//": "src/i18n/tw.json",
  "CreateLinkBtn":  "網址",
  "CreateImageBtn": "圖片",
  "CreateVideoBtn": "影片",
  "CreateAudioBtn": "音檔"
}

EN

{
  "//": "src/i18n/en.json",
  "CreateLinkBtn":  "Link",
  "CreateImageBtn": "Image",
  "CreateVideoBtn": "Video",
  "CreateAudioBtn": "Audio"
}

Store

該部分是用於儲存使用者所選擇的語言狀態,直接照抄MIS 腳印的範例,但最終執行你會發現動不了,由於是 Vue 2.0 的寫法,仍需修改 import 方式。

// src/store/index.js
import { createStore } from "vuex";

export const store = createStore({
  state: {
    lang: null  // 存放使用者選用的語系
  },
  mutations: {
    // 切換語系設定
    setLang (state, value) {
      state.lang = value;
    }
  },
  actions: {},
  modules: {}
});

importexport const store = createStore 兩處進行修改,其餘皆一樣,上方的 code 是已經改好的。

題外話

其實不是很清楚 Store 是怎麼在 Local 端紀錄使用者所選擇的語言的,在參考網路上的做法,有發現有其他人使用 Cookie 的方式來紀錄狀態,可以參考 vue 中英切換使用步驟,然後我又得吐槽這種文章,用 Vue2 標題下 Vue 3.0 真的令人翻白眼(引用的標題已將 3.0 去掉)。

App.vue (Provide)

首先佈署 src/App.vue 檔案內容,主要是下面那一段 script

// src/App.vue
<template>
  <div class="content">
    <router-view></router-view>
  </div>
  <Menu></Menu>
</template>

<script>
  import Menu from '@/components/Menu.vue';
  import { store } from "@/store";
  import { provideI18n } from "@/i18nPlugin";
  import tw from '@/i18n/tw'
  import en from '@/i18n/en'

  export default {
    components: {
      Menu
    },
    setup() {
      let locale = 'tw'
      if (localStorage.getItem('setLang')) {
        locale = localStorage.getItem('setLang');
        store.commit('setLang', locale);
      } else {
        store.commit('setLang', locale);
      }
      provideI18n({
        locale: locale,
        messages: {
          'tw': tw,
          'en': en
        }
      })
    }
  };
</script>

Components (Inject)

以下程式碼可以不用管其中的 CSS,因為我懶得移掉,這邊參考了不少網路資源,算是東奏西奏的綜合體,重點是可以 Work,直接貼了所有的程式碼,接著再慢慢解釋。

// src/components/Menu.vue
<template>
  <input type="checkbox" id="menu_switch" checked />
  <div class="et-hero-tabs-container nav-container" id="awd-site-nav">
    <router-link to="link" class="et-hero-tab" data-slide="link">{{ i18n.$t('CreateLinkBtn')}}</router-link>
    <router-link to="image" class="et-hero-tab" data-slide="image">{{ i18n.$t('CreateImageBtn')}}</router-link>
    <router-link to="video" class="et-hero-tab" data-slide="video">{{ i18n.$t('CreateVideoBtn')}}</router-link>
    <router-link to="audio" class="et-hero-tab" data-slide="audio">{{ i18n.$t('CreateAudioBtn')}}</router-link>
    <input type="button" value="tw" v-on:click="setLang('tw')" class="et-hero-tab" data-slide="audio" >
    <input type="button" value="en" v-on:click="setLang('en')" class="et-hero-tab" data-slide="audio" >
  </div>
</template>

<script>
  import { useI18n } from "@/i18nPlugin";
  import { store } from "@/store";
  export default {
    name: 'Menu',
    setup() {
      const i18n = useI18n();
      const setLang = (value) => {
        store.commit('setLang', value);
        localStorage.setItem('setLang', value);
        i18n.locale.value = value
      }
      return { i18n, setLang };
    }
  }
</script>

首先要注意的地方有,有幾個與參考範例不一樣的地方,分別是 importi18n.locale.value,並且使用 setup() 方式來運行,如果不懂 setup() 可以參考官方 Vue doc

Finish


透過右側兩個語言切換按鈕進行語言變換,上下圖為語言切換後的對照。

Conclusion

在寫這篇文章時,碰觸 Vue.js 僅僅只有大約五天的時間,先前除了 3 分鐘熱度的 Angular 以外,也都沒再碰其他 JavaScript 框架,一切都在十萬火急的情況下催生,然後還要寫這篇廢文,實在令人措手不及,根本還來不及搞懂一些原理及概念,像是這個例子使用 Vuex 的 store 來做狀態管理,但參照 MIS 腳印 的寫法,已經有使用 localStorage 來讓瀏覽器儲存狀態,所以就不是很懂,為什麼分別狀態還需要使用到兩種方式來記錄?

主要這篇文章新手應該只能學到複製貼上的功能,並且防止被網路上一堆文章誤導,由於我自己一開始以為自己使用的 vue3-cli 就等同於 vue3.js,於是碰壁好幾天,直到找到解法才發現,原來自己的 Cli 竟然是 4.X 版本,主要還是解決的最大的問題,完成 I18n 的國際化語言建置,對於一個對前端完全新手與開發苦手來說,真的是渾身解數,畢竟十萬火急,要在一個月內改完前端並上線,是一場跟時間賽跑的戰爭,還有後端 Django 的部分也還是另一個戰場…。

Referense

Vue Cli 3 使用 Vue I18n 實作多國語言網站和多語系切換功能 -> 概念結構說明都很棒,但是該篇文章使用 Vue Cli 3 + Vue2 而不是使用 Vue 3.0
Create a i18n Plugin with Composition API in Vue.js 3 -> 部分可參考,但仍然是 Vue 2
[譯] 用 Vue.js 3 Composition API 創建 i18n 插件 -> 樓上那篇的譯版,但內容好像也有稍微調整,仍然是 Vue 2
U n c a u g h t TypeError: Cannot read property ‘silent’ of undefined -> 如果你有遇到編譯可以過,但執行卻出現「’silent’ of undefined」可以參考
一、Vue國際化處理 vue-i18n -> 可參考 Cookie 做法

MksYi

透過網路分享知識的學習者。

8 thoughts to “[ Vue3.0 ] 使用 Vue 3.0 / Vue-cli 4 開發 i18n 國際化 多國語言功能”

  1. 您好,小弟參照您的教學文章實作,發現在router-view中似乎沒辦法正常使用vue-i18n,請問您是否有遇到相同的問題,若是有的話,又是如何解決的呢?

    1. Hello Jimmy:
      首先我不確定你提及的無法正常使用 vue-i18n 是什麼意思,若可以說明的更詳細一些,或是提供錯誤訊息,我會比較好掌握您的問題。
      由於文件分佈滿多檔案的,也建議可以逐步除錯。
      1. 確認語言包的json是否有被成功載入
      2. 確認程式程式是否編譯成功
      3. 編譯成功執行上是否出錯
      4. 重新確認程式相關變數是否對得上
      以上給您參考 :)

  2. 小弟後來隔天重試了一次,也沒有改動什麼,就可以正常使用了(?)
    不太確定是什麼地方弄錯了哈哈,謝謝大大分享教學。

    1. Hello superbing
      謝謝您的回饋
      不過 Vuex 的 store 我可以理解,但若將 localStorage 拿掉的話,不就無法記錄當前的語系狀態了嗎?
      還是有更好的方式可以參考呢?

  3. 感謝文章,受益良多!
    不過本人一開始多語系沒有設置en
    一直跳出一個找不到 json 內物件的錯誤
    console查了一下 i18nPlugin.js 發現是 locale: ref(config.locale) 指向 en 導致
    不過我照著文章 local 是設置 tw 跟其他語言,沒有 en
    最後只好設一個 en 才成功
    有點奇妙@@不知道是哪邊有設定錯誤….
    (簡單來說就是找不到 ref(config.locale) 那邊吃到的值要到哪預設,src/App.vue 那邊也跟版主寫的一樣是設 tw )

  4. 請問一下 Inject 可以註冊到全域嗎?看上面範例似乎只有注射到 menu.vue 我想要全局註冊 可以達到直接使用 {{ $t(‘CreateLinkBtn’)}} 不用寫 {{ i18n.$t(‘CreateLinkBtn’)}} 嗎?

  5. 找到 locale 問題點似乎是因為 ESLint 設定的關係,
    設定在 Airbnb 的狀況下 App.vue 要照文章中 Referense 數下來第三篇的方式 給 local 的值。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料