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

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

引入vue-i18n

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

import Vue from 'vue'
import VueI18n from 'vue-i18n'

Vue.use(VueI18n)

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

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

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

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源碼有很大幫助。git

使用vue-i18n

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

//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了,使用方法主要是兩種:web

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

添加locales文件夾

上節的messages是一個包含了多語言的的對象,它就像咱們的字典。既然是字典,我但願它只有一本。因此我只會new VueI18n()一次,並將實例化獲得的i18n對象做爲惟一的字典。json

因此新建一個locales文件夾,存放全部跟多語言相關的代碼。目前包含三個文件:index.js, en.json, zh.json。瀏覽器

en.json和zh.json就是咱們的語言包,是一個json形式。這裏爲了對照方便,咱們必須保證語言包的內容是一一對應的。而後咱們在index.js中完成設置。app

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選項,問題解決!

相關文章
相關標籤/搜索