当前位置: 首页 > 工具软件 > i18n-locales > 使用案例 >

实现I18n所有功能(TS)

刘阳荣
2023-12-01

首先浅谈一下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')
 类似资料: