Element UI 组件源码分析之国际化

2,330 阅读5分钟

前言

本文将系统讲解 Element UI 国际化方案实现,项目内置了51门语言。src/locale 目录下存放了语言配置文件、国际化处理方法、文本格式等,下面将逐一讲解,耐心读完,相信会对您有所帮助。

更多组件分析详见 👉 📚 Element UI 源码剖析组件总览

本专栏的 gitbook 版本地址已经发布 anduril.gitbook.io/learning-el… ,内容同步更新中!

国际化方案

element 的国际化方案没有引入第三方插件如 vue-i18n,通过自定义实现的。它兼容 vue-i18n,搭配使用能十分方便。

📁 src/locale/lang

用于存放支持语言的配置文件,默认语言为简体中文语言(zh-CN),src/locale/lang/zh-CN.js文件返回一个 JSON 格式的数据,内容为各组件相关描述翻译。

export default {
  el: {
    // 组件 select 选择器
    select: {
      loading: '加载中',
      noMatch: '无匹配数据',
      noData: '无数据',
      placeholder: '请选择'
    },
    // ...
  }
}

若需要使用其他的语言,在该目录下添加一个语言配置文件即可。

📃 src/locale/format.js

提供字符串模板函数,支持索引或命名关键字参数。 代码实现参考 npm/string-template

// 核心方法

/**
 * 字符串模板函数,支持索引或命名关键字参数 
 * @param {String} string 模板字符串
 * @param {Array} ...args 参数
 * @return {String}  格式化文本
 *
 */
function template(string, ...args) {
  const RE_NARGS = /(%|)\{([0-9a-zA-Z_]+)\}/g;
  if (args.length === 1 && typeof args[0] === "object") {
    args = args[0]; 
  }

  if (!args || !args.hasOwnProperty) {
    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;
    }
  });
}

📃 src/locale/index.js

index.js提供三个方法 useti18n 用于语言切换设置、国际化处理、i18n插件兼容以及自定义。详细内容请看注释源码。

use 方法参数为语言配置内容,用于语言切换。若参数为空,默认使用简体中文(zh-CN)。

import defaultLang from 'element-ui/src/locale/lang/zh-CN'; 
let lang = defaultLang; // 默认语言
 
// 语言切换
export const use = function(l) {
  lang = l || lang;
};
 

i18n 方法用于外部插件方法的兼容和自定义处理方法引入。项目 i18nHandler方法的实现就是用来兼容外部插件vue-i18n@5.x

若传入自定义处理方法, i18nHandler方法实现将被覆盖。


// i18nHandler 是一个 i18n 的处理方法,这块逻辑就是用来兼容外部的 i18n 方案如 vue-i18n@5.x。
let i18nHandler = function() {
  // 若引入插件 vue-i18n@5.x,会在 Vue 的原型上挂载 $t 方法。
  // i18nHandler 默认会尝试去找 Vue 原型中的 $t 函数,
  const vuei18n = Object.getPrototypeOf(this || Vue).$t;
  // 方法vuei18n存在
  if (typeof vuei18n === 'function' && !!Vue.locale) {
    // 合并语言配置内容
    if (!merged) {
      merged = true;
      Vue.locale(
        Vue.config.lang,
        deepmerge(lang, Vue.locale(Vue.config.lang) || {}, { clone: true })
      );
    }
    // 使用vuei18n进行国际化处理
    return vuei18n.apply(this, arguments);
  }
}; 
 
// i18n 的处理方法(支持引入自定义方法)
export const i18n = function(fn) {
  i18nHandler = fn || i18nHandler;
}; 

t 方法用于组件国际化处理,它根据传入的 path 路径,从语言配置中找到对应的文案,然后在结合 options进行格式化处理。

// 默认语言 中文简体
import defaultLang from 'element-ui/src/locale/lang/zh-CN'; 
// 字符串模板函数
import Format from './format';

// 等同于 template() 方法
const format = Format(Vue);
let lang = defaultLang; // 默认语言 

let i18nHandler = function() {
    // ...
}

// 国际化处理方法
export const t = function(path, options) { 

  // 用来兼容外部的 i18n 方案如 vue-i18n@5.x 或自定义方法
  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];
    // console.log(property, value);
    if (i === j - 1) {
      // 格式化内容
      // value 获取翻译内容   options 组件传入参数 
      return format(value, options);
    }
    if (!value) return '';
    current = value;
  }
  return '';
};

组件国际化引用

接下来通过SelectPagination组件的示例讲解下国际化功能引入使用。

功能引入

前文可知 src/mixins/locale.js 引入src/locale/index.js中的 t 方法,通过 mixin 方式提供给组件页面使用。

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

export default {
  methods: {
    t(...args) {
      return t.apply(this, args);
    }
  }
};

Select组件源码中可以看到国际化处理逻辑。

import Locale from 'element-ui/src/mixins/locale'; 

export default {
    mixins: [Locale], 
    name: 'ElSelect', 
    componentName: 'ElSelect',  
    computed: {  
      emptyText() {
        if (this.options.length === 0) {
           return this.noDataText || this.t('el.select.noData');
        } 
        // ...
      }, 
    },
    // ...
} 

