
經歷了無數的崩潰,甚至後悔幹嘛使用 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 拿掉的話,不就無法記錄當前的語系狀態了嗎?
還是有更好的方式可以參考呢?