2023-01-27 21:03:37 +08:00

389 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const { DataTypes,isPlainObject, isFunction, getByPath, deepMixin,deepClone } = require("./utils");
const { translate } = require("./translate")
module.exports = class VoerkaI18nScope {
constructor(options = {}, callback) {
this._id = options.id || Date.now().toString() + parseInt(Math.random() * 1000);
this._debug = options.debug == undefined ? process && process.env && process.env.NODE_ENV === "development" : options.debug; // 当出错时是否在控制台台输出错误信息
this._languages = options.languages; // 当前作用域支持的语言列表
this._defaultLanguage = options.defaultLanguage || "zh"; // 默认语言名称
this._activeLanguage = options.activeLanguage; // 当前语言名称
this._default = options.default; // 默认语言包
this._messages = options.messages; // 当前语言包
this._idMap = options.idMap; // 消息id映射列表
this._formatters = options.formatters; // 当前作用域的格式化函数列表{<lang>: {$types,$config,[格式化器名称]: () => {},[格式化器名称]: () => {}}}
this._loaders = options.loaders; // 异步加载语言文件的函数列表
this._global = null; // 引用全局VoerkaI18n配置注册后自动引用
this._patchMessages = {}; // 语言包补丁信息{<language>: {....},<language>:{....}}
this._refreshing = false; // 正在加载语言包标识
// 用来缓存格式化器的引用,当使用格式化器时可以直接引用,减少检索遍历
this.$cache = {
activeLanguage : null,
typedFormatters: {},
formatters : {},
};
this._initiLanguages()
// 如果不存在全局VoerkaI18n实例说明当前Scope是唯一或第一个加载的作用域则自动创建全局VoerkaI18n实例
if (!globalThis.VoerkaI18n) {
const { VoerkaI18nManager } = require("./");
globalThis.VoerkaI18n = new VoerkaI18nManager({
debug : this._debug,
defaultLanguage: this._defaultLanguage,
activeLanguage : this._activeLanguage,
languages : options.languages,
});
}
this._t = translate.bind(this)
this._global = globalThis.VoerkaI18n;
this._initFormatters(this.activeLanguage) // 初始化活动的格式化器
this._mergePatchedMessages(); // 从本地缓存中读取并合并补丁语言包
this._patch(this._messages, this.activeLanguage); // 延后执行补丁命令,该命令会向远程下载补丁包
this.register(callback); // 在全局注册作用域
}
get id() {return this._id;} // 作用域唯一id
get debug() {return this._debug;} // 调试开关
get defaultLanguage() {return this._defaultLanguage;} // 默认语言名称
get activeLanguage() {return this._global.activeLanguage;} // 默认语言名称
get default() {return this._default;} // 默认语言包
get messages() {return this._messages; } // 当前语言包
get idMap() {return this._idMap;} // 消息id映射列表
get languages() {return this._languages;} // 当前作用域支持的语言列表[{name,title,fallback}]
get loaders() { return this._loaders;} // 异步加载语言文件的函数列表
get global() { return this._global;} // 引用全局VoerkaI18n配置注册后自动引用
get formatters() { return this._formatters;} // 当前作用域的所有格式化器定义 {<语言名称>: {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () => {}}}
get activeFormatters() {return this._activeFormatters} // 当前作用域激活的格式化器定义 {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () = >{}}
get activeFormatterConfig(){return this._activeFormatterConfig} // 当前格式化器合并后的配置参数,参数已经合并了全局格式化器中的参数
get translate(){return this._t}
get t(){return this._t}
/**
* 对输入的语言配置进行处理
* - 将en配置为默认回退语言
*/
_initiLanguages(){
if(!Array.isArray(this._languages)){
console.warn("[VoerkaI18n] 无效的语言配置")
this._languages = [
{
name: "zh",
title: "中文"
},
{
name: "en",
title: "英文"
}
]
}
Object.entries(this._languages).forEach(([name,language])=>{
if(!language.fallback) language.fallback = "en"
})
}
/**
* 在全局注册作用域当前作用域
* @param {*} callback 注册成功后的回调
*/
register(callback) {
if (!isFunction(callback)) callback = () => {};
this.global.register(this).then(callback).catch(callback);
}
/**
* 注册格式化器
*
* 格式化器是一个简单的同步函数value=>{...},用来对输入进行格式化后返回结果
*
* registerFormatter(name,value=>{...}) // 注册到所有语言
* registerFormatter(name,value=>{...},{langauge:"zh"}) // 注册到zh语言
* registerFormatter(name,value=>{...},{langauge:"en"}) // 注册到en语言
registerFormatter("Date",value=>{...},{langauge:"en"}) // 注册到en语言的默认数据类型格式化器
registerFormatter(name,value=>{...},{langauge:["zh","cht"]}) // 注册到zh和cht语言
registerFormatter(name,value=>{...},{langauge:"zh,cht"})
* @param {*} formatter 格式化器
language : 字符串或数组,声明该格式化器适用语言
*代表适用于所有语言
语言名称,语言名称数组,或者使用,分割的语言名称字符串
asGlobal : 注册到全局
*/
registerFormatter(name, formatter, { language = "*", global : asGlobal } = {}) {
if (!isFunction(formatter) || typeof name !== "string") {
throw new TypeError("Formatter must be a function");
}
language = Array.isArray(language) ? language: language ? language.split(","): [];
if (asGlobal) {
this.global.registerFormatter(name, formatter, { language });
} else {
language.forEach((lng) => {
if(!(lng in this._formatters)) this._formatters[lng] = {}
if (DataTypes.includes(name)) {
this._formatters[lng].$types[name] = formatter;
} else {
this._formatters[lng][name] = formatter;
}
});
}
}
/**
* 注册多种格式化器
* registerFormatters(={"*",zh:{...},en:{...}})
* registerFormatters(={"*",zh:{...},en:{...}},true) 在全局注册
* @param {*} formatters ={"*",zh:{...},en:{...}}
* @returns
*/
registerFormatters(formatters,asGlobal=false) {
Object.entries(formatters).forEach(([language,fns])=>{
Object.entries(fns).forEach(([name,formatter])=>{
this.registerFormatter(name,formatter,{language,global:asGlobal})
})
})
}
/**
* 初始化格式化器
* 激活和默认语言的格式化器采用静态导入的形式,而没有采用异步块的形式,这是为了确保首次加载时的能马上读取,而不能采用延迟加载方式
* _activeFormatters={$config:{...},$types:{...},[格式化器名称]:()=>{...},[格式化器名称]:()=>{...},...}}
*/
_initFormatters(newLanguage){
// 全局格式化器,用来注册到全局
Object.entries(this._formatters).forEach(([langName,formatters])=>{
if(formatters.global===true){
this.registerFormatters({[langName]:formatters},true)
}else if(isPlainObject(formatters.global)){
this.registerFormatters({[langName]:formatters.global},true)
}
})
this._activeFormatters = {}
try {
if (newLanguage in this._formatters) {
this._activeFormatters = this._formatters[newLanguage];
} else {
if (this._debug) console.warn(`Not initialize <${newLanguage}> formatters.`);
}
this._generateFormatterConfig(newLanguage)
} catch (e) {
if (this._debug) console.error(`Error while initialize ${newLanguage} formatters: ${e.message}`);
}
}
/**
*
* 切换到对应语言的格式化器
*
* 当切换语言时,格式化器应该切换到对应语言的格式化器
*
* 重要需要处理:
* $config参数采用合并继承机制,从全局读取
*
*
* @param {*} language
*/
async _changeFormatters(newLanguage) {
try {
if (newLanguage in this._formatters) {
let loader = this._formatters[newLanguage];
if (isPlainObject(loader)) {
this._activeFormatters = loader;
} else if (isFunction(loader)) {
this._activeFormatters = (await loader()).default;
}
// 合并生成格式化器的配置参数,当执行格式化器时该参数将被传递给格式化器
this._generateFormatterConfig(newLanguage)
} else {
if (this._debug) console.warn(`Not configured <${newLanguage}> formatters.`);
}
} catch (e) {
if (this._debug) console.error(`Error loading ${newLanguage} formatters: ${e.message}`);
}
}
/**
* 生成格式化器的配置参数,该参数由以下合并而成:
* - global.formatters[*].$config
* - global.formatters[language].$config
* - scope.activeFormatters.$config 当前优先
*/
_generateFormatterConfig(language){
try{
const fallbackLanguage = this.getLanguage(language).fallback;
let configSources = [
getByPath(this._global.formatters,`${fallbackLanguage}.$config`,{}),
getByPath(this._global.formatters,"*.$config",{}),
getByPath(this._formatters,`${fallbackLanguage}.$config`,{}),
getByPath(this._formatter,"*.$config",{}),
getByPath(this._global.formatters,`${language}.$config`,{}),
getByPath(this._activeFormatters,"$config",{})
]
return this._activeFormatterConfig = configSources.reduce((finalConfig, config)=>{
if(isPlainObject(config)) deepMixin(finalConfig,config)
return finalConfig
},deepClone(getByPath(this._global.formatters,`*.$config`,{})))
}catch(e){
if(this.debug) console.error(`Error while generate <${language}> formatter options: `,e)
return this._activeFormatters.$config || {}
}
}
/**
* 注册默认文本信息加载器
* @param {Function} 必须是异步函数或者是返回Promise
*/
registerDefaultLoader(fn) {
this.global.registerDefaultLoader(fn);
}
/**
* 获取指定语言信息
* @param {*} language
* @returns
*/
getLanguage(language) {
let index = this._languages.findIndex((lng) => lng.name == language);
if (index !== -1) return this._languages[index];
}
/**
* 返回是否存在指定的语言
* @param {*} language 语言名称
* @returns
*/
hasLanguage(language) {
return this._languages.indexOf((lang) => lang.name == language) !== -1;
}
/**
* 回退到默认语言
*/
_fallback() {
this._messages = this._default;
this._activeLanguage = this.defaultLanguage;
}
/**
* 刷新当前语言包
* @param {*} newLanguage
*/
async refresh(newLanguage) {
this._refreshing = true;
if (!newLanguage) newLanguage = this.activeLanguage;
// 默认语言:由于默认语言采用静态加载方式而不是异步块,因此只需要简单的替换即可
if (newLanguage === this.defaultLanguage) {
this._messages = this._default;
await this._patch(this._messages, newLanguage); // 异步补丁
await this._changeFormatters(newLanguage);
return;
}
// 非默认语言需要异步加载语言包文件,加载器是一个异步函数
// 如果没有加载器,则无法加载语言包,因此回退到默认语言
let loader = this.loaders[newLanguage];
try {
let newMessages, useRemote =false;
if (isPlainObject(loader)) { // 静态语言包
newMessages = loader;
} else if (isFunction(loader)) { // 语言包异步chunk
newMessages = (await loader()).default;
} else if (isFunction(this.global.defaultMessageLoader)) { // 从远程加载语言包:如果该语言没有指定加载器,则使用全局配置的默认加载器
const loadedMessages = await this.global.loadMessagesFromDefaultLoader(newLanguage,this);
if(isPlainObject(loadedMessages)){
useRemote = true
// 需要保存动态语言包中的$config合并到对应语言的格式化器配置
if(isPlainObject(loadedMessages.$config)){
this._formatters[newLanguage] = {
$config : loadedMessages.$config
}
delete loadedMessages.$config
}
newMessages = Object.assign({},this._default,loadedMessages);
}
}
if(newMessages){
this._messages = newMessages
this._activeLanguage = newLanguage;
// 打语言包补丁, 如果是从远程加载语言包则不需要再打补丁了
if(!useRemote) {
await this._patch(this._messages, newLanguage);
}
// 切换到对应语言的格式化器
await this._changeFormatters(newLanguage);
}else{
this._fallback();
}
} catch (e) {
if (this._debug) console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`);
this._fallback();
} finally {
this._refreshing = false;
}
}
/**
* 当指定了默认语言包加载器后,会从服务加载语言补丁包来更新本地的语言包
*
* 补丁包会自动存储到本地的LocalStorage中
*
* @param {*} messages
* @param {*} newLanguage
* @returns
*/
async _patch(messages, newLanguage) {
if (!isFunction(this.global.loadMessagesFromDefaultLoader)) return;
try {
let pachedMessages = await this.global.loadMessagesFromDefaultLoader(newLanguage,this);
if (isPlainObject(pachedMessages)) {
Object.assign(messages, pachedMessages);
this._savePatchedMessages(pachedMessages, newLanguage);
}
} catch (e) {
if (this._debug) console.error(`Error while loading <${newLanguage}> patch messages from remote:`,e);
}
}
/**
* 从本地存储中读取语言包补丁合并到当前语言包中
*/
_mergePatchedMessages() {
let patchedMessages = this._getPatchedMessages(this.activeLanguage);
if (isPlainObject(patchedMessages)) {
Object.assign(this._messages, patchedMessages);
}
}
/**
* 将读取的补丁包保存到本地的LocalStorage中
*
* 为什么要保存到本地的LocalStorage中
*
* 因为默认语言是静态嵌入到源码中的,而加载语言包补丁是延后异步的,
* 当应用启动第一次就会渲染出来的是没有打过补丁的内容。
*
* - 如果还需要等待从服务器加载语言补丁合并后再渲染会影响速度
* - 如果不等待从服务器加载语言补丁就渲染,则会先显示未打补丁的内容,然后在打完补丁后再对应用进行重新渲染生效
* 这明显不是个好的方式
*
* 因此,采用的方式是:
* - 加载语言包补丁后将之保存到到本地的LocalStorage中
* - 当应用加载时会查询是否存在补丁,如果存在就会合并渲染
*
* @param {*} messages
*/
_savePatchedMessages(messages, language) {
try {
if (globalThis.localStorage) {
globalThis.localStorage.setItem(`voerkai18n_${this.id}_${language}_patched_messages`,JSON.stringify(messages));
}
} catch (e) {
if (this.$cache._debug) console.error("Error while save voerkai18n patched messages:",e);
}
}
/**
* 从本地缓存中读取补丁语言包
* @param {*} language
* @returns
*/
_getPatchedMessages(language) {
try {
return JSON.parse(localStorage.getItem(`voerkai18n_${this.id}_${language}_patched_messages`));
} catch (e) {
return {};
}
}
// 以下方法引用全局VoerkaI18n实例的方法
on() {return this._global.on(...arguments); }
off() {return this._global.off(...arguments); }
offAll() {return this._global.offAll(...arguments);}
async change(language) {
await this._global.change(language);
}
};