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:"這里是內(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ù)的更新操作,用法見自定義指令本地化

示例
代碼如下告喊,可以在組件內(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源碼秆吵,可以看出:

  1. i18n標簽內(nèi)的內(nèi)容可以是yaml格式淮椰,也可以是json(5)或一般文本格式,這塊主要是通過convert方法處理的纳寂;
  2. 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

一次加載所有翻譯文件是過度和不必要的猾浦。

使用 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默認國際化方案

select no match text

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

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方法采驻,在相應的位置調(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方法上一步用到礼旅,usei18n主要暴露給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. 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按“.”分割形成數(shù)組并遍歷跟磨,然后依次去zh-CN.js的返回結(jié)果中取得current.el,current.el.select和curren.select.noMatch值攒盈,得到值為“無匹配數(shù)據(jù)”抵拘,。
b. 支持format型豁,以el-pagination組件為例僵蛛,可以顯示共有多少條數(shù),如

條數(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——主題

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末驼侠,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子谆吴,更是在濱河造成了極大的恐慌倒源,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件句狼,死亡現(xiàn)場離奇詭異笋熬,居然都是意外死亡,警方通過查閱死者的電腦和手機腻菇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門胳螟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來昔馋,“玉大人,你說我怎么就攤上這事糖耸∶囟簦” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵蔬捷,是天一觀的道長垄提。 經(jīng)常有香客問我榔袋,道長周拐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任凰兑,我火速辦了婚禮妥粟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘吏够。我一直安慰自己勾给,他們只是感情好,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布锅知。 她就那樣靜靜地躺著播急,像睡著了一般。 火紅的嫁衣襯著肌膚如雪售睹。 梳的紋絲不亂的頭發(fā)上桩警,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音昌妹,去河邊找鬼捶枢。 笑死,一個胖子當著我的面吹牛飞崖,可吹牛的內(nèi)容都是我干的烂叔。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼固歪,長吁一口氣:“原來是場噩夢啊……” “哼蒜鸡!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起牢裳,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤逢防,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后贰健,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體胞四,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年伶椿,在試婚紗的時候發(fā)現(xiàn)自己被綠了辜伟。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片氓侧。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖导狡,靈堂內(nèi)的尸體忽然破棺而出约巷,到底是詐尸還是另有隱情,我是刑警寧澤旱捧,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布独郎,位于F島的核電站,受9級特大地震影響枚赡,放射性物質(zhì)發(fā)生泄漏氓癌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一贫橙、第九天 我趴在偏房一處隱蔽的房頂上張望贪婉。 院中可真熱鬧,春花似錦卢肃、人聲如沸疲迂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽尤蒿。三九已至,卻和暖如春幅垮,著一層夾襖步出監(jiān)牢的瞬間腰池,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工军洼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留巩螃,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓匕争,卻偏偏與公主長得像避乏,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子甘桑,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容