經歷了無數的崩潰,甚至後悔幹嘛使用 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: {}
});
將 import
與 export 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>
首先要注意的地方有,有幾個與參考範例不一樣的地方,分別是 import
與 i18n.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 做法
您好,小弟參照您的教學文章實作,發現在router-view中似乎沒辦法正常使用vue-i18n,請問您是否有遇到相同的問題,若是有的話,又是如何解決的呢?
Hello Jimmy:
首先我不確定你提及的無法正常使用 vue-i18n 是什麼意思,若可以說明的更詳細一些,或是提供錯誤訊息,我會比較好掌握您的問題。
由於文件分佈滿多檔案的,也建議可以逐步除錯。
1. 確認語言包的json是否有被成功載入
2. 確認程式程式是否編譯成功
3. 編譯成功執行上是否出錯
4. 重新確認程式相關變數是否對得上
以上給您參考 :)
小弟後來隔天重試了一次,也沒有改動什麼,就可以正常使用了(?)
不太確定是什麼地方弄錯了哈哈,謝謝大大分享教學。
Vuex 的 store以及localStorage都可以不需要的
Hello superbing
謝謝您的回饋
不過 Vuex 的 store 我可以理解,但若將 localStorage 拿掉的話,不就無法記錄當前的語系狀態了嗎?
還是有更好的方式可以參考呢?
感謝文章,受益良多!
不過本人一開始多語系沒有設置en
一直跳出一個找不到 json 內物件的錯誤
console查了一下 i18nPlugin.js 發現是 locale: ref(config.locale) 指向 en 導致
不過我照著文章 local 是設置 tw 跟其他語言,沒有 en
最後只好設一個 en 才成功
有點奇妙@@不知道是哪邊有設定錯誤….
(簡單來說就是找不到 ref(config.locale) 那邊吃到的值要到哪預設,src/App.vue 那邊也跟版主寫的一樣是設 tw )
請問一下 Inject 可以註冊到全域嗎?看上面範例似乎只有注射到 menu.vue 我想要全局註冊 可以達到直接使用 {{ $t(‘CreateLinkBtn’)}} 不用寫 {{ i18n.$t(‘CreateLinkBtn’)}} 嗎?
找到 locale 問題點似乎是因為 ESLint 設定的關係,
設定在 Airbnb 的狀況下 App.vue 要照文章中 Referense 數下來第三篇的方式 給 local 的值。