elementUI——locale,国际化方案

说明:本文基于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做一些数据的更新操作,用法见自定义指令本地化

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

<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源码,可以看出:

  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

一次加载所有翻译文件是过度和不必要的。

使用 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——主题

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容