Note: This article is based on [email protected], see element for source code. Common internationalization schemes are:

ECMAscript Intl: See Front-end internationalization and Intl

angular-translate

react-intl

vue-i18n

Before looking at elementUI’s internationalization scheme, let’s talk about VUE-i18n.

A,vue-i18n

Vue-i18n is a common internationalization solution. Here are a few key points. 1.1 code demo // step1: install vue-i18 plug-in in project

cnpm install vue-i18n --save-dev
Copy the code

// step2: introduce vue-i18n in the project entry file main.js

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'}}) /* eslint-disable no-new */ new vue ({el:'#app', 
 i18n, 
 router, 
 template: '<Layout/>', 
 components: { 
  Layout 
 }, 
})
Copy the code

// step3: use in the page

// zh.js
module.exports = { 
 menu : { 
   home:"Home page"
 }, 
 content:{ 
   main:"Here's the content."
 } 
}
// en.js
module.exports = { 
 menu : { 
   home:"home"
 }, 
 content:{ 
   main:"this is content"}} // business code <div class="title"> {{$t('menu.home')}}</div>
<input :placeholder="$t('content.main')" type="text"> // Render the result (apply en.js) <div class="title"< div> <input placeholder="Here's the content." type="text">
Copy the code

1.2 Features support complex, date-time localization, digital localization, linking, fallback (default language), component-based localization, custom instruction localization, component interpolation, single-file component, hot overloading, language change and lazy loading. A wide range of functions, this is mainly about a single file component, component-based localization, custom instructions and lazy loading three pieces.

  • 1.2.1 $i18nand$t vue-i18nAn instance of vUE is generated internally by the initialization method of_vm, such as1.1 Code demonstration - Step2Locale and messages from the VueI18n instance are injected into the vue instance, as shown in:
_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
    })
Copy the code

1.2.1.1 VuE-i8N install method

export functioninstall (_Vue) { ...... Extend (Vue) // Attach some common methods or properties to Vue. Prototype, for example$i18n,$t,$tcand$dVue. Mixin (mixin) // Inject i18n attributes into each Vue example, etc. Vue.directive('t', { bind, update, unbind}) / / global directives, called v - t Vue.com ponent (interpolationComponent. Name, interpolationComponent) / / global components, I18n Vue.component(numberComponent. Name, numberComponent) // global component, I18n-n // Use simple mergeStrategies to prevent I18n instance lose'__proto__'Const strats = Vue. Config. OptionMergeStrategies / / define a merge strategy strats i18n =function (parentVal, childVal) {
    return childVal === undefined
      ? parentVal
      : childVal
  }
}
Copy the code

1.2.1.2 extend(Vue) : Mount some common methods/attributes like $i18n, $t, $TC and $d on vue. prototype

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
    returni18n._t(key, i18n.locale, i18n._getMessages(), this, ... values) } ......Copy the code

1.2.1.3 vue. mixin(mixin) : Globally mix beforeCreate, beforeMount and beforeDestroy methods, make each Vue sample inject i18N attributes, add _I18N attributes to each Vue component

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 {
            letLocaleMessages = {} // options.__i18n is the contents of the < i18N ></i18n> tag in the vue component of a single file. Options. __i18n.forEach(resource => {localeMessages =  merge(localeMessages, JSON.parse(resource)) }) Object.keys(localeMessages).forEach((locale: Locale) = > {/ * mergeLocaleMessage, is i18n tag in the component data merged into _vm instance messages enclosing _vm.$set(this._vm.messages, locale, merge({}, this._vm.messages[locale] || {}, message)) */ options.i18n.mergeLocaleMessage(locale, localeMessages[locale]) }) } catch (e) {...... } this._i18nwatcher = this._i18n.watchi18nData ()} this._i18nwatcher = this._i18n.watchi18ndata ()}else if(isPlainObject(options.i18n)) {// i18n is a normal object, not an instance of VueI18n // componentlocalI18n // inject Vue. Prototype at extend(Vue)$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) { ...... // Add the current vue instances to the global _dataListeners array, and when the Watch method is notified, loop over these instances and call$forceUpdateMethod to update this. _i18n. SubscribeDataChanging (this) enclosing _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}},Copy the code

1.2.1.4 Update Mechanism: In Vue. Mixin (mixin), there is this._i18nwatcher = this._i18N.watchi18ndata (), which notifies Vue instances of updates, as well as the watchLocale method.

WatchI18nData (): Function {const self = this //$i18nand$tSection ', the global _VM instance's data property, which holds locale and messages informationreturn this._vm.$watch('$data', () = > {letI = self._datalisteners. Length // _dataListeners saves each vUE instancewhile (i--) {
        Vue.nextTick(() => {
          self._dataListeners[i] && self._dataListeners[i].$forceUpdate() // force update})}}, {deep:true})}Copy the code

$i18n: $i18n: $i18n: $i18n: $i18n: $i18n: $i18n: $i18n

  • 1.2.2 Single-file Components

The following example code allows you to manage internationalization within a component.

<i18n>
{
  "en": {
    "hello": "hello world!"
  },
  "ja": {
    "hello": "Kohiko: The world!"
  }
}
</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>
Copy the code

Webpack configuration (for VUe-Loader V15 or later) :

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
      },
      {
        resourceQuery: /blockType=i18n/,
        type: 'javascript/auto',
        loader: '@kazupon/vue-i18n-loader'} / /... ] } / /... }Copy the code

Vue i18n-loader is used to parse the custom tag
in vue single file.

  1. i18nThe contents of the tag can beyamlFormat, can also be JSON (5) or general text format, this is mainly throughconvertMethod processing;
  2. GenerateCode is mainly used to parse vUE single file componentsi18nTags (refer toVue custom block, the tag content is saved in__i18nArray) and some special characters (such as \u2028, \u2029, and \u0027, seeSpecial characters commonly encountered in 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
Copy the code
  • 1.2.3 Lazy loadingreferenceLazy loading translationThe following is the original text.

Loading all translation files at once is excessive and unnecessary.

With Webpack, lazy loading or asynchronous loading of transformation files is very simple.

Let’s assume we have a project directory similar to the following

our-cool-project
-dist
-src
--routes
--store
--setup
---i18n-setup.js
--lang
---en.js
---it.js

Copy the code

The lang folder is where all our translation files are located. The setup folder is our arbitrary Settings > file, such as I18N-setup, global component inits, plug-in inits and other locations.

//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'// Set the locale to fallbackLocale:'en'Const loadedLanguages = [const loadedLanguages = ['en'] // Our pre-installed default languagefunction 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)
}

Copy the code

In short, we are creating a new VueI18n instance. Then we create a loadedLanguages array that will keep track of the language we loaded. Next comes the setI18nLanguage function, which actually changes the vueI18n instance, AXIOS, and whatever else needs to be localized.

LoadLanguageAsync is the actual function used to change the language. Loading a new file is done through the import feature, generously provided by Webpack, which allows us to load the file dynamically, and because it uses Promise, we can easily wait for the load to complete.

You can learn more about the import functionality in the Webpack documentation.

Using the loadLanguageAsync function is simple. A common use case is in the VUe-router beforeEach hook.

router.beforeEach((to, from, next) => {
  const lang = to.params.lang
  loadLanguageAsync(lang).then(() => next())
})

Copy the code

We can improve on this by checking whether lang actually supports reject, calling Reject so that we can capture routing transformations in beforeEach.

The core method is loadLanguageAsync, and the core method of loadLanguageAsync is import method. The principle of import dynamic loading can be refer to the implementation process of Import in Webpack. Essentially, script tags are dynamically generated in HTML.

Element – UI default internationalization scheme

el-select
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'); }... }Copy the code

The locale/lang directory
locale/lang/zh-CN.js

t
t
this.t('el.select.noMatch')

import { t } from 'element-ui/src/locale';

exportdefault { methods: { t(... args) {returnt.apply(this, args); }}};Copy the code

Elemental-ui/SRC /locale/index.js is introduced in SRC /mixins/locale.js. The logic of the file is as follows: a. Use and i18n are exposed to SRC /index.js (see ElementUI for structure and source code). It is used to set the language category and the handling method globally (the default is to call its own supplied i18nHandler); b. use export const use = function(l) { lang = l || lang; // Default is Chinese}; Methods used in the project:

import lang from 'element-ui/lib/locale/lang/en'
import locale from 'element-ui/lib/locale'// Set the language locale.use(lang)Copy the code

C. I18n and i18nHandler, with vuei18n and $t, are obviously compatible with vuE-i18n internationalization schemes, as shown in the first part of this article;

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})); }returnvuei18n.apply(this, arguments); }};Copy the code

Which method

export const t = function(path, options) {// If the 'vuei18n' scheme is used in the project, then internationalization is taken over by itlet value = i18nHandler.apply(this, arguments);
  if(value ! == null && value ! == undefined)returnvalue; // self-processing logic 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 ' ';
};
Copy the code

As above, if the VUEI18N scheme is used in a project, internationalization is taken over directly by it; Denial goes into the logic behind it. T (‘ el.select.nomatch ‘); t(‘ select.nomatch ‘); Press “for the string el.select.nomatch.” Select and curren.select.noMatch from zh-cn. js. B. Format is supported. Taking the El-Pagination component as an example, the total number of entries can be displayed

this.t('el.pagination.total', { total: this.$parent.total })

{
  el: {
    pagination: {
      total: 'Total {total}'}}}Copy the code

For this case, method T is simplified as follows:

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 ' ';
            }

            returnresult; }}}})function t(string, args) {
    return format()(string, args)
}

var test = t('Total {total}', { total: 1000 })
console.log(test)
Copy the code

So if I do that, I’m going to end up with 1000 entries.

recommended

ElementUI structure and source code research

ElementUI — mixins

ElementUI — Cache: mousewheel & repeat-click

ElementU – transitions

Theme elementUI –