elementUI——locale,国际化方案

说明:本文基于[email protected],源码详见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: '', 
 components: { 
  Layout 
 }, 
})

// step3:页面中使用

// zh.js
module.exports = { 
 menu : { 
   home:"首页"
 }, 
 content:{ 
   main:"这里是内容"
 } 
}
// en.js
module.exports = { 
 menu : { 
   home:"home"
 }, 
 content:{ 
   main:"this is content"
 } 
}
// 业务代码
{{$t('menu.home')}}
// 渲染结果(应用zh.js)
首页

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组件中标签里的内容
            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 单文件组件

示例
代码如下,可以在组件内管理国际化。


{
  "en": {
    "hello": "hello world!"
  },
  "ja": {
    "hello": "こんにちは、世界!"
  }
}





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单文件这种自定义标签,根据下面的loader源码,可以看出:

  1. i18n标签内的内容可以是yaml格式,也可以是json(5)或一般文本格式,这块主要是通过convert方法处理的;
  2. 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默认国际化方案

select no match text

如上图所示,使用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');
          }
          .......
        }

zh-CN.js

elementUI处理国际化的代码在src/locale下:
locale

2.1 locale/lang目录
该目录下,主要一些语言包文件,中文语言包对应locale/lang/zh-CN.js
语言包

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方法上一步用到,usei18n主要暴露给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. i18ni18nHandler,看源码,有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——主题

你可能感兴趣的:(elementUI——locale,国际化方案)