不同的组件库如何支持多语言

957 阅读4分钟

国际化功能对于组件库还是很有必要的,主题、多地区等都是类似的功能。于是对几个使用量大的组件库进行研究。

共同点

不同组件库提供的功能以及对语言包的处理都基本一致的。

功能

  • 切换语言
  • 导入自定义语言包
  • 覆盖内置语言包文案(Vant 4)
  • 支持定制翻译函数(ElementUI, vuetify 3)
  • 支持翻译使用变量(ElementUI, vuetify 3)

对语言包的处理

由于组件的内建文案不多,内置的语言包会被存放在项目中,并随着项目一起发布。

考虑到生产环境按需加载,不同的翻译为不同的语言包,存在独立的文件中。语言包导出一个扁平化的 js/json 对象。

组件只会从指定的语言包下获取翻译。

实现差异

对于不同的框架,实现原理会有所不同,不同组件库之间的实现方式也会不同。

基于 Vue 的国际化方案

研究的组件库包括 ElementUI、vuetify 3、Vant 4

它们都使用了变量保存当前语言,翻译函数t内部根据该变量返回对应 key 的翻译。但是对于翻译函数t的使用方式会有差异。

ElementUI

实现方式:Vue2 + 语言包变量 + mixins

选择语言

不管是全量引入还是按需引入,最终都会调用 locale.use(),将内置或者自定义的语言包保存在变量lang中,

// locale/index.js
import defaultLang from 'element-ui/src/locale/lang/zh-CN';

let lang = defaultLang; // 保存当前使用的语言包
export const use = function(l) {
  lang = l || lang;
};

组件使用翻译函数

组件内部通过 Mixin 的方式注入t

// locale/index.js
export { use, t, i18n }


// select.vue
  import Locale from 'element-ui/src/mixins/locale';
  
  export default {
      mixins: [Locale, ...]
  }
搭配其它的 i18n 插件

ElementUI 还支持与其它 i18n 插件或者自定义方式搭配使用,翻译函数t中会优先使用其它传入的方式来处理。

不管是哪种方式,都会从lang中查找翻译。

// locale/index.js

export const t = function(path, options) {
  // 优先自定义插件的翻译方式
  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 '';
};

vuetify 3

实现方式:Vue3 + 当前语言变量 + 语言包 Map 变量 + provide/inject

选择语言

在初始化组件库的时候,需要将可能用到的语言包通过 locale.message 传入,locale 表示使用的语言包名称。

const vuetify = createVuetify({
  locale: {
    locale: 'zhHans',
    fallback: 'sv',
    messages: { zhHans, pl, sv }
  }
})

app.use(vuetify)
组件使用翻译函数

组件库在初始化的时候,根据传入的 locale 生成 Locale 对象,通过 provide 保存在名为 LocaleSymbol的依赖中。

Locale 对象可以通过useLocale获得。

  • Locale.current;当前语言包名称
  • Locale.message: 所有的语言包

如果使用的是 Option API 则会通过 mixin 的方式注入到全局中。

export function createVuetify (vuetify: VuetifyOptions = {}) {
    ...
    const locale = createLocale(options.locale)
    app.provide(LocaleSymbol, locale)
    ...
    // 
     if (typeof __VUE_OPTIONS_API__ !== 'boolean' || __VUE_OPTIONS_API__) {
      app.mixin({
        computed: {
          $vuetify () {
            return reactive({
              ...
              locale: inject.call(this, LocaleSymbol),
            })
          },
        },
      })
    }
}

通过 inject 将 Locale 对象注入到需要的组件中

// locale.ts
export function useLocale () {
  const locale = inject(LocaleSymbol)

  if (!locale) throw new Error('[Vuetify] Could not find injected locale instance')

  return locale
}

// VSelect.tsx
import { useLocale } from '@/composables/locale'
...
const { t } = useLocale()
搭配其它的 i18n 插件

当使用其它多语言插件后,LocaleSymbol 为插件格式化后的对象,因此数据、方法都来自于插件。

import { createVueI18nAdapter } from 'vuetify/locale/adapters/vue-i18n'

const vuetify = createVuetify({
  locale: {
    adapter: createVueI18nAdapter({ i18n, useI18n })
  }
})

没有使用其它插件时,会在 locale.message 中查找 locale.locale 对应的语言包,再获取对应的翻译。

const LANG_PREFIX = '$vuetify.'
const createTranslateFunction = (
  current: Ref<string>,
  fallback: Ref<string>,
  messages: Ref<LocaleMessages>,
) => {
  return (key: string, ...params: unknown[]) => {
    ...
    const shortKey = key.replace(LANG_PREFIX, '')
    const currentLocale = current.value && messages.value[current.value]
    ...
    let str: string = getObjectValueByPath(currentLocale, shortKey, null)
    ...
    return replace(str, params)
  }
}

Vant 4

实现方式:Vue3 + 当前语言变量 + 语言包 Map 变量 + 函数导入

选择语言

Vant 4 的实现方式相对简洁很多,Locale.use 添加或更新语言包并设置为当前语言。

  • 变量lang保存当前语言包名称
  • 变量messages保存所有的语言包
import { Locale } from 'vant'; 
// 引入英文语言包 import enUS from 'vant/es/locale/lang/en-US'; 
Locale.use('en-US', enUS);

// locale/index.js
const lang = ref('zh-CN');
const messages = reactive<Messages>({
  'zh-CN': defaultMessages,
});

export const Locale = {
  messages(): Message {
    return messages[lang.value];
  },

  use(newLang: string, newMessages?: Message) {
    lang.value = newLang;
    this.add({ [newLang]: newMessages });
  },

  add(newMessages: Message = {}) {
    deepAssign(messages, newMessages);
  },
};
组件使用翻译函数

翻译函数并没有提供全局的使用方式,而是需要导入,因此也就不需要考虑是否使用 Option API 了。

支持命名空间,那么不同组件之间就可以定义相同的 key。

查找的过程同样是先根据lang查找对应的语言包,再从语言包中查找翻译。

// AddressEdit.ts
import { ..., createNamespace } from '../utils';
const [name, bem, t] = createNamespace('address-edit');

// create.ts
import locale from '../locale';

export function createNamespace(name: string) {
  const prefixedName = `van-${name}`;
  return [
    prefixedName,
    createBEM(prefixedName),
    createTranslate(prefixedName),
  ] as const;
}

export function createTranslate(name: string) {
  const prefix = camelize(name) + '.';

  return (path: string, ...args: unknown[]) => {
    const messages = locale.messages();
    // 查找翻译
    const message = get(messages, prefix + path) || get(messages, path);

    return isFunction(message) ? message(...args) : message;
  };
}

但是翻译不支持使用变量,也不能搭配其它插件使用。

基于 React 的国际化方案

React 的实现方式基本上都会利用 Context 注入语言包。

当应用切换语言后,如何自动更新组件

React 使用 Context,当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。从 Provider 到其内部 consumer 组件传播不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件跳过更新的情况下也能更新。

上述的几个 Vue 组件库内部都没有做自动更新的逻辑,需要手动的更新组件。