const { isPlainObject } = require("./utils") const DataTypes = ["String","Number","Boolean","Object","Array","Function","Null","Undefined","Symbol","Date","RegExp","Error"]; module.exports = class i18nScope { constructor(options={},callback){ // 每个作用域都有一个唯一的id this._id = options.id || (new Date().getTime().toString()+parseInt(Math.random()*1000)) 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 // 当前作用域的格式化函数列表 this._loaders = options.loaders // 异步加载语言文件的函数列表 this._global = null // 引用全局VoerkaI18n配置,注册后自动引用 this._patchMessages = {} // 语言包补丁信息 {:{....},:{....}} // 主要用来缓存格式化器的引用,当使用格式化器时可以直接引用,避免检索 this.$cache={ activeLanguage : null, typedFormatters: {}, formatters : {}, } // 如果不存在全局VoerkaI18n实例,说明当前Scope是唯一或第一个加载的作用域, // 则使用当前作用域来初始化全局VoerkaI18n实例 if(!globalThis.VoerkaI18n){ const { I18nManager } = require("./") globalThis.VoerkaI18n = new I18nManager({ defaultLanguage: this.defaultLanguage, activeLanguage : this.activeLanguage, languages: options.languages, }) } this.global = globalThis.VoerkaI18n this._mergePatchedMessages() this._patch(this._messages,newLanguage) // 正在加载语言包标识 this._loading=false // 在全局注册作用域 this.register(callback) } // 作用域 get id(){return this._id} // 默认语言名称 get defaultLanguage(){return this._defaultLanguage} // 默认语言名称 get activeLanguage(){return this._activeLanguage} // 默认语言包 get default(){return this._default} // 当前语言包 get messages(){return this._messages} // 消息id映射列表 get idMap(){return this._idMap} // 当前作用域的格式化函数列表 get formatters(){return this._formatters} // 当前作用域支持的语言 get languages(){return this._languages} // 异步加载语言文件的函数列表 get loaders(){return this._loaders} // 引用全局VoerkaI18n配置,注册后自动引用 get global(){return this._global} set global(value){this._global = value} /** * 在全局注册作用域 * @param {*} callback 当注册 */ register(callback){ if(!typeof(callback)==="function") callback = ()=>{} this.global.register(this).then(callback).catch(callback) } registerFormatter(name,formatter,{language="*"}={}){ if(!typeof(formatter)==="function" || typeof(name)!=="string"){ throw new TypeError("Formatter must be a function") } if(DataTypes.includes(name)){ this.formatters[language].$types[name] = formatter }else{ this.formatters[language][name] = formatter } } /** * 注册默认文本信息加载器 * @param {Function} 必须是异步函数或者是返回Promise */ registerDefaultLoader(fn){ this.global.registerDefaultLoader(fn) } /** * 回退到默认语言 */ _fallback(){ this._messages = this._default this._activeLanguage = this.defaultLanguage } /** * 刷新当前语言包 * @param {*} newLanguage */ async refresh(newLanguage){ this._loading = Promise.resolve() if(!newLanguage) newLanguage = this.activeLanguage // 默认语言,默认语言采用静态加载方式,只需要简单的替换即可 if(newLanguage === this.defaultLanguage){ this._messages = this._default await this._patch(this._messages,newLanguage) // 异步补丁 return } // 非默认语言需要异步加载语言包文件,加载器是一个异步函数 // 如果没有加载器,则无法加载语言包,因此回退到默认语言 let loader = this.loaders[newLanguage] try{ if(typeof(loader) === "function"){ this._messages = (await loader()).default this._activeLanguage = newLanguage await this._patch(this._messages,newLanguage) }else if(typeof(this.global.defaultMessageLoader) === "function"){// 如果该语言没有指定加载器,则使用全局配置的默认加载器 this._messages = await this.global.loadMessagesFromDefaultLoader(newLanguage,this) this._activeLanguage = newLanguage }else{ this._fallback() } }catch(e){ console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`) this._fallback() } } /** * 当指定了默认语言包加载器后,会从服务加载语言补丁包来更新本地的语言包 * * 补丁包会自动存储到本地的LocalStorage中 * * @param {*} messages * @param {*} newLanguage * @returns */ async _patch(messages,newLanguage){ if(typeof(this.global.loadMessagesFromDefaultLoader) !== 'function') return try{ let pachedMessages = await this.global.loadMessagesFromDefaultLoader(newLanguage,this) if(isPlainObject(pachedMessages)){ Object.assign(messages,pachedMessages) this._savePatchedMessages(pachedMessages,newLanguage) } }catch{} } /** * 从本地存储中读取语言包补丁合并到当前语言包中 */ _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){ console.error("Error while save voerkai18n patched messages:",e.message) } } _getPatchedMessages(language){ try{ return JSON.parse(localStorage.getItem(`voerkai18n_${this.id}_${language}_patched_messages`)) }catch(e){ return {} } } // 以下方法引用全局VoerkaI18n实例的方法 get on(){return this.global.on.bind(this.global)} get off(){return this.global.off.bind(this.global)} get offAll(){return this.global.offAll.bind(this.global)} get change(){ return this.global.change.bind(this.global) } }