說明:本文基于element-ui@2.13.0输拇,源碼詳見element摘符。
常見的國際化方案有:
ECMAscript Intl:見前端國際化、前端國際化利器 - Intl
angular-translate
react-intl
vue-i18n
在講elementUI的國際化方案之前策吠,先講講vue-i18n
逛裤。
一、 vue-i18n
vue-i18n
是一種常見的國際化解決方案猴抹。下面就幾個關鍵點講講别凹。
1.1 代碼演示
// step1: 在項目中安裝vue-i18插件
cnpm install vue-i18n --save-dev
// step2:在項目的入口文件main.js中引入vue-i18n插件
import Vue from 'vue'
import router from './router'
import VueI18n from 'vue-i18n'
Vue.use(VueI18n)
const i18n = new VueI18n({
locale: 'zh', // 語言標識
messages: {
'zh': require('./assets/lang/zh'),
'en': require('./assets/lang/en')
}
})
// vue實例中引入
/* eslint-disable no-new */
new Vue({
el: '#app',
i18n,
router,
template: '<Layout/>',
components: {
Layout
},
})
// step3:頁面中使用
// zh.js
module.exports = {
menu : {
home:"首頁"
},
content:{
main:"這里是內(nèi)容"
}
}
// en.js
module.exports = {
menu : {
home:"home"
},
content:{
main:"this is content"
}
}
// 業(yè)務代碼
<div class="title">{{$t('menu.home')}}</div>
<input :placeholder="$t('content.main')" type="text">
// 渲染結(jié)果(應用zh.js)
<div class="title">首頁</div>
<input placeholder="這里是內(nèi)容" type="text">
1.2 功能
支持復數(shù)、日期時間本地化洽糟、數(shù)字本地化炉菲、鏈接、回退(默認語言)坤溃、基于組件本地化拍霜、自定義指令本地化、組件插值薪介、單文件組件祠饺、熱重載、語言變更及延遲加載汁政。
功能繁多道偷,在此主要講一下單文件組件
、基于組件的本地化
记劈、自定義指令
和延遲加載
三塊勺鸦。
- 1.2.1
$i18n
和$t
vue-i18n
的初始化方法內(nèi)部會生成一個vue實例_vm
,如1.1 代碼演示-step2
中所示目木,VueI18n實例中的locale和messages等信息會注入這個vue實例中:
_initVM (data: {
locale: Locale,
fallbackLocale: Locale,
messages: LocaleMessages,
dateTimeFormats: DateTimeFormats,
numberFormats: NumberFormats
}): void {
const silent = Vue.config.silent
Vue.config.silent = true
this._vm = new Vue({ data })
Vue.config.silent = silent
}
this._initVM({
locale,
fallbackLocale,
messages,
dateTimeFormats,
numberFormats
})
1.2.1.1 vue-i8n
的install方法
export function install (_Vue) {
......
extend(Vue) // 往Vue.prototype上掛載一些常用方法或?qū)傩曰煌荆?i18n、$t、$tc和$d等
Vue.mixin(mixin) // 往每個vue示例注入i18n屬性等
Vue.directive('t', { bind, update, unbind }) // 全局指令军拟,名為v-t
Vue.component(interpolationComponent.name, interpolationComponent) // 全局組件剃执,名為i18n
Vue.component(numberComponent.name, numberComponent) // 全局組件,名為i18n-n
// use simple mergeStrategies to prevent i18n instance lose '__proto__'
const strats = Vue.config.optionMergeStrategies // 定義一個合并的策略
strats.i18n = function (parentVal, childVal) {
return childVal === undefined
? parentVal
: childVal
}
}
1.2.1.2 extend(Vue)
:往Vue.prototype上掛載一些常用方法或?qū)傩孕赶ⅲ?code>$i18n肾档、$t
、$tc
和$d
等
export default function extend (Vue: any): void {
if (!Vue.prototype.hasOwnProperty('$i18n')) {
Object.defineProperty(Vue.prototype, '$i18n', {
get () { return this._i18n }
})
}
Vue.prototype.$t = function (key: Path, ...values: any): TranslateResult {
const i18n = this.$i18n
return i18n._t(key, i18n.locale, i18n._getMessages(), this, ...values)
}
......
1.2.1.3 Vue.mixin(mixin)
:全局混入beforeCreate辫继、beforeMount 和beforeDestroy方法阁最,使每個vue示例注入i18n屬性等,給每個vue組件添加_i18n
屬性
beforeCreate (){
const options = this.$options
options.i18n = options.i18n || (options.__i18n ? {} : null)
if (options.i18n) {
if (options.i18n instanceof VueI18n) {
// init locale messages via custom blocks
if (options.__i18n) {
try {
let localeMessages = {}
// options.__i18n即單文件vue組件中<i18n></i18n>標簽里的內(nèi)容
options.__i18n.forEach(resource => {
localeMessages = merge(localeMessages, JSON.parse(resource))
})
Object.keys(localeMessages).forEach((locale: Locale) => {
/*
mergeLocaleMessage 骇两,就是把組件里i18n標簽的數(shù)據(jù)合并到_vm實例的messages
this._vm.$set(this._vm.messages, locale, merge({}, this._vm.messages[locale] || {}, message))
*/
options.i18n.mergeLocaleMessage(locale, localeMessages[locale])
})
} catch (e) {......}
}
this._i18n = options.i18n
// watchI18nData的作用見下一小節(jié)
this._i18nWatcher = this._i18n.watchI18nData()
} else if (isPlainObject(options.i18n)) { // i18n是普通對象速种,而不是VueI18n實例
// component local i18n
// 在extend(Vue)中往Vue.prototype中注入了$i18n
if (this.$root && this.$root.$i18n && this.$root.$i18n instanceof VueI18n) {
options.i18n.root = this.$root
options.i18n.formatter = this.$root.$i18n.formatter
......
options.i18n.preserveDirectiveContent = this.$root.$i18n.preserveDirectiveContent
}
// init locale messages via custom blocks
if (options.__i18n) {
......
// 大致邏輯同上
}
// 大致邏輯同上
}
}
} else if (this.$root && this.$root.$i18n && this.$root.$i18n instanceof VueI18n) {
// root i18n
this._i18n = this.$root.$i18n
} else if (options.parent && options.parent.$i18n && options.parent.$i18n instanceof VueI18n) {
// parent i18n
this._i18n = options.parent.$i18n
}
},
beforeMount (): void {
const options: any = this.$options
options.i18n = options.i18n || (options.__i18n ? {} : null)
if (options.i18n) {
......
// 講當前vue實例添加到全局_dataListeners數(shù)組中,當有watch方法通知時低千,遍歷這些實例配阵,并調(diào)用$forceUpdate方法更新
this._i18n.subscribeDataChanging(this)
this._subscribing = true
} else if (this.$root && this.$root.$i18n && this.$root.$i18n instanceof VueI18n) {
this._i18n.subscribeDataChanging(this)
this._subscribing = true
} else if (options.parent && options.parent.$i18n && options.parent.$i18n instanceof VueI18n) {
this._i18n.subscribeDataChanging(this)
this._subscribing = true
}
},
1.2.1.4 更新機制
:在上一節(jié)Vue.mixin(mixin)
中,有this._i18nWatcher = this._i18n.watchI18nData()
示血,其作用就是通知各vue實例更新棋傍,類似的還有watchLocale
方法(監(jiān)控locale變化)
watchI18nData (): Function {
const self = this
// 在`1.2 功能 $i18n和$t節(jié)`中,全局_vm實例的data屬性难审,保存有l(wèi)ocale和messages等信息
return this._vm.$watch('$data', () => {
let i = self._dataListeners.length // _dataListeners保存有各vue實例
while (i--) {
Vue.nextTick(() => {
self._dataListeners[i] && self._dataListeners[i].$forceUpdate() // 強制更新
})
}
}, { deep: true })
}
v-t
指令:不詳細講了瘫拣,不外乎是利用vm.$i18n做一些數(shù)據(jù)的更新操作,用法見自定義指令本地化
- 1.2.2
單文件組件
示例
代碼如下告喊,可以在組件內(nèi)管理國際化麸拄。
<i18n>
{
"en": {
"hello": "hello world!"
},
"ja": {
"hello": "こんにちは、世界黔姜!"
}
}
</i18n>
<template>
<div id="app">
<label for="locale">locale</label>
<select v-model="locale">
<option>en</option>
<option>ja</option>
</select>
<p>message: {{ $t('hello') }}</p>
</div>
</template>
<script>
export default {
name: 'app',
data () { return { locale: 'en' } },
watch: {
locale (val) {
this.$i18n.locale = val
}
}
}
</script>
webpack配置(對于 vue-loader v15 或更高版本):
module.exports = {
// ...
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
},
{
resourceQuery: /blockType=i18n/,
type: 'javascript/auto',
loader: '@kazupon/vue-i18n-loader'
}
// ...
]
},
// ...
}
vue-i18n-loader
拢切,主要是用來解析vue單文件
中<i18n></i18n>
這種自定義標簽,根據(jù)下面的loader源碼秆吵,可以看出:
-
i18n
標簽內(nèi)的內(nèi)容可以是yaml
格式淮椰,也可以是json(5)或一般文本格式,這塊主要是通過convert
方法處理的纳寂; - generateCode主要用來解析vue單文件組件內(nèi)
i18n
標簽(可以參考vue 自定義塊主穗,標簽內(nèi)容被保存在__i18n
數(shù)組內(nèi) )和一些特殊字符(如\u2028、\u2029和\u0027毙芜,參考json中常遇到的特殊字符)忽媒。
import webpack from 'webpack'
import { ParsedUrlQuery, parse } from 'querystring'
import { RawSourceMap } from 'source-map'
import JSON5 from 'json5'
import yaml from 'js-yaml'
const loader: webpack.loader.Loader = function (
source: string | Buffer,
sourceMap: RawSourceMap | undefined
): void {
if (this.version && Number(this.version) >= 2) {
try {
......
this.callback(
null,
`export default ${generateCode(source, parse(this.resourceQuery))}`,
sourceMap
)
} catch (err) {
......
}
} else {
......
}
}
function generateCode(source: string | Buffer, query: ParsedUrlQuery): string {
const data = convert(source, query.lang as string)
let value = JSON.parse(data)
if (query.locale && typeof query.locale === 'string') {
value = Object.assign({}, { [query.locale]: value })
}
value = JSON.stringify(value)
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029')
.replace(/\\/g, '\\\\')
let code = ''
code += `function (Component) {
Component.__i18n = Component.__i18n || []
Component.__i18n.push('${value.replace(/\u0027/g, '\\u0027')}')
}\n`
return code
}
function convert(source: string | Buffer, lang: string): string {
const value = Buffer.isBuffer(source) ? source.toString() : source
switch (lang) {
case 'yaml':
case 'yml':
const data = yaml.safeLoad(value)
return JSON.stringify(data, undefined, '\t')
case 'json5':
return JSON.stringify(JSON5.parse(value))
default:
return value
}
}
export default loader
- 1.2.3
延遲加載
參考延遲加載翻譯,下面內(nèi)容是原文爷肝。
一次加載所有翻譯文件是過度和不必要的猾浦。
使用 Webpack 時陆错,延遲加載或異步加載轉(zhuǎn)換文件非常簡單灯抛。
讓我們假設我們有一個類似于下面的項目目錄
our-cool-project -dist -src --routes --store --setup ---i18n-setup.js --lang ---en.js ---it.js
lang
文件夾是我們所有翻譯文件所在的位置金赦。setup
文件夾是我們的任意設置> 的文件,如 i18n-setup对嚼,全局組件 inits夹抗,插件 inits 和其他位置。//i18n-setup.js import Vue from 'vue' import VueI18n from 'vue-i18n' import messages from '@/lang/en' import axios from 'axios' Vue.use(VueI18n) export const i18n = new VueI18n({ locale: 'en', // 設置語言環(huán)境 fallbackLocale: 'en', messages // 設置語言環(huán)境信息 }) const loadedLanguages = ['en'] // 我們的預裝默認語言 function setI18nLanguage (lang) { i18n.locale = lang axios.defaults.headers.common['Accept-Language'] = lang document.querySelector('html').setAttribute('lang', lang) return lang } export function loadLanguageAsync (lang) { if (i18n.locale !== lang) { if (!loadedLanguages.includes(lang)) { return import(/* webpackChunkName: "lang-[request]" */ `@/lang/${lang}`).then(msgs => { i18n.setLocaleMessage(lang, msgs.default) loadedLanguages.push(lang) return setI18nLanguage(lang) }) } return Promise.resolve(setI18nLanguage(lang)) } return Promise.resolve(lang) }
簡而言之纵竖,我們正在創(chuàng)建一個新的 VueI18n 實例漠烧。然后我們創(chuàng)建一個
loadedLanguages
數(shù)組,它將跟蹤我們加載的語言靡砌。接下來是setI18nLanguage
函數(shù)已脓,它將實際更改 vueI18n 實例、axios 以及其它需要本地化的地方通殃。
loadLanguageAsync
是實際用于更改語言的函數(shù)度液。加載新文件是通過import功能完成的,import
功能由 Webpack 慷慨提供画舌,它允許我們動態(tài)加載文件堕担,并且因為它使用 promise,我們可以輕松地等待加載完成曲聂。你可以在 Webpack 文檔 中了解有關導入功能的更多信息霹购。
使用
loadLanguageAsync
函數(shù)很簡單泉手。一個常見的用例是在 vue-router beforeEach 鉤子里面沸呐。router.beforeEach((to, from, next) => { const lang = to.params.lang loadLanguageAsync(lang).then(() => next()) })
我們可以通過檢查
lang
實際上是否支持來改進這一點徊件,調(diào)用reject
這樣我們就可以在 beforeEach 捕獲路由轉(zhuǎn)換倒庵。
核心方法是loadLanguageAsync
榛做,而loadLanguageAsync
的核心是import方法桐款,import實現(xiàn)動態(tài)加載的原理可以參考webpack中import實現(xiàn)過程阳柔,本質(zhì)上是在html中動態(tài)生成script標簽没酣。
二轻专、element-ui默認國際化方案
如上圖所示忆矛,使用element-ui中
el-select
組件的遠程搜索功能,當無匹配數(shù)據(jù)時请垛,默認文本為“無數(shù)據(jù)”催训,深入packages/select/src/select.vue中,發(fā)現(xiàn)來自于this.t('el.select.noMatch')宗收,本質(zhì)是來自于src/locale/lang/zh-CN.js:packages/select/src/select.vue
部分代碼:
emptyText() {
if (this.loading) {
......
} else {
......
if (this.filterable && this.query && this.options.length > 0 && this.filteredOptionsCount === 0) {
return this.noMatchText || this.t('el.select.noMatch');
}
.......
}
elementUI處理國際化的代碼在src/locale下:
2.1
locale/lang目錄
該目錄下漫拭,主要一些語言包文件,中文語言包對應
locale/lang/zh-CN.js
:2.2 代碼邏輯
2.2.1. ui組件中引入src/mixins/locale.js混稽,獲取到
t
方法采驻,在相應的位置調(diào)用t
方法(如select.vue中this.t('el.select.noMatch')
):
import { t } from 'element-ui/src/locale';
export default {
methods: {
t(...args) {
return t.apply(this, args);
}
}
};
2.2.2 src/mixins/locale.js中引入的是element-ui/src/locale/index.js审胚,該文件邏輯如下:
a. 對外暴露use
, t
, i18n
三個方法,t
方法上一步用到礼旅,use
和i18n
主要暴露給src/index.js
(對外提供install插件方法膳叨,見ElementUI的結(jié)構(gòu)與源碼研究
),用于全局設置語言種類和處理方法(默認會調(diào)用自身提供的i18nHandler
)痘系;
b. use
export const use = function(l) {
lang = l || lang; // 默認是中文
};
在項目中使用方法:
import lang from 'element-ui/lib/locale/lang/en'
import locale from 'element-ui/lib/locale'
// 設置語言
locale.use(lang)
c. i18n
和i18nHandler
菲嘴,看源碼,有vuei18n
和$t
汰翠,很明顯是用來兼容類似vue-i18n的國際化方案龄坪,見本文第一部分;
let i18nHandler = function() {
const vuei18n = Object.getPrototypeOf(this || Vue).$t;
if (typeof vuei18n === 'function' && !!Vue.locale) {
if (!merged) {
merged = true;
Vue.locale(
Vue.config.lang,
deepmerge(lang, Vue.locale(Vue.config.lang) || {}, { clone: true })
);
}
return vuei18n.apply(this, arguments);
}
};
d.t
方法
export const t = function(path, options) {
// 如果項目中使用了`vuei18n `方案复唤,那么國際化就直接被它接管
let value = i18nHandler.apply(this, arguments);
if (value !== null && value !== undefined) return value;
// 自身處理邏輯
const array = path.split('.');
let current = lang;
for (let i = 0, j = array.length; i < j; i++) {
const property = array[i];
value = current[property];
if (i === j - 1) return format(value, options);
if (!value) return '';
current = value;
}
return '';
};
如上健田,如果項目中使用了vuei18n
方案,那么國際化就直接被它接管佛纫;否認進入后面的邏輯妓局。
在前文中,我們講到雳旅,使用t
的方式如下:this.t('el.select.noMatch')
所以核心邏輯就兩點:
a. 將字符串el.select.noMatch
按“.”分割形成數(shù)組并遍歷跟磨,然后依次去zh-CN.js的返回結(jié)果中取得current.el,current.el.select和curren.select.noMatch值攒盈,得到值為“無匹配數(shù)據(jù)”抵拘,。
b. 支持format型豁,以el-pagination
組件為例僵蛛,可以顯示共有多少條數(shù),如
在源碼packages/pagination/src/pagination.js中有:
this.t('el.pagination.total', { total: this.$parent.total })
(其中this.$parent.total就是1000)對應的src/locale/lang/zh-CN.js中有:
{
el: {
pagination: {
total: '共 {total} 條'
}
}
}
對于這種情形迎变,t
方法充尉,簡化如下:
var RE_NARGS = /(%|)\{([0-9a-zA-Z_]+)\}/g;
function hasOwn(obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key);
}
function format() {
return function template(string, args) {
return string.replace(RE_NARGS, (match, prefix, i, index) => {
let result;
if (string[index - 1] === '{' &&
string[index + match.length] === '}') {
return i;
} else {
result = hasOwn(args, i) ? args[i] : null;
if (result === null || result === undefined) {
return '';
}
return result;
}
})
}
}
function t(string, args) {
return format()(string, args)
}
var test = t('共 {total} 條', { total: 1000 })
console.log(test)
執(zhí)行一下,最后的結(jié)果就是共 1000 條
衣形。
推薦
ElementUI的結(jié)構(gòu)與源碼研究
elementUI——mixins
elementUI——directives:mousewheel & repeat-click
elementU——transitions
elementUI——主題