這两天手頭的一個使命是給一個五六年的老項目增加多言語。這個項目巨大且複雜,初期是用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了,運用要領重假如兩種:
- 在組件的template中,挪用
$t()
要領 - 在組件的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選項,題目處理!