说明:本文基于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:"这里是内容"
}
}
// en.js
module.exports = {
menu : {
home:"home"
},
content:{
main:"this is content"
}
}
// 业务代码
<div class="title">{{$t('menu.home')}}</div>
<input :placeholder="$t('content.main')" type="text">
// 渲染结果(应用zh.js)
<div class="title">首页</div>
<input placeholder="这里是内容" type="text">
1.2 功能
支持复数、日期时间本地化、数字本地化、链接、回退(默认语言)、基于组件本地化、自定义指令本地化、组件插值、单文件组件、热重载、语言变更及延迟加载。
功能繁多,在此主要讲一下单文件组件
、基于组件的本地化
、自定义指令
和延迟加载
三块。
- 1.2.1
$i18n
和$t
vue-i18n
的初始化方法内部会生成一个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上挂载一些常用方法或属性,如$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上挂载一些常用方法或属性,如$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>标签里的内容
options.__i18n.forEach(resource => {
localeMessages = merge(localeMessages, JSON.parse(resource))
})
Object.keys(localeMessages).forEach((locale: Locale) => {
/*
mergeLocaleMessage ,就是把组件里i18n标签的数据合并到_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的作用见下一小节
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数组中,当有watch方法通知时,遍历这些实例,并调用$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 更新机制
:在上一节Vue.mixin(mixin)
中,有this._i18nWatcher = this._i18n.watchI18nData()
,其作用就是通知各vue实例更新,类似的还有watchLocale
方法(监控locale变化)
watchI18nData (): Function {
const self = this
// 在`1.2 功能 $i18n和$t节`中,全局_vm实例的data属性,保存有locale和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做一些数据的更新操作,用法见自定义指令本地化
- 1.2.2
单文件组件
示例
代码如下,可以在组件内管理国际化。
<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>
这种自定义标签,根据下面的loader源码,可以看出:
-
i18n
标签内的内容可以是yaml
格式,也可以是json(5)或一般文本格式,这块主要是通过convert
方法处理的; - generateCode主要用来解析vue单文件组件内
i18n
标签(可以参考vue 自定义块,标签内容被保存在__i18n
数组内 )和一些特殊字符(如\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
延迟加载
参考延迟加载翻译,下面内容是原文。
一次加载所有翻译文件是过度和不必要的。
使用 Webpack 时,延迟加载或异步加载转换文件非常简单。
让我们假设我们有一个类似于下面的项目目录
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', // 设置语言环境 fallbackLocale: 'en', messages // 设置语言环境信息 }) 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) }
简而言之,我们正在创建一个新的 VueI18n 实例。然后我们创建一个
loadedLanguages
数组,它将跟踪我们加载的语言。接下来是setI18nLanguage
函数,它将实际更改 vueI18n 实例、axios 以及其它需要本地化的地方。
loadLanguageAsync
是实际用于更改语言的函数。加载新文件是通过import功能完成的,import
功能由 Webpack 慷慨提供,它允许我们动态加载文件,并且因为它使用 promise,我们可以轻松地等待加载完成。你可以在 Webpack 文档 中了解有关导入功能的更多信息。
使用
loadLanguageAsync
函数很简单。一个常见的用例是在 vue-router beforeEach 钩子里面。router.beforeEach((to, from, next) => { const lang = to.params.lang loadLanguageAsync(lang).then(() => next()) })
我们可以通过检查
lang
实际上是否支持来改进这一点,调用reject
这样我们就可以在 beforeEach 捕获路由转换。
核心方法是loadLanguageAsync
,而loadLanguageAsync
的核心是import方法,import实现动态加载的原理可以参考webpack中import实现过程,本质上是在html中动态生成script标签。
二、element-ui默认国际化方案
如上图所示,使用element-ui中
el-select
组件的远程搜索功能,当无匹配数据时,默认文本为“无数据”,深入packages/select/src/select.vue中,发现来自于this.t('el.select.noMatch'),本质是来自于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
方法,在相应的位置调用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的结构与源码研究
),用于全局设置语言种类和处理方法(默认会调用自身提供的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
按“.”分割形成数组并遍历,然后依次去zh-CN.js的返回结果中取得current.el,current.el.select和curren.select.noMatch值,得到值为“无匹配数据”,。
b. 支持format,以el-pagination
组件为例,可以显示共有多少条数,如
在源码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)
执行一下,最后的结果就是共 1000 条
。
推荐
ElementUI的结构与源码研究
elementUI——mixins
elementUI——directives:mousewheel & repeat-click
elementU——transitions
elementUI——主题