如何讓一個vue項目支持多語言(vue-i18n)


這兩天手頭的一個任務是給一個五六年的老項目添加多語言。這個項目龐大且復雜,早期是用jQuery實現的,兩年前引入Vue並逐漸用組件替換了之前的Mustache風格模板。要添加多語言,不可避免存在很多文本替換的工作,這么龐雜的一個項目,怎么才能使文本替換變得高效且不會引入bug是這篇文章主要要寫的東西。

引入vue-i18n

vue-i18n是一個vue插件,主要作用就是讓項目支持國際化多語言。首先我們引入這個插件:

import Vue from 'vue' import VueI18n from 'vue-i18n' Vue.use(VueI18n)

這里注意的就是vue插件的使用方法,通過全局方法 Vue.use() 使用插件。

插件通常會為 Vue 添加全局功能。插件的范圍沒有限制——一般有下面幾種:添加全局方法或者屬性;添加全局資源:指令/過濾器/過渡等;通過全局 mixin 方法添加一些組件選項;添加 Vue 實例方法,通過把它們添加到 Vue.prototype 上實現。

Vue.js 的插件應當有一個公開方法 install, 通過代碼可以更直觀的看出插件提供的功能:

MyPlugin.install = function (Vue, options) {
  // 1. 添加全局方法或屬性 Vue.myGlobalMethod = function () { // 邏輯... } // 2. 添加全局資源 Vue.directive('my-directive', { bind (el, binding, vnode, oldVnode) { // 邏輯... } ... }) // 3. 注入組件 Vue.mixin({ created: function () { // 邏輯... } ... }) // 4. 添加實例方法 Vue.prototype.$myMethod = function (methodOptions) { // 邏輯... } }

了解vue插件的install方法對我們等會查看i18n源碼有很大幫助。

使用vue-i18n

我們先看官方提供的最簡單的使用模板:

//HTML
<div id="app"> <p>{{ $t("message.hello") }}</p> </div> //JAVASCRIPT const messages = { en: { message: { hello: 'hello world' } }, ja: { message: { hello: 'こんにちは、世界' } } } const i18n = new VueI18n({ locale: 'ja', // set locale messages, // set locale messages }) new Vue({ i18n }).$mount('#app') //OUTPUT <div id="#app"> <p>こんにちは、世界</p> </div>

可以看到,我們在實例化Vue的時候,將i18n當做一個option傳了進去。之后我們就可以在vue的組件里使用i18n了,使用方法主要是兩種:

  1. 在組件的template中,調用$t()方法
  2. 在組件的script中,調用this.$i18n.t()方法

添加locales文件夾

上節的messages是一個包含了多語言的的對象,它就像我們的字典。既然是字典,我希望它只有一本。所以我只會new VueI18n()一次,並將實例化得到的i18n對象作為唯一的字典。

所以新建一個locales文件夾,存放所有跟多語言相關的代碼。目前包含三個文件:index.js, en.json, zh.json。

en.json和zh.json就是我們的語言包,是一個json形式。這里為了對照方便,我們必須保證語言包的內容是一一對應的。然后我們在index.js中完成設置。

import Vue from 'vue' import VueI18n from 'vue-i18n' Vue.use(VueI18n) const DEFAULT_LANG = 'zh' const LOCALE_KEY = 'localeLanguage' const locales = { zh: require('./zh.json'), en: require('./en.json'), } const i18n = new VueI18n({ locale: DEFAULT_LANG, messages: locales, }) export const setup = lang => { if (lang === undefined) { lang = window.localStorage.getItem(LOCALE_KEY) if (locales[lang] === undefined) { lang = DEFAULT_LANG } } window.localStorage.setItem(LOCALE_KEY, lang) Object.keys(locales).forEach(lang => { document.body.classList.remove(`lang-${lang}`) }) document.body.classList.add(`lang-${lang}`) document.body.setAttribute('lang', lang) Vue.config.lang = lang i18n.locale = lang } setup() export default i18n 

我們對外提供了一個setup()的方法,給使用者修改當前使用語種的能力。同時,我們在setup里還做了三件事:
將當前語種存到 localStorage中,保存用戶的使用習慣;給body添加語種相關的class,因為不同語言可能導致排版出現差異,我們需要適配;將當前語種存到Vue的全局配置中,以便未來可能的使用。

最后我們在main.js中引入這個Index.js即可。

import Vue from 'vue' import App from './app.vue' import store from './store' import router from './router' ... import i18n from '@crm/locales' ... new Vue({ i18n, router, store, render: h => h(App), }).$mount('#app')

這樣看起來,我們的國際化已經完成了,然而之后馬上就有新的問題出現了!

問題一:vue實例外的js代碼中的文本怎么替換?

前面說到,vue實例中我們可以使用this.$i18n.t,這里的this是vue的實例。那項目中很多js代碼在vue的實例之外,我們要怎么辦?

最簡單的解決方法是這樣的,我們的locales/index.js這個文件已經export了i18n這個對象,那我們只需要在每次要使用的時候手動將i18n導入進來就可以了。

<script> import i18n from '@crm/locales' //const test = "測試數據" const test = i18n.t('message.test') </script>

可是這樣一來,我們之后做諸如上面的文本替換時,就得小心翼翼的區別是否在vue實例中。如果是,我們用this.$i18n.t,否則先import然后用i18n.t。這顯然增加了復雜性!

為了解決這個問題,只直接的解決辦法就是將i18n掛到window下,變成全局變量。我們就不必再Import進來,同時只使用統一方法:i18n.t

我們在main.js中添加這行代碼:

import Vue from 'vue' import App from './app.vue' import store from './store' import router from './router' ... import i18n from '@crm/locales' ... window.i18n = i18n new Vue({ i18n, router, store, render: h => h(App), }).$mount('#app')

然后我們興高采烈的將組件中的import i18n全去掉,並將this.$i18n.t改為i18n.t。然后項目跑起來就報錯了:i18n is not defined。

問題出在哪里?顯示是組件調用i18n的時候,i18n還沒有掛載到window上,所以是執行順序出了問題。我們先來看一下下面代碼的執行順序:

//假設webpack的入口文件是```main.js```
 
//main.js import moduleA from 'moduleA' console.log(1) import moduleB from 'moduleB' console.log(2) //moduleA.js console.log(3) //moduleB.js console.log(4) //最終在瀏覽器中打印出的數字順序是: 3 4 1 2

為什么會這樣呢?跟ES6 module的機制有關系。import命令具有提升效果,會提升到整個模塊的頭部,首先執行。這種行為的本質是,import命令是編譯階段執行的,在代碼運行之前。

這樣我們就找出之前報錯的原因了,我們先import了App, router這些視圖,然后Import的i18n並掛載到window。所以組件的script中的代碼會最先執行,而此時i18n並未開始。所以我們首先將window.i18n = i18n移到locales/index中,然后調整main.js中import的順序:

//locales/index
...
setup() window.i18n = i18n export default i18n //main.js import Vue from 'vue' import i18n from '@crm/locales' import App from './app.vue' import store from './store' import router from './router' ...

問題二:假如存在很多個new Vue()怎么辦?

前面我們在main.js的new Vue({i18n, ...})中將i18n作為option放了進去,但很快我發現這個項目並只有一個Vue的實例。全局搜索發現一共有70多個。

項目中很的諸如彈窗之類的組件,都是直接自己實例化一個Vue然后自己$mount()到DOM中。這些組件在實例化的過程中並沒有混入i18n選項,所以他們的template上自然找不到$t()方法。

怎么辦?難道給每一個new Vue()都手動添加i18n選項嗎?肯定不行,首先我們要給添加70多次,其次如果未來又有人寫了新的new Vue()忘了添加Ii8n,那又回導致報錯。所以我們要想一個萬全的法子。

官方文檔里找不到解決辦法,看來我們得hack一下了。首先我們來查vue-i18n的源碼,找到$t()方法是怎么工作的。

全局搜索$t,找到定義它的地方:

  Object.defineProperty(Vue.prototype, '$t', {
    get: function get () { var this$1 = this; return function (key) { var values = [], len = arguments.length - 1; while ( len-- > 0 ) values[ len ] = arguments[ len + 1 ]; var i18n = this$1.$i18n; return i18n._t.apply(i18n, [ key, i18n.locale, i18n._getMessages(), this$1 ].concat( values )) } } });

可以看到$t掛載在Vue.prototype上,每當我們在實例中調用$t時,其實我們是在調用this.$i18n對象上的_t方法。現在問題變成,實例上的$i18n是什么是時候定義的。

全局搜索$i18n,我們找到了前面提到過的每個插件必須提供的install方法:

function install (_Vue) {
  Vue = _Vue;
  
  ...

  Object.defineProperty(Vue.prototype, '$i18n', { get: function get () { return this._i18n } }); extend(Vue); Vue.mixin(mixin); Vue.directive('t', { bind: bind, update: update }); Vue.component(component.name, component); // use object-based merge strategy var strats = Vue.config.optionMergeStrategies; strats.i18n = strats.methods; }

可以看到$i18n一開始就被定義在了Vue.prototype上,每次調用的時候其實我們是在調用this._i18n,所以現在問題變成實例的_i18n在哪里。同時可以看到在Install中我們還混入了mixin, directive, component,這些在上面都有提過它的作用。

var mixin = {
  beforeCreate: function beforeCreate () {
    var options = this.$options; options.i18n = options.i18n || (options.__i18n ? {} : null); if (options.i18n) { if (options.i18n instanceof VueI18n) { ... this._i18n = options.i18n;

我們在mixin中找到了this._i18n的來源,前面提到mixin會被注入到組件中。在每個組件創建前,我們將this.$options的i18n給了this._i18n。

這個this.$options是什么?它的使用方式是Vue.mixin(mixin),所以我們看一下vue的文檔:全局混入

// 為自定義的選項 'myOption' 注入一個處理器。 Vue.mixin({ created: function () { var myOption = this.$options.myOption if (myOption) { console.log(myOption) } } }) new Vue({ myOption: 'hello!' }) // => "hello!"

所以this.$options就是我們new Vue時提供的選項對象。

所以問題的根源就是除了main.js中的new Vue外,其余70多個new Vue我們沒有混入i18n這個選項。怎樣才可以讓每次new Vue時自動將i18n混入選項呢?看上去我們只能修改Vue的源碼了。

function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword'); } this._init(options); }

可以看到每次Vue實例化時,會調用_init方法,這個方法從哪里來呢?

function initMixin (Vue) { Vue.prototype._init = function (options) { ...

在Vue.prototype上,所以我們只需要修改Vue.prototype就好了。

//locales/index
const init = Vue.prototype._init Vue.prototype._init = function(options) { init.call(this, { i18n, ...options, }) } 

這樣我們在任何時候new Vue()就自動添加了i18n選項,問題解決!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM