diff --git a/.gitignore b/.gitignore index 7b7af42..fae58f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ /.vscode /node_modules node_modules -/demo/apps/app/languages -/demo/apps/*/languages -/demo/*/node_modules -/coverage \ No newline at end of file +/packages/**/node_modules +/coverage +/packages/apps/vueapp/src/languages +/packages/apps/app/languages \ No newline at end of file diff --git a/package.json b/package.json index 78cceef..8babf63 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,7 @@ "test:extract": "jest extract", "test:translate": "jest translate", "demo:extract": "node ./packages/demo/extract.demo.js", - "demo:compile": "node ./packages/demo/compile.demo.js", - "publish":"" + "demo:compile": "node ./packages/demo/compile.demo.js" }, "author": "", "license": "ISC", diff --git a/packages/apps/app/languages/index.js b/packages/apps/app/languages/index.js index 9ca9114..1824519 100644 --- a/packages/apps/app/languages/index.js +++ b/packages/apps/app/languages/index.js @@ -1,12 +1,12 @@ const messageIds = require("./idMap") -const { translate,I18nManager,i18nScope } = require("@voerkai18n/runtime") +const { translate,i18nScope } = require("./runtime.js") + const formatters = require("./formatters.js") const defaultMessages = require("./cn.js") const activeMessages = defaultMessages - // 语言配置文件 const scopeSettings = { "languages": [ @@ -40,6 +40,5 @@ const scope = new i18nScope({ const t = translate.bind(scope) module.exports.t = t -module.exports.scope = scope -module.exports.i18nManager = VoerkaI18n +module.exports.i18nScope = scope diff --git a/packages/apps/app/package.json b/packages/apps/app/package.json index d6b9f02..cc097dc 100644 --- a/packages/apps/app/package.json +++ b/packages/apps/app/package.json @@ -1 +1,6 @@ -{"type":"module","dependencies":{"@voerkai18n/cli":"workspace:^1.0.6","@voerkai18n/runtime":"^1.0.0"}} \ No newline at end of file +{ + "dependencies": { + "@voerkai18n/cli": "workspace:^1.0.6", + "@voerkai18n/runtime": "^1.0.0" + } +} \ No newline at end of file diff --git a/packages/apps/vueapp/src/App.vue b/packages/apps/vueapp/src/App.vue index 7505026..9cfbc00 100644 --- a/packages/apps/vueapp/src/App.vue +++ b/packages/apps/vueapp/src/App.vue @@ -2,6 +2,8 @@ // This starter template is using Vue 3 diff --git a/packages/apps/vueapp/src/languages/index.js b/packages/apps/vueapp/src/languages/index.js index 6d2e561..03127fa 100644 --- a/packages/apps/vueapp/src/languages/index.js +++ b/packages/apps/vueapp/src/languages/index.js @@ -1,6 +1,8 @@ import messageIds from "./idMap.js" -import { translate,I18nManager,i18nScope } from "@voerkai18n/runtime" +import runtime from "./runtime.js" +const { translate,i18nScope } = runtime + import formatters from "./formatters.js" import defaultMessages from "./cn.js" const activeMessages = defaultMessages @@ -40,7 +42,6 @@ const t = translate.bind(scope) export { t, - i18nScope:scope, - i18nManager:VoerkaI18n, + i18nScope as scope } diff --git a/packages/apps/vueapp/src/main.js b/packages/apps/vueapp/src/main.js index 80e3bb2..03550a6 100644 --- a/packages/apps/vueapp/src/main.js +++ b/packages/apps/vueapp/src/main.js @@ -1,6 +1,5 @@ import { createApp } from 'vue' import App from './App.vue' -import { t, i18nScope } from './languages' createApp(App).mount('#app') diff --git a/packages/cli/compile.command.js b/packages/cli/compile.command.js index 9a12715..a2997a1 100644 --- a/packages/cli/compile.command.js +++ b/packages/cli/compile.command.js @@ -43,7 +43,7 @@ function normalizeCompileOptions(opts={}) { module.exports =async function compile(langFolder,opts={}){ const options = normalizeCompileOptions(opts); - let { moduleType } = options; + let { moduleType,inlineRuntime } = options; // 如果自动则会从当前项目读取,如果没有指定则会是esm if(moduleType==="auto"){ moduleType = findModuleType(langFolder) @@ -51,7 +51,11 @@ module.exports =async function compile(langFolder,opts={}){ const projectPackageJson = getCurrentPackageJson(langFolder) // 加载多语言配置文件 const settingsFile = path.join(langFolder,"settings.json") + + try{ + + // 读取多语言配置文件 const langSettings = fs.readJSONSync(settingsFile) let { languages,defaultLanguage,activeLanguage,namespaces } = langSettings @@ -80,8 +84,8 @@ module.exports =async function compile(langFolder,opts={}){ logger.log(t("读取语言文件{}失败:{}"),file,e.message) } }) - logger.log(t(" - 共合成{}条语言包文本"),Object.keys(messages).length) - + logger.log(t(" - 共合成{}条文本"),Object.keys(messages).length) + // 2. 为每一个文本内容生成一个唯一的id let messageIds = {} Object.entries(messages).forEach(([msg,langs])=>{ @@ -113,8 +117,19 @@ module.exports =async function compile(langFolder,opts={}){ } logger.log(t(" - idMap文件: {}"),path.basename(idMapFile)) + // 嵌入运行时源码 + if(inlineRuntime){ + const runtimeSourceFolder = path.join(require.resolve("@voerkai18n/runtime"),"../..") + fs.copyFileSync( + path.join(runtimeSourceFolder,"dist",`runtime.${moduleType === 'esm' ? 'mjs' : 'cjs'}`), + path.join(langFolder,"runtime.js") + ) + logger.log(t(" - 运行时: {}"),"runtime.js") + } + const templateContext = { scopeId:projectPackageJson.name, + inlineRuntime, languages, defaultLanguage, activeLanguage, diff --git a/packages/cli/extract.plugin.js b/packages/cli/extract.plugin.js index 66a1044..3b75cc5 100644 --- a/packages/cli/extract.plugin.js +++ b/packages/cli/extract.plugin.js @@ -10,7 +10,6 @@ const deepmerge = require("deepmerge") const path = require('path') const fs = require('fs-extra') const createLogger = require("logsets") -const { replaceInterpolateVars,getDataTypeName } = require("@voerkai18n/runtime") const { findModuleType,createPackageJsonFile,t } = require("./utils") const logger = createLogger() diff --git a/packages/cli/index.js b/packages/cli/index.js index 3bfc48b..351a0a9 100644 --- a/packages/cli/index.js +++ b/packages/cli/index.js @@ -46,7 +46,7 @@ program .option('-r, --reset', t('重新生成当前项目的语言配置')) .option('-lngs, --languages ', t('支持的语言列表'), ['cn','en']) .option('-d, --defaultLanguage ', t('默认语言'), 'cn') - .option('-i, --installRuntime', t('自动安装默认语言'),true) + // .option('-i, --installRuntime', t('自动安装默认语言'),true) .option('-a, --activeLanguage ', t('激活语言'), 'cn') .hook("preAction",async function(location){ const lang= process.env.LANGUAGE || "cn" @@ -104,6 +104,7 @@ program .command('compile') .description(t('编译指定项目的语言包')) .option('-d, --debug', t('输出调试信息')) + .option('--no-inline-runtime', t('不嵌入运行时源码')) .option('-m, --moduleType [types]', t('输出模块类型,取值auto,esm,cjs'), 'esm') .argument('[location]', t('工程项目所在目录'),"./") .hook("preAction",async function(location){ diff --git a/packages/cli/init.command.js b/packages/cli/init.command.js index d9d409d..afbbf53 100644 --- a/packages/cli/init.command.js +++ b/packages/cli/init.command.js @@ -56,10 +56,10 @@ module.exports = function(srcPath,{debug = true,languages=["cn","en"],defaultLan fs.writeFileSync(settingsFile,JSON.stringify(settings,null,4)) // 自动安装运行时@voerkai18n/runtime - if(installRuntime){ - logger.log(t("正在安装多语言运行时:{}"),"@voerkai18n/runtime") - installVoerkai18nRuntim(srcPath) - } + // if(installRuntime){ + // logger.log(t("正在安装多语言运行时:{}"),"@voerkai18n/runtime") + // installVoerkai18nRuntim(srcPath) + // } if(debug) { logger.log(t("生成语言配置文件:{}"),"./languages/settings.json") diff --git a/packages/cli/languages/cn.js b/packages/cli/languages/cn.js index bea4eea..a70b30b 100644 --- a/packages/cli/languages/cn.js +++ b/packages/cli/languages/cn.js @@ -38,9 +38,12 @@ module.exports = { "37": "模块类型\\t: {}", "38": "编译结果输出至:{}", "39": "读取语言文件{}失败:{}", - "40": " - 共合成{}条语言包文本", - "41": " - 语言包文件: {}", - "42": " - idMap文件: {}", - "43": " - 格式化器:{}", - "44": "Now is { value | date | bjTime }" + "40": " - 语言包文件: {}", + "41": " - idMap文件: {}", + "42": " - 格式化器:{}", + "43": "Now is { value | date | bjTime }", + "44": " - 共合成{}条文本", + "45": " - 运行时: {}", + "46": "自动安装默认语言", + "47": "不嵌入运行时源码" } \ No newline at end of file diff --git a/packages/cli/languages/en.js b/packages/cli/languages/en.js index 1dfda9d..8ecdcc3 100644 --- a/packages/cli/languages/en.js +++ b/packages/cli/languages/en.js @@ -38,9 +38,12 @@ module.exports = { "37": "Type of module\\t\\t: {}", "38": "Compile to:{}", "39": "Error while read language file{}: {}", - "40": " - Total {} messages", - "41": " - Language file: {}", - "42": " - idMap file: {}", - "43": " - Formatters: {}", - "44": "Now is { value | date | bjTime }" + "40": " - Language file: {}", + "41": " - idMap file: {}", + "42": " - Formatters: {}", + "43": "Now is { value | date | bjTime }", + "44": " - Total{} messages", + "45": " - Runtime: {}", + "46": "Auto install default language", + "47": "Not inline runtime source" } \ No newline at end of file diff --git a/packages/cli/languages/idMap.js b/packages/cli/languages/idMap.js index 27275be..2b87550 100644 --- a/packages/cli/languages/idMap.js +++ b/packages/cli/languages/idMap.js @@ -38,9 +38,12 @@ module.exports = { "模块类型\\t: {}": 37, "编译结果输出至:{}": 38, "读取语言文件{}失败:{}": 39, - " - 共合成{}条语言包文本": 40, - " - 语言包文件: {}": 41, - " - idMap文件: {}": 42, - " - 格式化器:{}": 43, - "Now is { value | date | bjTime }": 44 + " - 语言包文件: {}": 40, + " - idMap文件: {}": 41, + " - 格式化器:{}": 42, + "Now is { value | date | bjTime }": 43, + " - 共合成{}条文本": 44, + " - 运行时: {}": 45, + "自动安装默认语言": 46, + "不嵌入运行时源码": 47 } \ No newline at end of file diff --git a/packages/cli/languages/index.js b/packages/cli/languages/index.js index 757d603..743e19b 100644 --- a/packages/cli/languages/index.js +++ b/packages/cli/languages/index.js @@ -1,6 +1,7 @@ const messageIds = require("./idMap") -const { translate,I18nManager,i18nScope } = require("@voerkai18n/runtime") +const { translate,i18nScope } = require("./runtime.js") + const formatters = require("./formatters.js") const defaultMessages = require("./cn.js") const activeMessages = defaultMessages diff --git a/packages/cli/languages/runtime.js b/packages/cli/languages/runtime.js new file mode 100644 index 0000000..cc4055d --- /dev/null +++ b/packages/cli/languages/runtime.js @@ -0,0 +1,924 @@ +'use strict'; + +/** +* +* 简单的事件触发器 +* +*/ + +var eventemitter = class EventEmitter{ + constructor(){ + this._callbacks = []; + } + on(callback){ + if(this._callbacks.includes(callback)) return + this._callbacks.push(callback); + } + off(callback){ + for(let i=0;icb(...args))); + }else { + await Promise.all(this._callbacks.map(cb=>cb(...args))); + } + } +}; + +var scope = 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 || "cn"; // 默认语言名称 + 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.$cache={ + activeLanguage : null, + typedFormatters: {}, + formatters : {}, + }; + // 如果不存在全局VoerkaI18n实例,说明当前Scope是唯一或第一个加载的作用域, + // 则使用当前作用域来初始化全局VoerkaI18n实例 + if(!globalThis.VoerkaI18n){ + const { I18nManager } = runtime; + globalThis.VoerkaI18n = new I18nManager({ + defaultLanguage: this.defaultLanguage, + activeLanguage : this.activeLanguage, + languages: options.languages, + }); + } + this.global = globalThis.VoerkaI18n; + // 正在加载语言包标识 + 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 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; + } + } + /** + * 回退到默认语言 + */ + _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; + return + } + // 非默认语言需要异步加载语言包文件,加载器是一个异步函数 + // 如果没有加载器,则无法加载语言包,因此回退到默认语言 + const loader = this.loaders[newLanguage]; + if(typeof(loader) === "function"){ + try{ + this._messages = (await loader()).default; + this._activeLanguage = newLanguage; + }catch(e){ + console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`); + this._fallback(); + } + }else { + this._fallback(); + } + } + // 以下方法引用全局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) + } +}; + +/** + * 内置的格式化器 + * + */ + +/** + * 字典格式化器 + * 根据输入data的值,返回后续参数匹配的结果 + * dict(data,,,,,,,...) + * + * + * dict(1,1,"one",2,"two",3,"three",4,"four") == "one" + * dict(2,1,"one",2,"two",3,"three",4,"four") == "two" + * dict(3,1,"one",2,"two",3,"three",4,"four") == "three" + * dict(4,1,"one",2,"two",3,"three",4,"four") == "four" + * // 无匹配时返回原始值 + * dict(5,1,"one",2,"two",3,"three",4,"four") == 5 + * // 无匹配时并且后续参数个数是奇数,则返回最后一个参数 + * dict(5,1,"one",2,"two",3,"three",4,"four","more") == "more" + * + * 在翻译中使用 + * I have { value | dict(1,"one",2,"two",3,"three",4,"four")} apples + * + * @param {*} value + * @param {...any} args + * @returns + */ + function dict(value,...args){ + for(let i=0;i0 && (args.length % 2!==0)) return args[args.length-1] + return value +} + +var formatters$1 = { + "*":{ + $types:{ + Date:(value)=>value.toLocaleString() + }, + time:(value)=> value.toLocaleTimeString(), + shorttime:(value)=> value.toLocaleTimeString(), + date: (value)=> value.toLocaleDateString(), + dict, //字典格式化器 + }, + cn:{ + $types:{ + Date:(value)=> `${value.getFullYear()}年${value.getMonth()+1}月${value.getDate()}日 ${value.getHours()}点${value.getMinutes()}分${value.getSeconds()}秒` + }, + shortime:(value)=> value.toLocaleTimeString(), + time:(value)=>`${value.getHours()}点${value.getMinutes()}分${value.getSeconds()}秒`, + date: (value)=> `${value.getFullYear()}年${value.getMonth()+1}月${value.getDate()}日`, + shortdate: (value)=> `${value.getFullYear()}-${value.getMonth()+1}-${value.getDate()}`, + currency:(value)=>`${value}元`, + }, + en:{ + currency:(value)=>{ + return `$${value}` + } + } +}; + +const EventEmitter = eventemitter; +const i18nScope = scope; +let formatters = formatters$1; + + +// 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter } +// 不支持参数: let varWithPipeRegexp = /\{\s*(?\w+)?(?(\s*\|\s*\w*\s*)*)\s*\}/g + +// 支持参数: { var | formatter(x,x,..) | formatter } +let varWithPipeRegexp = /\{\s*(?\w+)?(?(\s*\|\s*\w*(\(.*\)){0,1}\s*)*)\s*\}/g; + +// 有效的语言名称列表 +const languages = ["af","am","ar-dz","ar-iq","ar-kw","ar-ly","ar-ma","ar-sa","ar-tn","ar","az","be","bg","bi","bm","bn","bo","br","bs","ca","cs","cv","cy","da","de-at","de-ch","de","dv","el","en-au","en-ca","en-gb","en-ie","en-il","en-in","en-nz","en-sg","en-tt","en","eo","es-do","es-mx","es-pr","es-us","es","et","eu","fa","fi","fo","fr-ca","fr-ch","fr","fy","ga","gd","gl","gom-latn","gu","he","hi","hr","ht","hu","hy-am","id","is","it-ch","it","ja","jv","ka","kk","km","kn","ko","ku","ky","lb","lo","lt","lv","me","mi","mk","ml","mn","mr","ms-my","ms","mt","my","nb","ne","nl-be","nl","nn","oc-lnc","pa-in","pl","pt-br","pt","ro","ru","rw","sd","se","si","sk","sl","sq","sr-cyrl","sr","ss","sv-fi","sv","sw","ta","te","tet","tg","th","tk","tl-ph","tlh","tr","tzl","tzm-latn","tzm","ug-cn","uk","ur","uz-latn","uz","vi","x-pseudo","yo","zh-cn","zh-hk","zh-tw","zh"]; + +/** + * 考虑到通过正则表达式进行插件的替换可能较慢,因此提供一个简单方法来过滤掉那些 + * 不需要进行插值处理的字符串 + * 原理很简单,就是判断一下是否同时具有{和}字符,如果有则认为可能有插值变量,如果没有则一定没有插件变量,则就不需要进行正则匹配 + * 从而可以减少不要的正则匹配 + * 注意:该方法只能快速判断一个字符串不包括插值变量 + * @param {*} str + * @returns {boolean} true=可能包含插值变量, + */ +function hasInterpolation(str){ + return str.includes("{") && str.includes("}") +} +const DataTypes$1 = ["String","Number","Boolean","Object","Array","Function","Error","Symbol","RegExp","Date","Null","Undefined","Set","Map","WeakSet","WeakMap"]; + +/** + * 获取指定变量类型名称 + * getDataTypeName(1) == Number + * getDataTypeName("") == String + * getDataTypeName(null) == Null + * getDataTypeName(undefined) == Undefined + * getDataTypeName(new Date()) == Date + * getDataTypeName(new Error()) == Error + * + * @param {*} v + * @returns + */ + function getDataTypeName(v){ + if (v === null) return 'Null' + if (v === undefined) return 'Undefined' + if(typeof(v)==="function") return "Function" + return v.constructor && v.constructor.name; +}function isPlainObject(obj){ + if (typeof obj !== 'object' || obj === null) return false; + var proto = Object.getPrototypeOf(obj); + if (proto === null) return true; + var baseProto = proto; + + while (Object.getPrototypeOf(baseProto) !== null) { + baseProto = Object.getPrototypeOf(baseProto); + } + return proto === baseProto; +} +function isNumber(value){ + return !isNaN(parseInt(value)) +} + + + +/** + * 简单进行对象合并 + * + * options={ + * array:0 , // 数组合并策略,0-替换,1-合并,2-去重合并 + * object:0, // 对象合并策略,0-替换,1-合并,2-去重合并 + * } + * + * @param {*} toObj + * @param {*} formObj + * @returns 合并后的对象 + */ +function deepMerge(toObj,formObj,options={}){ + let results = Object.assign({},toObj); + Object.entries(formObj).forEach(([key,value])=>{ + if(key in results){ + if(typeof value === "object" && value !== null){ + if(Array.isArray(value)){ + if(options.array === 0){ + results[key] = value; + }else if(options.array === 1){ + results[key] = [...results[key],...value]; + }else if(options.array === 2){ + results[key] = [...new Set([...results[key],...value])]; + } + }else { + results[key] = deepMerge(results[key],value,options); + } + }else { + results[key] = value; + } + }else { + results[key] = value; + } + }); + return results +} + + +/** + 通过正则表达式对原始文本内容进行解析匹配后得到的 + formatters="| aaa(1,1) | bbb " + + 需要统一解析为 + + [ + [aaa,[1,1]], // [formatter'name,[args,...]] + [bbb,[]], + ] + + formatters="| aaa(1,1,"dddd") | bbb " + + 目前对参数采用简单的split(",")来解析,因为无法正确解析aaa(1,1,"dd,,dd")形式的参数 + 在此场景下基本够用了,如果需要支持更复杂的参数解析,可以后续考虑使用正则表达式来解析 + + @returns [[,[,,...]]] + */ +function parseFormatters(formatters){ + if(!formatters) return [] + // 1. 先解析为 ["aaa()","bbb"]形式 + let result = formatters.trim().substr(1).trim().split("|").map(r=>r.trim()); + + // 2. 解析格式化器参数 + return result.map(formatter=>{ + let firstIndex = formatter.indexOf("("); + let lastIndex = formatter.lastIndexOf(")"); + if(firstIndex!==-1 && lastIndex!==-1){ // 带参数的格式化器 + const argsContent = formatter.substr(firstIndex+1,lastIndex-firstIndex-1).trim(); + let args = argsContent=="" ? [] : argsContent.split(",").map(arg=>{ + arg = arg.trim(); + if(!isNaN(parseInt(arg))){ + return parseInt(arg) // 数字 + }else if((arg.startsWith('\"') && arg.endsWith('\"')) || (arg.startsWith('\'') && arg.endsWith('\'')) ){ + return arg.substr(1,arg.length-2) // 字符串 + }else if(arg.toLowerCase()==="true" || arg.toLowerCase()==="false"){ + return arg.toLowerCase()==="true" // 布尔值 + }else if((arg.startsWith('{') && arg.endsWith('}')) || (arg.startsWith('[') && arg.endsWith(']'))){ + try{ + return JSON.parse(arg) + }catch(e){ + return String(arg) + } + }else { + return String(arg) + } + }); + return [formatter.substr(0,firstIndex),args] + }else {// 不带参数的格式化器 + return [formatter,[]] + } + }) +} + +/** + * 提取字符串中的插值变量 + * // [ + // { + name:<变量名称>,formatters:[{name:<格式化器名称>,args:[<参数>,<参数>,....]]}],<匹配字符串>], + // .... + // + * @param {*} str + * @param {*} isFull =true 保留所有插值变量 =false 进行去重 + * @returns {Array} + * [ + * { + * name:"<变量名称>", + * formatters:[ + * {name:"<格式化器名称>",args:[<参数>,<参数>,....]}, + * {name:"<格式化器名称>",args:[<参数>,<参数>,....]}, + * ], + * match:"<匹配字符串>" + * }, + * ... + * ] + */ +function getInterpolatedVars(str){ + let vars = []; + forEachInterpolatedVars(str,(varName,formatters,match)=>{ + let varItem = { + name:varName, + formatters:formatters.map(([formatter,args])=>{ + return { + name:formatter, + args:args + } + }), + match:match + }; + if(vars.findIndex(varDef=>((varDef.name===varItem.name) && (varItem.formatters.toString() == varDef.formatters.toString())))===-1){ + vars.push(varItem); + } + return "" + }); + return vars +} +/** + * 遍历str中的所有插值变量传递给callback,将callback返回的结果替换到str中对应的位置 + * @param {*} str + * @param {Function(<变量名称>,[formatters],match[0])} callback + * @returns 返回替换后的字符串 + */ +function forEachInterpolatedVars(str,callback,options={}){ + let result=str, match; + let opts = Object.assign({ + replaceAll:true, // 是否替换所有插值变量,当使用命名插值时应置为true,当使用位置插值时应置为false + },options); + varWithPipeRegexp.lastIndex=0; + while ((match = varWithPipeRegexp.exec(result)) !== null) { + const varname = match.groups.varname || ""; + // 解析格式化器和参数 = [,[,[,,...]]] + const formatters = parseFormatters(match.groups.formatters); + if(typeof(callback)==="function"){ + try{ + if(opts.replaceAll){ + result=result.replaceAll(match[0],callback(varname,formatters,match[0])); + }else { + result=result.replace(match[0],callback(varname,formatters,match[0])); + } + }catch{// callback函数可能会抛出异常,如果抛出异常,则中断匹配过程 + break + } + } + varWithPipeRegexp.lastIndex=0; + } + return result +} + +function resetScopeCache(scope,activeLanguage=null){ + scope.$cache = {activeLanguage,typedFormatters:{},formatters:{}}; +} +/** + * 取得指定数据类型的默认格式化器 + * + * 可以为每一个数据类型指定一个默认的格式化器,当传入插值变量时, + * 会自动调用该格式化器来对值进行格式化转换 + + const formatters = { + "*":{ + $types:{...} // 在所有语言下只作用于特定数据类型的格式化器 + }, // 在所有语言下生效的格式化器 + cn:{ + $types:{ + [数据类型]:(value)=>{...}, + }, + [格式化器名称]:(value)=>{...}, + [格式化器名称]:(value)=>{...}, + [格式化器名称]:(value)=>{...}, + }, + } + * @param {*} scope + * @param {*} activeLanguage + * @param {*} dataType 数字类型 + * @returns {Function} 格式化函数 + */ +function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){ + if(!scope.$cache) resetScopeCache(scope); + if(scope.$cache.activeLanguage === activeLanguage) { + if(dataType in scope.$cache.typedFormatters) return scope.$cache.typedFormatters[dataType] + }else {// 当语言切换时清空缓存 + resetScopeCache(scope,activeLanguage); + } + + // 先在当前作用域中查找,再在全局查找 + const targets = [scope.formatters,scope.global.formatters]; + for(const target of targets){ + if(!target) continue + // 优先在当前语言的$types中查找 + if((activeLanguage in target) && isPlainObject(target[activeLanguage].$types)){ + let formatters = target[activeLanguage].$types; + if(dataType in formatters && typeof(formatters[dataType])==="function"){ + return scope.$cache.typedFormatters[dataType] = formatters[dataType] + } + } + // 在所有语言的$types中查找 + if(("*" in target) && isPlainObject(target["*"].$types)){ + let formatters = target["*"].$types; + if(dataType in formatters && typeof(formatters[dataType])==="function"){ + return scope.$cache.typedFormatters[dataType] = formatters[dataType] + } + } + } +} + +/** + * 获取指定名称的格式化器函数 + * @param {*} scope + * @param {*} activeLanguage + * @param {*} name 格式化器名称 + * @returns {Function} 格式化函数 + */ +function getFormatter(scope,activeLanguage,name){ + // 缓存格式化器引用,避免重复检索 + if(!scope.$cache) resetScopeCache(scope); + if(scope.$cache.activeLanguage === activeLanguage) { + if(name in scope.$cache.formatters) return scope.$cache.formatters[name] + }else {// 当语言切换时清空缓存 + resetScopeCache(scope,activeLanguage); + } + // 先在当前作用域中查找,再在全局查找 + const targets = [scope.formatters,scope.global.formatters]; + for(const target of targets){ + // 优先在当前语言查找 + if(activeLanguage in target){ + let formatters = target[activeLanguage] || {}; + if((name in formatters) && typeof(formatters[name])==="function") return scope.$cache.formatters[name] = formatters[name] + } + // 在所有语言的$types中查找 + let formatters = target["*"] || {}; + if((name in formatters) && typeof(formatters[name])==="function") return scope.$cache.formatters[name] = formatters[name] + } +} + +/** + * 执行格式化器并返回结果 + * @param {*} value + * @param {*} formatters 多个格式化器顺序执行,前一个输出作为下一个格式化器的输入 + */ +function executeFormatter(value,formatters){ + if(formatters.length===0) return value + let result = value; + try{ + for(let formatter of formatters){ + if(typeof(formatter) === "function") { + result = formatter(result); + }else {// 如果碰到无效的格式化器,则跳过过续的格式化器 + return result + } + } + }catch(e){ + console.error(`Error while execute i18n formatter for ${value}: ${e.message} ` ); + } + return result +} +/** + * 将 [[格式化器名称,[参数,参数,...]],[格式化器名称,[参数,参数,...]]]格式化器转化为 + * + * + * + * @param {*} scope + * @param {*} activeLanguage + * @param {*} formatters + */ +function buildFormatters(scope,activeLanguage,formatters){ + let results = []; + for(let formatter of formatters){ + if(formatter[0]){ + const func = getFormatter(scope,activeLanguage,formatter[0]); + if(typeof(func)==="function"){ + results.push((v)=>{ + return func(v,...formatter[1]) + }); + }else { + // 格式化器无效或者没有定义时,查看当前值是否具有同名的原型方法,如果有则执行调用 + // 比如padStart格式化器是String的原型方法,不需要配置就可以直接作为格式化器调用 + results.push((v)=>{ + if(typeof(v[formatter[0]])==="function"){ + return v[formatter[0]].call(v,...formatter[1]) + }else { + return v + } + }); + } + } + } + return results +} + +/** + * 将value经过格式化器处理后返回 + * @param {*} scope + * @param {*} activeLanguage + * @param {*} formatters + * @param {*} value + * @returns + */ +function getFormattedValue(scope,activeLanguage,formatters,value){ + // 1. 取得格式化器函数列表 + const formatterFuncs = buildFormatters(scope,activeLanguage,formatters); + // 2. 查找每种数据类型默认格式化器,并添加到formatters最前面,默认数据类型格式化器优先级最高 + const defaultFormatter = getDataTypeDefaultFormatter(scope,activeLanguage,getDataTypeName(value)); + if(defaultFormatter){ + formatterFuncs.splice(0,0,defaultFormatter); + } + // 3. 执行格式化器 + value = executeFormatter(value,formatterFuncs); + return value +} + +/** + * 字符串可以进行变量插值替换, + * replaceInterpolatedVars("<模板字符串>",{变量名称:变量值,变量名称:变量值,...}) + * replaceInterpolatedVars("<模板字符串>",[变量值,变量值,...]) + * replaceInterpolatedVars("<模板字符串>",变量值,变量值,...]) + * +- 当只有两个参数并且第2个参数是{}时,将第2个参数视为命名变量的字典 + replaceInterpolatedVars("this is {a}+{b},{a:1,b:2}) --> this is 1+2 +- 当只有两个参数并且第2个参数是[]时,将第2个参数视为位置参数 + replaceInterpolatedVars"this is {}+{}",[1,2]) --> this is 1+2 +- 普通位置参数替换 + replaceInterpolatedVars("this is {a}+{b}",1,2) --> this is 1+2 +- +this == scope == { formatters: {}, ... } +* @param {*} template +* @returns +*/ +function replaceInterpolatedVars(template,...args) { + const scope = this; + // 当前激活语言 + const activeLanguage = scope.global.activeLanguage; + + // 没有变量插值则的返回原字符串 + if(args.length===0 || !hasInterpolation(template)) return template + + // ****************************变量插值**************************** + if(args.length===1 && isPlainObject(args[0])){ + // 读取模板字符串中的插值变量列表 + // [[var1,[formatter,formatter,...],match],[var2,[formatter,formatter,...],match],...} + let varValues = args[0]; + return forEachInterpolatedVars(template,(varname,formatters)=>{ + let value = (varname in varValues) ? varValues[varname] : ''; + return getFormattedValue(scope,activeLanguage,formatters,value) + }) + }else { + // ****************************位置插值**************************** + // 如果只有一个Array参数,则认为是位置变量列表,进行展开 + const params=(args.length===1 && Array.isArray(args[0])) ? [...args[0]] : args; + if(params.length===0) return template // 没有变量则不需要进行插值处理,返回原字符串 + let i = 0; + return forEachInterpolatedVars(template,(varname,formatters)=>{ + if(params.length>i){ + return getFormattedValue(scope,activeLanguage,formatters,params[i++]) + }else { + throw new Error() // 抛出异常,停止插值处理 + } + },{replaceAll:false}) + + } +} + +// 默认语言配置 +const defaultLanguageSettings = { + defaultLanguage: "cn", + activeLanguage: "cn", + languages:[ + {name:"cn",title:"中文",default:true}, + {name:"en",title:"英文"} + ], + formatters +}; + +function isMessageId(content){ + return parseInt(content)>0 +} +/** + * 根据值的单数和复数形式,从messages中取得相应的消息 + * + * @param {*} messages 复数形式的文本内容 = [<=0时的内容>,<=1时的内容>,<=2时的内容>,...] + * @param {*} value + */ +function getPluraMessage(messages,value){ + try{ + if(Array.isArray(messages)){ + return messages.length > value ? messages[value] : messages[messages.length-1] + }else { + return messages + } + }catch{ + return Array.isArray(messages) ? messages[0] : messages + } +} +function escape(str){ + return str.replaceAll(/\\(?![trnbvf'"]{1})/g,"\\\\") + .replaceAll("\t","\\t") + .replaceAll("\n","\\n") + .replaceAll("\b","\\b") + .replaceAll("\r","\\r") + .replaceAll("\f","\\f") + .replaceAll("\'","\\'") + .replaceAll('\"','\\"') + .replaceAll('\v','\\v') +} +function unescape(str){ + return str + .replaceAll("\\t","\t") + .replaceAll("\\n","\n") + .replaceAll("\\b","\b") + .replaceAll("\\r","\r") + .replaceAll("\\f","\f") + .replaceAll("\\'","\'") + .replaceAll('\\"','\"') + .replaceAll('\\v','\v') + .replaceAll(/\\\\(?![trnbvf'"]{1})/g,"\\") +} +/** + * 翻译函数 + * +* translate("要翻译的文本内容") 如果默认语言是中文,则不会进行翻译直接返回 +* translate("I am {} {}","man") == I am man 位置插值 +* translate("I am {p}",{p:"man"}) 字典插值 +* translate("total {$count} items", {$count:1}) //复数形式 +* translate("total {} {} {} items",a,b,c) // 位置变量插值 + * + * this===scope 当前绑定的scope + * + */ +function translate(message) { + const scope = this; + const activeLanguage = scope.global.activeLanguage; + let content = message; + let vars=[]; // 插值变量列表 + let pluralVars= []; // 复数变量 + let pluraValue = null; // 复数值 + if(!typeof(message)==="string") return message + try{ + // 1. 预处理变量: 复数变量保存至pluralVars中 , 变量如果是Function则调用 + if(arguments.length === 2 && isPlainObject(arguments[1])){ + Object.entries(arguments[1]).forEach(([name,value])=>{ + if(typeof(value)==="function"){ + try{ + vars[name] = value(); + }catch(e){ + vars[name] = value; + } + } + // 以$开头的视为复数变量 + if(name.startsWith("$") && typeof(vars[name])==="number") pluralVars.push(name); + }); + vars = [arguments[1]]; + }else if(arguments.length >= 2){ + vars = [...arguments].splice(1).map((arg,index)=>{ + try{ + arg = typeof(arg)==="function" ? arg() : arg; + // 位置参数中以第一个数值变量为复数变量 + if(isNumber(arg)) pluraValue = parseInt(arg); + }catch(e){ } + return arg + }); + + } + + + + + // 3. 取得翻译文本模板字符串 + if(activeLanguage === scope.defaultLanguage){ + // 2.1 从默认语言中取得翻译文本模板字符串 + // 如果当前语言就是默认语言,不需要查询加载,只需要做插值变换即可 + // 当源文件运用了babel插件后会将原始文本内容转换为msgId + // 如果是msgId则从scope.default中读取,scope.default=默认语言包={:} + if(isMessageId(content)){ + content = scope.default[content] || message; + } + }else { + // 2.2 从当前语言包中取得翻译文本模板字符串 + // 如果没有启用babel插件将源文本转换为msgId,需要先将文本内容转换为msgId + // JSON.stringify在进行转换时会将\t\n\r转换为\\t\\n\\r,这样在进行匹配时就出错 + let msgId = isMessageId(content) ? content : scope.idMap[escape(content)]; + content = scope.messages[msgId] || content; + content = Array.isArray(content) ? content.map(v=>unescape(v)) : unescape(content); + } + // 2. 处理复数 + // 经过上面的处理,content可能是字符串或者数组 + // content = "原始文本内容" || 复数形式["原始文本内容","原始文本内容"....] + // 如果是数组说明要启用复数机制,需要根据插值变量中的某个变量来判断复数形式 + if(Array.isArray(content) && content.length>0){ + // 如果存在复数命名变量,只取第一个复数变量 + if(pluraValue!==null){ // 启用的是位置插值,pluraIndex=第一个数字变量的位置 + content = getPluraMessage(content,pluraValue); + }else if(pluralVar.length>0){ + content = getPluraMessage(content,parseInt(vars(pluralVar[0]))); + }else { // 如果找不到复数变量,则使用第一个内容 + content = content[0]; + } + } + + // 进行插值处理 + if(vars.length==0){ + return content + }else { + return replaceInterpolatedVars.call(scope,content,...vars) + } + }catch(e){ + return content // 出错则返回原始文本 + } +} + +/** + * 多语言管理类 + * + * 当导入编译后的多语言文件时(import("./languages")),会自动生成全局实例VoerkaI18n + * + * VoerkaI18n.languages // 返回支持的语言列表 + * VoerkaI18n.defaultLanguage // 默认语言 + * VoerkaI18n.language // 当前语言 + * VoerkaI18n.change(language) // 切换到新的语言 + * + * + * VoerkaI18n.on("change",(language)=>{}) // 注册语言切换事件 + * VoerkaI18n.off("change",(language)=>{}) + * + * */ + class I18nManager extends EventEmitter{ + constructor(settings={}){ + super(); + if(I18nManager.instance!=null){ + return I18nManager.instance; + } + I18nManager.instance = this; + this._settings = deepMerge(defaultLanguageSettings,settings); + this._scopes=[]; + return I18nManager.instance; + } + get settings(){ return this._settings } + get scopes(){ return this._scopes } + // 当前激活语言 + get activeLanguage(){ return this._settings.activeLanguage} + // 默认语言 + get defaultLanguage(){ return this.this._settings.defaultLanguage} + // 支持的语言列表 + get languages(){ return this._settings.languages} + // 全局格式化器 + get formatters(){ return formatters } + /** + * 切换语言 + */ + async change(value){ + value=value.trim(); + if(this.languages.findIndex(lang=>lang.name === value)!==-1){ + // 通知所有作用域刷新到对应的语言包 + await this._refreshScopes(value); + this._settings.activeLanguage = value; + /// 触发语言切换事件 + await this.emit(value); + }else { + throw new Error("Not supported language:"+value) + } + } + /** + * 当切换语言时调用此方法来加载更新语言包 + * @param {*} newLanguage + */ + async _refreshScopes(newLanguage){ + // 并发执行所有作用域语言包的加载 + try{ + const scopeRefreshers = this._scopes.map(scope=>{ + return scope.refresh(newLanguage) + }); + if(Promise.allSettled){ + await Promise.allSettled(scopeRefreshers); + }else { + await Promise.all(scopeRefreshers); + } + }catch(e){ + console.warn("Error while refreshing i18n scopes:",e.message); + } + } + /** + * + * 注册一个新的作用域 + * + * 每一个库均对应一个作用域,每个作用域可以有多个语言包,且对应一个翻译函数 + * 除了默认语言外,其他语言采用动态加载的方式 + * + * @param {*} scope + */ + async register(scope){ + if(!(scope instanceof i18nScope)){ + throw new TypeError("Scope must be an instance of I18nScope") + } + this._scopes.push(scope); + await scope.refresh(this.activeLanguage); + } + /** + * 注册全局格式化器 + * 格式化器是一个简单的同步函数value=>{...},用来对输入进行格式化后返回结果 + * + * registerFormatters(name,value=>{...}) // 适用于所有语言 + * registerFormatters(name,value=>{...},{langauge:"cn"}) // 适用于cn语言 + * registerFormatters(name,value=>{...},{langauge:"en"}) // 适用于en语言 + + * @param {*} formatters + */ + registerFormatter(name,formatter,{language="*"}={}){ + if(!typeof(formatter)==="function" || typeof(name)!=="string"){ + throw new TypeError("Formatter must be a function") + } + if(DataTypes$1.includes(name)){ + this.formatters[language].$types[name] = formatter; + }else { + this.formatters[language][name] = formatter; + } + } +} + +var runtime ={ + getInterpolatedVars, + replaceInterpolatedVars, + I18nManager, + translate, + languages, + i18nScope, + defaultLanguageSettings, + getDataTypeName, + isNumber, + isPlainObject +}; + +module.exports = runtime; diff --git a/packages/cli/languages/translates/default.json b/packages/cli/languages/translates/default.json index 2347110..781d9e0 100644 --- a/packages/cli/languages/translates/default.json +++ b/packages/cli/languages/translates/default.json @@ -237,12 +237,6 @@ "compile.command.js" ] }, - " - 共合成{}条语言包文本": { - "en": " - Total {} messages", - "$file": [ - "compile.command.js" - ] - }, " - 语言包文件: {}": { "en": " - Language file: {}", "$file": [ @@ -266,5 +260,29 @@ "$file": [ "templates\\formatters.js" ] + }, + " - 共合成{}条文本": { + "en": " - Total{} messages", + "$file": [ + "compile.command.js" + ] + }, + " - 运行时: {}": { + "en": " - Runtime: {}", + "$file": [ + "compile.command.js" + ] + }, + "自动安装默认语言": { + "en": "Auto install default language", + "$file": [ + "index.js" + ] + }, + "不嵌入运行时源码": { + "en": "Not inline runtime source", + "$file": [ + "index.js" + ] } } \ No newline at end of file diff --git a/packages/cli/package.json b/packages/cli/package.json index 03f84d7..4f48a64 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@voerkai18n/cli", - "version": "1.0.7", + "version": "1.0.10", "description": "VoerkaI18n command line interactive tools", "main": "index.js", "homepage": "https://gitee.com/zhangfisher/voerka-i18n", @@ -18,13 +18,13 @@ "test": "echo \"Error: no test specified\" && exit 1", "extract": "node ./index.js extract -d -e babel-plugin-voerkai18n.js,templates/**", "compile": "node ./index.js compile -d", - "compile:en": "cross-env LANGUAGE=en node ./index.js compile -d" + "compile:en": "cross-env LANGUAGE=en node ./index.js compile -d", + "release": "npm version patch && pnpm publish --no-git-checks --access public" }, "author": "wxzhang", "license": "MIT", "bin": { - "voerkai18n": "./index.js", - "publish": "npm publish -access public" + "voerkai18n": "./index.js" }, "dependencies": { "@babel/cli": "^7.17.6", diff --git a/packages/cli/readme.md b/packages/cli/readme.md index 42c3de4..8ab058a 100644 --- a/packages/cli/readme.md +++ b/packages/cli/readme.md @@ -11,12 +11,11 @@ Arguments: location 工程项目所在目录 Options: - -d, --debug 输出调试信息 + -D, --debug 输出调试信息 -r, --reset 重新生成当前项目的语言配置 - -m, --moduleType [type] 生成的js模块类型,取值auto,esm,cjs (default: "auto") -lngs, --languages 支持的语言列表 (default: ["cn","en"]) - -default, --defaultLanguage 默认语言 - -active, --activeLanguage 激活语言 + -d, --defaultLanguage 默认语言 + -a, --activeLanguage 激活语言 -h, --help display help for command @@ -33,10 +32,10 @@ Arguments: location 工程项目所在目录 (default: "./") Options: - -d, --debug 输出调试信息 + -D, --debug 输出调试信息 -lngs, --languages 支持的语言 - -default, --defaultLanguage 默认语言 - -active, --activeLanguage 激活语言 + -d, --defaultLanguage 默认语言 + -a, --activeLanguage 激活语言 -ns, --namespaces 翻译名称空间 -e, --exclude 排除要扫描的文件夹,多个用逗号分隔 -u, --updateMode 本次提取内容与已存在内容的数据合并策略,默认取值sync=同步,overwrite=覆盖,merge=合并 @@ -58,7 +57,7 @@ Arguments: Options: -d, --debug 输出调试信息 - -m, --moduleType [types] 输出模块类型,取值auto,esm,cjs (default: "auto") + -m, --moduleType [types] 输出模块类型,取值auto,esm,cjs (default: "esm") -h, --help display help for command ``` diff --git a/packages/cli/templates/entry.js b/packages/cli/templates/entry.js index 6d63eb6..34170f4 100644 --- a/packages/cli/templates/entry.js +++ b/packages/cli/templates/entry.js @@ -1,13 +1,16 @@ {{if moduleType === "esm"}} import messageIds from "./idMap.js" -import { translate,I18nManager,i18nScope } from "@voerkai18n/runtime" +{{if inlineRuntime }}import runtime from "./runtime.js" +const { translate,i18nScope } = runtime +{{else}}import { translate,i18nScope } from "@voerkai18n/runtime"{{/if}} import formatters from "./formatters.js" import defaultMessages from "./{{defaultLanguage}}.js" {{if defaultLanguage === activeLanguage}}const activeMessages = defaultMessages {{else}}import activeMessages from "./{{activeLanguage}}.js"{{/if}} {{else}} const messageIds = require("./idMap") -const { translate,I18nManager,i18nScope } = require("@voerkai18n/runtime") +{{if inlineRuntime }}const { translate,i18nScope } = require("./runtime.js") +{{else}}const { translate,i18nScope } = require("@voerkai18n/runtime"){{/if}} const formatters = require("./formatters.js") const defaultMessages = require("./{{defaultLanguage}}.js") {{if defaultLanguage === activeLanguage}}const activeMessages = defaultMessages @@ -33,11 +36,9 @@ const t = translate.bind(scope) {{if moduleType === "esm"}} export { t, - i18nScope:scope, - i18nManager:VoerkaI18n, + i18nScope as scope } {{else}} module.exports.t = t -module.exports.i18nScope = scope -module.exports.i18nManager = VoerkaI18n +module.exports.i18nScope = scope {{/if}} diff --git a/packages/runtime/babel.config.js b/packages/runtime/babel.config.js new file mode 100644 index 0000000..1c3d3ee --- /dev/null +++ b/packages/runtime/babel.config.js @@ -0,0 +1,25 @@ +module.exports = { + "presets": [ + [ + "@babel/preset-env", + { + "useBuiltIns": "usage", + "debug": false, + "modules": false, + "corejs":{ + "version":"3.21", + "proposals": true + } + } + ] + ], + "plugins": [ + [ + "@babel/plugin-transform-runtime", + { + "corejs":3, + "proposals": true + } + ] + ] + } \ No newline at end of file diff --git a/packages/runtime/dist/index.cjs b/packages/runtime/dist/index.cjs index e4473c2..0023309 100644 --- a/packages/runtime/dist/index.cjs +++ b/packages/runtime/dist/index.cjs @@ -1 +1,2 @@ -"use strict";var t=function(t){return function(t){return!!t&&"object"==typeof t}(t)&&!function(t){var r=Object.prototype.toString.call(t);return"[object RegExp]"===r||"[object Date]"===r||function(t){return t.$$typeof===e}(t)}(t)};var e="function"==typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function r(t,e){return!1!==e.clone&&e.isMergeableObject(t)?l((r=t,Array.isArray(r)?[]:{}),t,e):t;var r}function a(t,e,a){return t.concat(e).map((function(t){return r(t,a)}))}function n(t){return Object.keys(t).concat(function(t){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(t).filter((function(e){return t.propertyIsEnumerable(e)})):[]}(t))}function s(t,e){try{return e in t}catch(t){return!1}}function i(t,e,a){var i={};return a.isMergeableObject(t)&&n(t).forEach((function(e){i[e]=r(t[e],a)})),n(e).forEach((function(n){(function(t,e){return s(t,e)&&!(Object.hasOwnProperty.call(t,e)&&Object.propertyIsEnumerable.call(t,e))})(t,n)||(s(t,n)&&a.isMergeableObject(e[n])?i[n]=function(t,e){if(!e.customMerge)return l;var r=e.customMerge(t);return"function"==typeof r?r:l}(n,a)(t[n],e[n],a):i[n]=r(e[n],a))})),i}function l(e,n,s){(s=s||{}).arrayMerge=s.arrayMerge||a,s.isMergeableObject=s.isMergeableObject||t,s.cloneUnlessOtherwiseSpecified=r;var l=Array.isArray(n);return l===Array.isArray(e)?l?s.arrayMerge(e,n,s):i(e,n,s):r(n,s)}l.all=function(t,e){if(!Array.isArray(t))throw new Error("first argument should be an array");return t.reduce((function(t,r){return l(t,r,e)}),{})};var o=l;var c={"*":{$types:{Date:t=>t.toLocaleString()},time:t=>t.toLocaleTimeString(),shorttime:t=>t.toLocaleTimeString(),date:t=>t.toLocaleDateString(),dict:function(t,...e){for(let r=0;r0&&e.length%2!=0?e[e.length-1]:t}},cn:{$types:{Date:t=>`${t.getFullYear()}年${t.getMonth()+1}月${t.getDate()}日 ${t.getHours()}点${t.getMinutes()}分${t.getSeconds()}秒`},shortime:t=>t.toLocaleTimeString(),time:t=>`${t.getHours()}点${t.getMinutes()}分${t.getSeconds()}秒`,date:t=>`${t.getFullYear()}年${t.getMonth()+1}月${t.getDate()}日`,shortdate:t=>`${t.getFullYear()}-${t.getMonth()+1}-${t.getDate()}`,currency:t=>`${t}元`},en:{currency:t=>`$${t}`}};const u=o,g=class{constructor(){this._callbacks=[]}on(t){this._callbacks.includes(t)||this._callbacks.push(t)}off(t){for(let e=0;ee(...t)))):await Promise.all(this._callbacks.map((e=>e(...t))))}},f=class{constructor(t={},e){if(this._id=t.id||(new Date).getTime().toString()+parseInt(1e3*Math.random()),this._languages=t.languages,this._defaultLanguage=t.defaultLanguage||"cn",this._activeLanguage=t.activeLanguage,this._default=t.default,this._messages=t.messages,this._idMap=t.idMap,this._formatters=t.formatters,this._loaders=t.loaders,this._global=null,this.$cache={activeLanguage:null,typedFormatters:{},formatters:{}},!globalThis.VoerkaI18n){const{I18nManager:e}=E;globalThis.VoerkaI18n=new e({defaultLanguage:this.defaultLanguage,activeLanguage:this.activeLanguage,languages:t.languages})}this.global=globalThis.VoerkaI18n,this._loading=!1,this.register(e)}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}get idMap(){return this._idMap}get formatters(){return this._formatters}get loaders(){return this._loaders}get global(){return this._global}set global(t){this._global=t}register(t){this.global.register(this).then(t).catch(t)}registerFormatter(t,e,{language:r="*"}={}){if("string"!=typeof t)throw new TypeError("Formatter must be a function");DataTypes.includes(t)?this.formatters[r].$types[t]=e:this.formatters[r][t]=e}_fallback(){this._messages=this._default,this._activeLanguage=this.defaultLanguage}async refresh(t){if(this._loading=Promise.resolve(),t||(t=this.activeLanguage),t===this.defaultLanguage)return void(this._messages=this._default);const e=this.loaders[t];if("function"==typeof e)try{this._messages=(await e()).default,this._activeLanguage=t}catch(e){console.warn(`Error while loading language <${t}> on i18nScope(${this.id}): ${e.message}`),this._fallback()}else this._fallback()}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)}};let h=c,p=/\{\s*(?\w+)?(?(\s*\|\s*\w*(\(.*\)){0,1}\s*)*)\s*\}/g;const m=["String","Number","Boolean","Object","Array","Function","Error","Symbol","RegExp","Date","Null","Undefined","Set","Map","WeakSet","WeakMap"];function b(t){return null===t?"Null":void 0===t?"Undefined":"function"==typeof t?"Function":t.constructor&&t.constructor.name}function y(t){if("object"!=typeof t||null===t)return!1;var e=Object.getPrototypeOf(t);if(null===e)return!0;for(var r=e;null!==Object.getPrototypeOf(r);)r=Object.getPrototypeOf(r);return e===r}function d(t){return!isNaN(parseInt(t))}function _(t){if(!t)return[];return t.trim().substr(1).trim().split("|").map((t=>t.trim())).map((t=>{let e=t.indexOf("("),r=t.lastIndexOf(")");if(-1!==e&&-1!==r){const a=t.substr(e+1,r-e-1).trim();let n=""==a?[]:a.split(",").map((t=>{if(t=t.trim(),!isNaN(parseInt(t)))return parseInt(t);if(t.startsWith('"')&&t.endsWith('"')||t.startsWith("'")&&t.endsWith("'"))return t.substr(1,t.length-2);if("true"===t.toLowerCase()||"false"===t.toLowerCase())return"true"===t.toLowerCase();if(!(t.startsWith("{")&&t.endsWith("}")||t.startsWith("[")&&t.endsWith("]")))return String(t);try{return JSON.parse(t)}catch(e){return String(t)}}));return[t.substr(0,e),n]}return[t,[]]}))}function v(t,e,r={}){let a,n=t,s=Object.assign({replaceAll:!0},r);for(p.lastIndex=0;null!==(a=p.exec(n));){const t=a.groups.varname||"",r=_(a.groups.formatters);if("function"==typeof e)try{n=s.replaceAll?n.replaceAll(a[0],e(t,r,a[0])):n.replace(a[0],e(t,r,a[0]))}catch{break}p.lastIndex=0}return n}function $(t,e=null){t.$cache={activeLanguage:e,typedFormatters:{},formatters:{}}}function A(t,e,r){if(t.$cache||$(t),t.$cache.activeLanguage===e){if(r in t.$cache.formatters)return t.$cache.formatters[r]}else $(t,e);const a=[t.formatters,t.global.formatters];for(const n of a){if(e in n){let a=n[e]||{};if(r in a&&"function"==typeof a[r])return t.$cache.formatters[r]=a[r]}let a=n["*"]||{};if(r in a&&"function"==typeof a[r])return t.$cache.formatters[r]=a[r]}}function L(t,e,r,a){const n=function(t,e,r){let a=[];for(let n of r)if(n[0]){const r=A(t,e,n[0]);"function"==typeof r?a.push((t=>r(t,...n[1]))):a.push((t=>"function"==typeof t[n[0]]?t[n[0]].call(t,...n[1]):t))}return a}(t,e,r),s=function(t,e,r){if(t.$cache||$(t),t.$cache.activeLanguage===e){if(r in t.$cache.typedFormatters)return t.$cache.typedFormatters[r]}else $(t,e);const a=[t.formatters,t.global.formatters];for(const n of a)if(n){if(e in n&&y(n[e].$types)){let a=n[e].$types;if(r in a&&"function"==typeof a[r])return t.$cache.typedFormatters[r]=a[r]}if("*"in n&&y(n["*"].$types)){let e=n["*"].$types;if(r in e&&"function"==typeof e[r])return t.$cache.typedFormatters[r]=e[r]}}}(t,e,b(a));return s&&n.splice(0,0,s),a=function(t,e){if(0===e.length)return t;let r=t;try{for(let t of e){if("function"!=typeof t)return r;r=t(r)}}catch(e){console.error(`Error while execute i18n formatter for ${t}: ${e.message} `)}return r}(a,n),a}function w(t,...e){const r=this,a=r.global.activeLanguage;if(0===e.length||(!(n=t).includes("{")||!n.includes("}")))return t;var n;if(1===e.length&&y(e[0])){let n=e[0];return v(t,((t,e)=>{let s=t in n?n[t]:"";return L(r,a,e,s)}))}{const n=1===e.length&&Array.isArray(e[0])?[...e[0]]:e;if(0===n.length)return t;let s=0;return v(t,((t,e)=>{if(n.length>s)return L(r,a,e,n[s++]);throw new Error}),{replaceAll:!1})}}const S={defaultLanguage:"cn",activeLanguage:"cn",languages:{cn:{name:"cn",title:"中文",default:!0},en:{name:"en",title:"英文"}},formatters:h};function k(t){return parseInt(t)>0}function O(t,e){try{return Array.isArray(t)?t.length>e?t[e]:t[t.length-1]:t}catch{return Array.isArray(t)?t[0]:t}}function M(t){return t.replaceAll(/\\(?![trnbvf'"]{1})/g,"\\\\").replaceAll("\t","\\t").replaceAll("\n","\\n").replaceAll("\b","\\b").replaceAll("\r","\\r").replaceAll("\f","\\f").replaceAll("'","\\'").replaceAll('"','\\"').replaceAll("\v","\\v")}function j(t){return t.replaceAll("\\t","\t").replaceAll("\\n","\n").replaceAll("\\b","\b").replaceAll("\\r","\r").replaceAll("\\f","\f").replaceAll("\\'","'").replaceAll('\\"','"').replaceAll("\\v","\v").replaceAll(/\\\\(?![trnbvf'"]{1})/g,"\\")}class I extends g{static instance=null;callbacks=[];constructor(t={}){return super(),null!=I.instance||(I.instance=this,this._settings=u(S,t),this._scopes=[]),I.instance}get settings(){return this._settings}get scopes(){return this._scopes}get activeLanguage(){return this._settings.activeLanguage}get defaultLanguage(){return this.this._settings.defaultLanguage}get languages(){return this._settings.languages}get formatters(){return h}async change(t){if(t=t.trim(),-1===this.languages.findIndex((e=>e.name===t)))throw new Error("Not supported language:"+t);await this._refreshScopes(t),this._settings.activeLanguage=t,await this.emit(t)}async _refreshScopes(t){try{const e=this._scopes.map((e=>e.refresh(t)));Promise.allSettled?await Promise.allSettled(e):await Promise.all(e)}catch(t){console.warn("Error while refreshing i18n scopes:",t.message)}}async register(t){if(!(t instanceof f))throw new TypeError("Scope must be an instance of I18nScope");this._scopes.push(t),await t.refresh(this.activeLanguage)}registerFormatter(t,e,{language:r="*"}={}){if("string"!=typeof t)throw new TypeError("Formatter must be a function");m.includes(t)?this.formatters[r].$types[t]=e:this.formatters[r][t]=e}}var E={getInterpolatedVars:function(t){let e=[];return v(t,((t,r,a)=>{let n={name:t,formatters:r.map((([t,e])=>({name:t,args:e}))),match:a};return-1===e.findIndex((t=>t.name===n.name&&n.formatters.toString()==t.formatters.toString()))&&e.push(n),""})),e},replaceInterpolatedVars:w,I18nManager:I,translate:function(t){const e=this,r=e.global.activeLanguage;let a=t,n=[],s=[],i=null;try{if(2===arguments.length&&y(arguments[1])?(Object.entries(arguments[1]).forEach((([t,e])=>{if("function"==typeof e)try{n[t]=e()}catch(r){n[t]=e}t.startsWith("$")&&"number"==typeof n[t]&&s.push(t)})),n=[arguments[1]]):arguments.length>=2&&(n=[...arguments].splice(1).map(((t,e)=>{try{d(t="function"==typeof t?t():t)&&(i=parseInt(t))}catch(t){}return t}))),r===e.defaultLanguage)k(a)&&(a=e.default[a]||t);else{let t=k(a)?a:e.idMap[M(a)];a=e.messages[t]||a,a=Array.isArray(a)?a.map((t=>j(t))):j(a)}return Array.isArray(a)&&a.length>0&&(a=null!==i?O(a,i):pluralVar.length>0?O(a,parseInt(n(pluralVar[0]))):a[0]),0==n.length?a:w.call(e,a,...n)}catch(t){return a}},languages:["af","am","ar-dz","ar-iq","ar-kw","ar-ly","ar-ma","ar-sa","ar-tn","ar","az","be","bg","bi","bm","bn","bo","br","bs","ca","cs","cv","cy","da","de-at","de-ch","de","dv","el","en-au","en-ca","en-gb","en-ie","en-il","en-in","en-nz","en-sg","en-tt","en","eo","es-do","es-mx","es-pr","es-us","es","et","eu","fa","fi","fo","fr-ca","fr-ch","fr","fy","ga","gd","gl","gom-latn","gu","he","hi","hr","ht","hu","hy-am","id","is","it-ch","it","ja","jv","ka","kk","km","kn","ko","ku","ky","lb","lo","lt","lv","me","mi","mk","ml","mn","mr","ms-my","ms","mt","my","nb","ne","nl-be","nl","nn","oc-lnc","pa-in","pl","pt-br","pt","ro","ru","rw","sd","se","si","sk","sl","sq","sr-cyrl","sr","ss","sv-fi","sv","sw","ta","te","tet","tg","th","tk","tl-ph","tlh","tr","tzl","tzm-latn","tzm","ug-cn","uk","ur","uz-latn","uz","vi","x-pseudo","yo","zh-cn","zh-hk","zh-tw","zh"],i18nScope:f,defaultLanguageSettings:S,getDataTypeName:b,isNumber:d,isPlainObject:y};module.exports=E; +"use strict";var t="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};zE({global:!0},{globalThis:n});var r=n,e=function(t){return t&&t.Math==Math&&t},n=e("object"==(void 0===r?"undefined":NO(r))&&r)||e("object"==("undefined"==typeof window?"undefined":NO(window))&&window)||e("object"==("undefined"==typeof self?"undefined":NO(self))&&self)||e("object"==NO(t)&&t)||function(){return this}()||Function("return this")(),o=function(t){return"function"==typeof t},i=n.String,a=function(t){try{return i(t)}catch(t){return"Object"}},u=o,c=a,f=n.TypeError,s=function(t){if(u(t))return t;throw f(c(t)+" is not a function")},l=o,v=function(t){return"object"==NO(t)?null!==t:l(t)},h=n,p=v,g=h.String,d=h.TypeError,y=function(t){if(p(t))return t;throw d(g(t)+" is not an object")},m=function(t){try{return!!t()}catch(t){return!0}},b=mE,w=s,x=y;zE({target:"Reflect",stat:!0,forced:!m((function(){E((function(){}))}))},{apply:function(t,r,e){return b(w(t),r,x(e))}});var S={},E=S.Reflect.apply,O=function(t){return t&&t.Math==Math&&t},A=O("object"==(void 0===r?"undefined":NO(r))&&r)||O("object"==("undefined"==typeof window?"undefined":NO(window))&&window)||O("object"==("undefined"==typeof self?"undefined":NO(self))&&self)||O("object"==NO(t)&&t)||function(){return this}()||Function("return this")(),j={exports:{}},k={exports:{}},L=!m((function(){return 7!=US({},1,{get:function(){return 7}})[1]})),I={exports:{}},T=Math.ceil,_=Math.floor,P=function(t){var r=+t;return r!=r||0===r?0:(r>0?_:T)(r)},R=P,M=Math.max,F=Math.min,N=function(t,r){var e=R(t);return e<0?M(e+r,0):F(e,r)},C=P,D=Math.min,z=function(t){return t>0?D(C(t),9007199254740991):0},$=z,G=function(t){return $(t.length)},B=mS,U=N,W=G,V=function(t){return function(r,e,n){var o,i=B(r),a=W(i),u=U(n,a);if(t&&e!=e){for(;a>u;)if((o=i[u++])!=o)return!0}else for(;a>u;u++)if((t||u in i)&&i[u]===e)return t||u||0;return!t&&-1}},Y={includes:V(!0),indexOf:V(!1)},H=m,J=function(t,r){var e=[][t];return!!e&&H((function(){e.call(null,r||function(){return 1},1)}))},q=zE,K=fO,X=ct(Y),Q=J,Z=K(ct([])),tt=!!Z&&1/Z([1],1,-0)<0,rt=Q("indexOf");q({target:"Array",proto:!0,forced:tt||!rt},{indexOf:function(t){var r=arguments.length>1?arguments[1]:void 0;return tt?Z(this,t,r)||0:X(this,t,r)}});var et=S,nt=function(t){return et[t+"Prototype"]},ot=ct(nt("Array")),it=sO,at=ot,ut=Array.prototype,ct=function(t){var r=ct(t);return t===ut||it(ut,t)&&r===ct(ut)?at:r};zE({target:"Array",stat:!0},{isArray:dt});var ft=S.Array.isArray,st=ft,lt=fO,vt=lt({}.toString),ht=lt(Bx("")),pt=function(t){return ht(vt(t),8,-1)},gt=pt,dt=st||function(t){return"Array"==gt(t)},yt={exports:{}},mt=n,bt=US,wt=function(t,r){try{bt(mt,t,{value:r,configurable:!0,writable:!0})}catch(e){mt[t]=r}return r},xt=n["__core-js_shared__"]||wt("__core-js_shared__",{}),St=xt;(yt.exports=function(t,r){return St[t]||(St[t]=void 0!==r?r:{})})("versions",[]).push({version:"3.21.1",mode:"pure",copyright:"© 2014-2022 Denis Pushkarev (zloirock.ru)",license:"https://github.com/zloirock/core-js/blob/v3.21.1/LICENSE",source:"https://github.com/zloirock/core-js"});var Et=function(t){try{return!!t()}catch(t){return!0}},Ot=!Et((function(){var t,r=nO(t=function(){}).call(t);return"function"!=typeof r||r.hasOwnProperty("prototype")})),At=Ot,jt=Function.prototype,kt=nO(jt),Lt=jt.call,It=At&&nO(kt).call(kt,Lt,Lt),Tt=At?function(t){return t&&It(t)}:function(t){return t&&function(){return Lt.apply(t,arguments)}},_t=A.TypeError,Pt=function(t){if(null==t)throw _t("Can't call method on "+t);return t},Rt=Pt,Mt=A.Object,Ft=function(t){return Mt(Rt(t))},Nt=Ft,Ct=Tt({}.hasOwnProperty),Dt=Object.hasOwn||function(t,r){return Ct(Nt(t),r)};WO({target:"Object",stat:!0},{hasOwn:Dt});var zt=n.TypeError,$t=function(t){if(null==t)throw zt("Can't call method on "+t);return t},Gt=$t,Bt=n.Object,Ut=function(t){return Bt(Gt(t))},Wt=Ut,Vt=fO({}.hasOwnProperty),Yt=Object.hasOwn||function(t,r){return Vt(Wt(t),r)},Ht=fO,Jt=0,qt=Math.random(),Kt=Ht(1..toString),Xt=function(t){return"Symbol("+(void 0===t?"":t)+")_"+Kt(++Jt+qt,36)},Qt=!Et((function(){return 7!=US({},1,{get:function(){return 7}})[1]})),Zt={exports:{}},tr=zE,rr=L,er=ur.f;tr({target:"Object",stat:!0,forced:ar!==er,sham:!rr},{defineProperties:er});var nr=S.Object,or=Zt.exports=function(t,r){return nr.defineProperties(t,r)};nr.defineProperties.sham&&(or.sham=!0);var ir=Zt.exports,ar=ir,ur={},cr=L&&m((function(){return 42!=US((function(){}),"prototype",{value:42,writable:!1}).prototype})),fr=function(t,r){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:r}},sr=LS,lr=fr,vr=L?function(t,r,e){return sr.f(t,r,lr(1,e))}:function(t,r,e){return t[r]=e,t},hr={},pr={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0},gr=n,dr=nx,yr=vr,mr=hr,br=Hw("toStringTag");for(var wr in pr){var xr=gr[wr],Sr=xr&&xr.prototype;Sr&&dr(Sr)!==br&&yr(Sr,br,wr),mr[wr]=mr.Array}var Er=Tr(nt("Array")),Or=nx,Ar=Yt,jr=sO,kr=Er,Lr=Array.prototype,Ir={DOMTokenList:!0,NodeList:!0},Tr=function(t){var r=Tr(t);return t===Lr||jr(Lr,t)&&r===Tr(Lr)||Ar(Ir,Or(t))?kr:r},_r=Ut,Pr=Vr;zE({target:"Object",stat:!0,forced:m((function(){Pr(1)}))},{keys:function(t){return Pr(_r(t))}});var Rr=Tr(S.Object),Mr={},Fr=fO,Nr=Yt,Cr=mS,Dr=ct(Y),zr=Mr,$r=Fr([].push),Gr=function(t,r){var e,n=Cr(t),o=0,i=[];for(e in n)!Nr(zr,e)&&Nr(n,e)&&$r(i,e);for(;r.length>o;)Nr(n,e=r[o++])&&(~Dr(i,e)||$r(i,e));return i},Br=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],Ur=Gr,Wr=Br,Vr=Rr||function(t){return Ur(t,Wr)},Yr=L,Hr=cr,Jr=LS,qr=y,Kr=mS,Xr=Vr;ur.f=Yr&&!Hr?ar:function(t,r){qr(t);for(var e,n=Kr(r),o=Xr(r),i=o.length,a=0;i>a;)Jr.f(t,e=o[a++],n[e]);return t};var Qr,Zr=S,te=n,re=o,ee=function(t){return re(t)?t:void 0},ne=function(t,r){return arguments.length<2?ee(Zr[t])||ee(te[t]):Zr[t]&&Zr[t][r]||te[t]&&te[t][r]},oe=ne("document","documentElement"),ie=v,ae=n.document,ue=ie(ae)&&ie(ae.createElement),ce=function(t){return ue?ae.createElement(t):{}},fe=yt.exports,se=Xt,le=fe("keys"),ve=function(t){return le[t]||(le[t]=se(t))},he=y,pe=ur,ge=Br,de=Mr,ye=oe,me=ce,be=ve("IE_PROTO"),we=function(){},xe=function(t){return"