首先浅谈一下i18n我们要实现的功能
- 拥有一个控制全局语言的变量
- 根据指定key抓取语言包的value显示到View上
- 能对语言包中的变量,进行替换
- 能够动态写入语言包
有了这些目标我们就开始大胆实现。
首先我们简单写一个i18n的类(ts)
class i18n {
private locales: Ref; // 全局控制语言的变量
private message: Record<string, Record<string, unknown>>; // 语言包
private variable: string = "${#}"; // 声明返回数据中存在的 变量
constructor (options: Record<string, any>) {
// 初始化类中变量
this.locales = ref(options.locales)
this.message = options.message
}
}
现在我们先写一个简单的返回指定key的值
// t 函数 => 根据语言与key返回值
public t (key: string, args = ""): string | undefined {
// 这里的args是传递过来的替换变量的值 => 也就是我们将 this.variable 替换成 args
const _messgae = this.message[this.locales] // 根据语言返回当前语言包
// 替换数据中的变量
const _value = _messgae[key].replace(this.variable, args)
return _value
}
但是这个函数存在两个问题,
1、当_message是一个深层的对象,那么t函数将会返回一个[Object Object],
2、我们是假设args是一个字符串,遇到多个变量的时候就没办法处理掉了
所以我们需要改造一下, 第一点来说我们只需要递归获取深层的object即可,对于第二点来说,可能性就比较多了,我们假如args 支持传入 字符串、数组、object
假设翻译文件为 { hello: “My name is ${#} !”}
字符串: t(“hello”, “Leon”) 输出:My name is Leon !
数组: t(“hello”, [ “Leon” ]) 输出:My name is Leon !
对象翻译文件要与key对应了,比如翻译文件为 { hello: “My name is ${name} !”}
对象: t(“hello”, { name: “Leon” }) 输出:My name is Leon !
// t 函数 => 根据语言与key返回值
public t (key: string, args = ""): string | undefined {
// 根据语言返回当前语言包
const _messgae = this._deep(this.message[this.locales], key) // 抓取到值
// 判断 args 类型 来匹配变量
if (["string", "number"].includes(typeof args)) {
return this._insertStringVariable(v, args)
}
if (Array.isArray(args)) {
return this._insertArrayVariable(v, args)
}
if (typeof args === "object") {
return this._insertObjectVariables(v, args)
}
}
// 递归获取 深层object的value
private _deep (obj: Record<string, any>, keys: string): string | undefined {
if (!keys) {
return undefined;
}
const k = keys.split(".")
const v = obj?.[k[0]]
if (v == null) {
return undefined;
}
return k.length === 1 ? v : this._deep(v, k.slice(1).join(","))
}
// 写入字符串变量
private _insertStringVariable (value: string, variables: string): string {
return value.replace(this.variable, variables.toString());
}
// 递归写入数组变量值
private _insertArrayVariable (value: string, variables: Array<any>): string {
let v = value.replace(this.variable, variables[0])
return v.includes(this.variable) ? this._insertArrayVariable(v, variables.slice(1)) : v
}
// 写入对象变量值
private _insertObjectVariables(value: string, variables: Record<string, any>): string {
if (!variables) {
return value
}
return value.replace(/\$\{([\w\d_]+)\}/g, (match: string, k: string) => {
const variable = variables[k];
return variable === undefined ? match : variable.toString();
});
}
截止到这里最主要的t函数已经写完了,接下来我们要写,改变全局的语言变量locales、能够动态写入语言包、挂载到Vue上的install函数
import { ref, watchEffect } from 'vue';
import type { Ref, DirectiveBinding, App } from 'vue';
class I18n {
private locales: Ref;
private message: Record<string, Record<string, unknown>>;
private variable: string = "${#}";
constructor (options: Record<string, any>) {
this.locales = ref(options.locales)
this.message = options.message
this.variable = options?.variable || "${#}"
}
// 返回 翻译文字
public t (key: string, args = ""): string | undefined {
return this._t(key, args)
}
// 设置 语言
public setLocales (locales: string): void {
if (!locales) {
console.error("[i18n]语言不可为空值!");
return;
}
if (this.locales.value === locales) {
console.warn(`当前语言已是[${locales}], 无需改变!`)
return
}
this.locales.value = locales
}
// 插入 语言包
public setLanguagePack (locales: string, pack: Record<string, unknown>): void {
const _pack = this.message[locales] || {}
this.message[locales] = Object.assign(_pack, pack)
}
// 初始化函数
public install (Vue: App) {
if (!Vue) {
console.error("[I18n]未获取到Vue实例!");
return;
}
var version = (Vue.version && Number(Vue.version.split('.')[0])) || -1;
if (version != 3) {
console.error("[I18n]仅支持Vue 3.x版本!");
return;
}
const i18n = new I18n({
locales: this.locales,
message: this.message,
})
Vue.config.globalProperties.$i18n = i18n
Vue.config.globalProperties.$t = (key: string, args = "") => {
return i18n.t(key, args)
}
Vue.directive('t', {
mounted: function (el: HTMLImageElement, binding: DirectiveBinding): void {
// 监听 locales 的变化,重新绑定指令
watchEffect(() => {
const _v = i18n.t(binding.value, i18n.locales.value) || "";
el.innerHTML = _v;
});
},
unmounted: function (el: HTMLImageElement, binding: DirectiveBinding): void {
el.innerText = ""
}
})
}
// 处理 t
private _t (key: string, args = "") {
if (typeof key !== "string") {
console.warn(`[i18n.t]需要传入一个String的参数,而非[${typeof key}]`);
return key;
}
if (Object.keys(this.message).length == 0) {
console.warn(`[i18n]未注册任何语言包`);
return key;
}
if (!Object.keys(this.message).includes(this.locales.value)) {
console.warn(`[i18n]未找到当前${this.locales}]的语言包`);
return key;
}
const v = this._deep(this._getMessage() , key) || ""
if (!v) {
console.warn(`[i18n]未找到当前${this.locales}]的语言包下的[${key}]对应的值`);
return v;
}
if (["string", "number"].includes(typeof args)) {
return this._insertStringVariable(v, args)
}
if (Array.isArray(args)) {
return this._insertArrayVariable(v, args)
}
if (typeof args === "object") {
return this._insertObjectVariables(v, args)
}
}
// 根据语言 获取当前 message 语言包
private _getMessage () {
const _pack = this.message[this.locales.value]
return _pack
}
// 递归获取 深层object的value
private _deep (obj: Record<string, any>, keys: string): string | undefined {
if (!keys) {
return undefined;
}
const k = keys.split(".")
const v = obj?.[k[0]]
if (v == null) {
return undefined;
}
return k.length === 1 ? v : this._deep(v, k.slice(1).join(","))
}
// 写入字符串变量
private _insertStringVariable (value: string, variables: string): string {
return value.replace(this.variable, variables.toString());
}
// 递归写入数组变量值
private _insertArrayVariable (value: string, variables: Array<any>): string {
let v = value.replace(this.variable, variables[0])
return v.includes(this.variable) ? this._insertArrayVariable(v, variables.slice(1)) : v
}
// 写入对象变量值
private _insertObjectVariables(value: string, variables: Record<string, any>): string {
if (!variables) {
return value
}
return value.replace(/\$\{([\w\d_]+)\}/g, (match: string, k: string) => {
const variable = variables[k];
return variable === undefined ? match : variable.toString();
});
}
}
export default I18n
以上是i18n的所有代码了(自写的,可能与github上的有所不同),用法与vue- i18n的用法一致
import { createApp } from 'vue'
import App from './App.vue'
import en from './locales/en.json';
import zh from './locales/zh.json';
import I18n from './utils/ts/i18n';
const i18n = new I18n({
locales: "zh",
message: { zh, en }
})
// console.log(i18n);
const app = createApp(App)
app.use(i18n) // 初始化 i18n
app.mount('#app')