t(path)

this.t('el.select.noData') 调用 t 方法据传入的 path 路径值 el.select.noData,参数optionsundefined ,默认情况下没有引入第三方插件或自定义方法,直接从默认语言(简体中文)配置中找到对应的文案 。

此时 value 值为 “无数据”,参数options 为 “undefined”,使用 format 方法(即 src/locale/format.js定义的 template 方法 )进行文本格式化。

format('无数据', undefined)  =>  '无数据'

组件效果如下 👇。

image.png

t(path, options)

Pagination分页组件中, this.t('el.pagination.total', { total: this.$parent.total }) 传入path 路径值 el.pagination.total,参数 options{total: 1000}

carbon (1).png

根据 el.pagination.total 获取值为 共 {total} 条,是一个模板字符串。

{
 el: {
   pagination: {
     goto: "前往",
     pagesize: "条/页",
     total: "共 {total} 条",
     pageClassifier: "页",
   },
   // ...
 }
}

此时 value 值为 共 {total} 条,参数options{total: 1000}, 使用 format 方法进行文本格式化。

format('共 {total} 条', {total: 1000})  =>  '共 1000 条'

不是所有的值都是纯文本,存在模板字符串。引入src/locale/format.js定义的 format 方法用于字符串格式化操作。

组件效果如下 👇。

image.png

项目国际化设置

通过上述篇幅详细讨论组件内部国际化方案实现。接下来介绍组件的使用中如何使用设置多语言,引入第三方插件、手动扩展方法等操作。

完整引入

使用 Vue.use()全局注册组件时,也可以传入一个可选的选项对象,locale 用于语言设置、i18n 自定义i18n 的处理方法 。

import Vue from 'vue'; 
import ElementUI from 'element-ui'; 
import lang from 'element-ui/lib/locale/lang/en';

import "element-ui/lib/theme-chalk/index.css"; 

Vue.use(ElementUI, {
  locale: lang,
  i18n: function(path, options) {
    // ...
  }
}); 

src/index.js 中的 install 方法中提供 locale.use()locale.i18n() ,接收参数 opts,用于国际化全局设置。

import locale from 'element-ui/src/locale'; 
// ...

const install = function(Vue, opts = {}) {
  locale.use(opts.locale);
  locale.i18n(opts.i18n); 
  // ...
}; 

按需引入

引入部分组件比如 Button 和Select,可以使用两种方式进行注册:插件注册 Vue.use() 或 组件全局注册Vue.component();引入 locale 方法设置语言;引入 i18n 方法用于处理方法;。

import Vue from 'vue'; 
import { Button, Select, locale, i18n } from "element-ui";
import lang from 'element-ui/lib/locale/lang/en' 
// import locale from 'element-ui/lib/locale' 

import "element-ui/lib/theme-chalk/index.css";

// 设置语言  等同于 locale.use()
locale(lang)

// 设置i18n处理方法  等同于  locale.i18n()
i18n(function(path, options) {
  // ...
});

// 引入组件 
Vue.use(Button);
Vue.component(Select.name, Select) 

src/index.js 中导出的 localei18n方法。 locale 等同于locale.usei18n 等同于locale.i18n

import locale from 'element-ui/src/locale'; 
// ...

const install = function(Vue, opts = {}) { 
  // ...
}; 

export default {
  locale: locale.use,
  i18n: locale.i18n,
  install,
  Button,
  Select,
  // ...
};

搭配vue-i18n使用

项目也可引入第三方插件,如vue-i18n,最新版本为 8.x ,使用需要手动处理 (Element 兼容 5.x) 。

import Vue from "vue"; 
import Element from "element-ui";
import VueI18n from "vue-i18n";
import enLocale from "element-ui/lib/locale/lang/en";
import zhLocale from "element-ui/lib/locale/lang/zh-CN"; 

Vue.use(VueI18n);

const messages = {
  en: {
    message: {
      hello: "hello world",
    },
    ...enLocale, // 或者用 Object.assign({ message: {hello: "hello world"}} }, enLocale)
  },
  zh: {
    message: {
      hello: "你好世界",
    },
    ...zhLocale, // 或者用 Object.assign({ message: {hello: "你好世界"} }, zhLocale)
  },
};
 

// 创建 VueI18n 实例
const i18n = new VueI18n({
  locale: "zh", // 设置语言
  messages, // 语言翻译内容
});

//注册Element  自定义i18n 的处理方法
Vue.use(Element, {
  i18n: (key, value) => i18n.t(key, value),
});

new Vue({
  i18n,
  render: (h) => h(App),
}).$mount("#app");

测试页面

<template>
  <div id="app">
    <div>message.hello -- {{ $t("message.hello") }}</div>
    <div>el.select.noData -- {{ $t("el.select.noData") }}</div>
  </div>
</template>

<script>
export default {
  name: "App",
};
</script>

设置语言为zh, 页面输出内容:

image.png

设置语言为en, 页面输出内容:

image.png

关注专栏

如果本文对您有所帮助请关注➕、 点赞👍、 收藏⭐!您的认可就是对我的最大支持!

此文章已收录到专栏中 👇,可以直接关注。