国际化功能对于组件库还是很有必要的,主题、多地区等都是类似的功能。于是对几个使用量大的组件库进行研究。
共同点
不同组件库提供的功能以及对语言包的处理都基本一致的。
功能
- 切换语言
- 导入自定义语言包
- 覆盖内置语言包文案(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 组件库内部都没有做自动更新的逻辑,需要手动的更新组件。