From af148a30cd2f2eafc16cf59522c8d113e5d00edc Mon Sep 17 00:00:00 2001 From: wxzhang Date: Mon, 15 Aug 2022 18:24:27 +0800 Subject: [PATCH] update formatters --- packages/cli/templates/formatters.js | 4 +- packages/runtime/formatters/en.js | 177 ++++++++++++++++++++++----- packages/runtime/formatters/zh.js | 42 ++++--- packages/runtime/index.js | 8 +- packages/runtime/scope.js | 44 ++++--- packages/runtime/utils.js | 82 +++++++++---- test/translate.test.js | 172 +++++++++++--------------- 7 files changed, 339 insertions(+), 190 deletions(-) diff --git a/packages/cli/templates/formatters.js b/packages/cli/templates/formatters.js index 3cd6df5..fa25b4b 100644 --- a/packages/cli/templates/formatters.js +++ b/packages/cli/templates/formatters.js @@ -7,7 +7,7 @@ - 以下定义了一些格式化器,在中文场景下,会启用这些格式化器。 import dayjs from "dayjs"; export default { - $options:{...}, + $config:{...}, $types:{ Date:(value)=>dayjs(value).format("YYYY年MM月DD日 HH:mm:ss"), }, @@ -32,7 +32,7 @@ {{if moduleType === "esm"}} export default{{else}}module.exports = {{/if}}{ // 格式化器参数 - $options:{ + $config:{ }, // 指定数据类型的默认格式化器 diff --git a/packages/runtime/formatters/en.js b/packages/runtime/formatters/en.js index c591f4b..192c86e 100644 --- a/packages/runtime/formatters/en.js +++ b/packages/runtime/formatters/en.js @@ -3,27 +3,147 @@ * */ - const { toDate,toCurrency } = require("../utils") - + const { toDate,toCurrency,formatDatetime,formatTime,Formatter } = require("../utils") + +// 日期格式化器 +// format取字符串"long","short","local","iso","gmt","utc"或者日期模块字符串 +// { value | date } == '2022/8/15' +// { value | date('long') } == '2022/8/15 12:08:32' +// { value | date('short') } == '8/15' +// { value | date('GMT') } == 'Mon, 15 Aug 2022 06:39:38 GMT' +// { value | date('ISO') } == 'Mon, 15 Aug 2022 06:39:38 ISO' +// { value | date('YYYY-MM-DD HH:mm:ss') } == '2022-8-15 12:08:32' +const dateFormatter = Formatter((value,format,$config)=>{ + const optionals = ["long","short","local","iso","gmt","utc"] + const optionIndex = optionals.findIndex((v,i)=>{ + if(typeof(format)=="string"){ + return v==format || v== format.toUpperCase() + }else if(typeof(format)=="number"){ + return format === i + } + }) + switch(optionIndex){ + case 0: // long + return formatDatetime(value,$config.long) + case 1: // short + return formatDatetime(value,$config.short) + case 2: // local + return value.toLocaleString() + case 3: // ISO + return value.toISOString() + case 4: // GMT + return value.toGMTString() + case 5: // UTC + return value.toUTCString() + default: + return formatDatetime(value,format) + } +},{ + normalize: toDate, // 转换输入为Date类型 + params : ['format'], + configKey: "datetime.date" +}) +// 月份格式化器 format可以取值0,1,2,也可以取字符串long,short,number +const monthFormatter = Formatter((value,format,$config)=>{ + const month = value.getMonth() + if(typeof(format)==='string'){ + format = ['long','short','number'].indexOf(format) + } + if(format<0 && format>2) format = 0 + return format==0 ? $config.names[month] : (format==1 ? $config.shortNames[month] : month+1) +},{ + normalize: toDate, + params : ['format'], + configKey: "datetime.month" +}) + +// 周格式化器 format可以取值0,1,2,也可以取字符串long,short,number +const weekdayFormatter = Formatter((value,format,$config)=>{ + const day = value.getDay() + if(typeof(format)==='string'){ + format = ['long','short','number'].indexOf(format) + } + if(format<0 && format>2) format = 0 + return format==0 ? $config.names[day] : (format==1 ? $config.shortNames[day] : day) +},{ + normalize: toDate, + params : ['format'], + configKey: "datetime.weekday" +}) + + +// 时间格式化器 format可以取值0,1,2,也可以取字符串long,short,timestamp,local +const timeFormatter = Formatter((value,format,$config)=>{ + const month = value.getMonth() + const optionals = ['long','short','timestamp','local'] //toLocaleTimeString + const optionIndex = optionals.findIndex((v,i)=>{ + if(typeof(format)=="string"){ + return v==format || v== format.toUpperCase() + }else if(typeof(format)=="number"){ + return format === i + } + }) + switch(optionIndex){ + case 0: // long + return formatTime(value,$config.long) + case 1: // short + return formatTime(value,$config.short) + case 2: // timestamp + return value.getTime() + case 3: // local + return value.toLocaleTimeString() + default: + return formatTime(value,format) + } +},{ + normalize: toDate, + params : ['format'], + configKey: "datetime.month" +}) + +// 货币格式化器, CNY $13,456.00 +const currencyFormatter = Formatter((value, unit,prefix ,suffix, division,precision) =>{ + return toCurrency(value, { unit,division, prefix, precision,suffix }) +},{ + normalize: toNumber, + params:["prefix","suffix", "division","precision"], + configKey: "currency" +}) module.exports = { - // 配置参数: 格式化器函数的最后一个参数就是该配置参数 - $options:{ - datetime : { - units : ["Year","Quarter","Month","Week","Day","Hour","Minute","Second","Millisecond","Microsecond"], - weekday : ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], - shortWeekdays : ["Sun", "Mon", "Tues", "Wed", "Thur", "Fri", "Sat"], - monthNames : ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], - shorMonthNames: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"] - }, + // 配置参数 + $config:{ + datetime : { + units : ["Year","Quarter","Month","Week","Day","Hour","Minute","Second","Millisecond","Microsecond"], + date :{ + long : 'YYYY/MM/DD HH:mm:ss', + short : "MM/DD", + format : "local" + }, + month:{ + names : ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], + shortNames : ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"], + format : 0 // 0-长名称,1-短名称,2-数字 + }, + weekday:{ + names :["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], + shortNames : ["Sun", "Mon", "Tues", "Wed", "Thur", "Fri", "Sat"], + format : 0, // 0-长名称,1-短名称,2-数字 + }, + time : { + long : "HH:mm:ss", + short : "HH:mm:ss", + format : 'local' + }, + } currency : { - unit : "$", - prefix : "", - suffix : "", - division : 3, - precision : 2 + unit : "$", // 单位 + prefix : "", // 前缀 + suffix : "", // 后缀 + division : 3, // ,分割位 + precision : 2, // 精度 }, number : { division : 3, @@ -50,32 +170,27 @@ Date : value => { const d = toDate(value); return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()} ${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}` }, Null : value =>"", Undefined: value =>"", - Error : value => "ERROR" + Error : value => "ERROR", + Boolean : value =>value ? "True":"False" }, // 以下是格式化定义 - // 日期 - date : value => { const d = toDate(value); return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}` }, - shortdate : value => { const d = toDate(value); return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}` }, - time : value => { const d = toDate(value); return `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}` }, - shorttime : value => { const d = toDate(value); return `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}` }, + // ******************* 日期 ******************* + date : dateFormatter, + time : timeFormatter, year : value => toDate(value).getFullYear(), month : value => toDate(value).getMonth() + 1, day : value => toDate(value).getDate(), - weekdayValue : value => toDate(value).getDay(), - weekday : value => ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][toDate(value).getDay()], - shortWeekday : value => ["Sun", "Mon", "Tues", "Wed", "Thur", "Fri", "Sat"][toDate(value).getDay()], - monthName : value => ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"][toDate(value).getMonth()], - shorMonthName : value => ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"][toDate(value).getMonth()], - // 时间 + weekday : weekdayFormatter, + month : monthFormatter, + // ******************* 时间 ******************* hour : value => toDate(value).getHours(), hour12 : value => {const hour = toDate(value).getHours(); return hour > 12 ? hour - 12 : thour}, minute : value => toDate(value).getMinutes(), second : value => toDate(value).getSeconds(), millisecond : value => toDate(value).getMilliseconds(), timestamp : value => toDate(value).getTime(), - // 货币 - // 常规货币形式 $111,233.33 - currency : (value, prefix = "$",suffix="", division = 3,precision = 2) => toCurrency(value, { division, prefix, precision,suffix }), + // ******************* 货币 ******************* + currency : currencyFormatter, // 数字,如,使用分割符 number : (value, division = 3,precision = 0) => toCurrency(value, { division, precision}) } \ No newline at end of file diff --git a/packages/runtime/formatters/zh.js b/packages/runtime/formatters/zh.js index 8ed91ab..dc89596 100644 --- a/packages/runtime/formatters/zh.js +++ b/packages/runtime/formatters/zh.js @@ -8,19 +8,35 @@ module.exports = { // 配置参数: 格式化器函数的最后一个参数就是该配置参数 - $options:{ + $config:{ datetime : { units : CN_DATETIME_UNITS, - weekdays : CN_WEEK_DAYS, - shortWeekdays : CN_SHORT_WEEK_DAYS, - monthNames : CN_MONTH_NAMES, - shorMonthNames: CN_SHORT_MONTH_NAMES + date :{ + long : 'YYYY年MM月DD日 HH点mm分ss秒', + short : "MM/DD", + format : 'YYYY年MM月DD日 HH点mm分ss秒' + }, + month:{ + names : CN_MONTH_NAMES, + shortNames : CN_SHORT_MONTH_NAMES, + format : 0, // 0-长名称,1-短名称,2-数字 + }, + weekday:{ + names : CN_WEEK_DAYS, + shortNames : CN_SHORT_WEEK_DAYS, + format : 0, // 0-长名称,1-短名称,2-数字 + }, + time:{ + long : "HH点mm分ss秒", + short : "HH:mm:ss", + } }, + currency : { unit : "¥", prefix : "", - suffix : "", - division : 3, + suffix : "元", + division : 4, precision : 2 }, number : { @@ -29,16 +45,10 @@ module.exports = { } }, $types: { - Date: value => {const d = toDate(value);return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日 ${d.getHours()}点${d.getMinutes()}分${d.getSeconds()}秒`} + Date: value => {const d = toDate(value);return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日 ${d.getHours()}点${d.getMinutes()}分${d.getSeconds()}秒`}, + Boolean : value =>value ? "是":"否" + }, - // 日期 - date : value => { const d = toDate(value); return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日` }, - weekday : value => CN_WEEK_DAYS[toDate(value).getDay()], - shortWeekday : value => CN_SHORT_WEEK_DAYS[toDate(value).getDay()], - monthName : value => CN_MONTH_NAMES[toDate(value).getMonth()], - shorMonthName: value => CN_SHORT_MONTH_NAMES[toDate(value).getMonth()], - // 时间 - time : value =>{const d = toDate(value);return `${d.getHours()}点${d.getMinutes()}分${d.getSeconds()}秒`}, // 货币 currency : (value,prefix = "¥",suffix="", division = 4, precision = 2) => toCurrency(value, { division, prefix, precision,suffix }), // 中文货币,big=true代表大写形式 diff --git a/packages/runtime/index.js b/packages/runtime/index.js index 56571ec..f6ba24d 100644 --- a/packages/runtime/index.js +++ b/packages/runtime/index.js @@ -271,7 +271,7 @@ function getFormatter(scope,activeLanguage,name){ resetScopeCache(scope,activeLanguage) } const fallbackLanguage = scope.getLanguage(activeLanguage).fallback - // 2. 先在当前作用域中查找,再在全局查找 formatters={$types,$options,[格式化器名称]:()=>{},[格式化器名称]:()=>{}} + // 2. 先在当前作用域中查找,再在全局查找 formatters={$types,$config,[格式化器名称]:()=>{},[格式化器名称]:()=>{}} const range = [ scope.activeFormatters, scope.formatters[fallbackLanguage], // 如果指定了回退语言时,也在该回退语言中查找 @@ -305,9 +305,9 @@ function executeChecker(checker,value){ /** * 执行格式化器并返回结果 * - * 格式化器this指向当前scope,并且最后一个参数是当前scope格式化器的$options + * 格式化器this指向当前scope,并且最后一个参数是当前scope格式化器的$config * - * 这样格式化器可以读取$options + * 这样格式化器可以读取$config * * @param {*} value * @param {Array[Function]} formatters 多个格式化器函数(经过包装过的)顺序执行,前一个输出作为下一个格式化器的输入 @@ -655,7 +655,7 @@ function translate(message) { get activeLanguage(){ return this._settings.activeLanguage} // 当前激活语言 名称 get defaultLanguage(){ return this._settings.defaultLanguage} // 默认语言名称 get languages(){ return this._settings.languages} // 支持的语言列表 - get formatters(){ return this._settings.formatters } // 内置格式化器{*:{$options,$types,...},zh:{$options,$types,...},en:{$options,$types,...}} + get formatters(){ return this._settings.formatters } // 内置格式化器{*:{$config,$types,...},zh:{$config,$types,...},en:{$config,$types,...}} get defaultMessageLoader(){ return this._defaultMessageLoader} // 默认语言包加载器 // 通过默认加载器加载文件 diff --git a/packages/runtime/scope.js b/packages/runtime/scope.js index 98d10ea..39aef6c 100644 --- a/packages/runtime/scope.js +++ b/packages/runtime/scope.js @@ -25,7 +25,7 @@ module.exports = class i18nScope { this._default = options.default; // 默认语言包 this._messages = options.messages; // 当前语言包 this._idMap = options.idMap; // 消息id映射列表 - this._formatters = options.formatters; // 当前作用域的格式化函数列表{: {$types,$options,[格式化器名称]: () => {},[格式化器名称]: () => {}}} + this._formatters = options.formatters; // 当前作用域的格式化函数列表{: {$types,$config,[格式化器名称]: () => {},[格式化器名称]: () => {}}} this._loaders = options.loaders; // 异步加载语言文件的函数列表 this._global = null; // 引用全局VoerkaI18n配置,注册后自动引用 this._patchMessages = {}; // 语言包补丁信息{: {....},:{....}} @@ -62,8 +62,8 @@ module.exports = class i18nScope { 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,$options,[格式化器名称]: () = >{},[格式化器名称]: () => {}}} - get activeFormatters() {return this._activeFormatters} // 当前作用域激活的格式化器定义 {$types,$options,[格式化器名称]: () = >{},[格式化器名称]: () = >{}} + get formatters() { return this._formatters;} // 当前作用域的所有格式化器定义 {<语言名称>: {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () => {}}} + get activeFormatters() {return this._activeFormatters} // 当前作用域激活的格式化器定义 {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () = >{}} get activeFormatterOptions(){return this._activeFormatterOptions} // 当前格式化器合并后的配置参数,参数已经合并了全局格式化器中的参数 /** @@ -86,10 +86,12 @@ module.exports = class i18nScope { registerFormatter(name,value=>{...},{langauge:["zh","cht"]}) // 注册到zh和cht语言 registerFormatter(name,value=>{...},{langauge:"zh,cht"}) * @param {*} formatter 格式化器 - language : 声明该格式化器适用语言 + language : 字符串或数组,声明该格式化器适用语言 + *代表适用于所有语言 + 语言名称,语言名称数组,或者使用,分割的语言名称字符串 asGlobal : 注册到全局 */ - registerFormatter(name, formatter, { language = "*", asGlobal } = {}) { + registerFormatter(name, formatter, { language = "*", global : asGlobal } = {}) { if (!isFunction(formatter) || typeof name !== "string") { throw new TypeError("Formatter must be a function"); } @@ -110,6 +112,20 @@ module.exports = class i18nScope { }); } } + /** + * 注册多种格式化器 + * registerFormatters(={"*",zh:{...},en:{...}}) + * registerFormatters(={"*",zh:{...},en:{...}},true) 在全局注册 + * @param {*} formatters ={"*",zh:{...},en:{...}} + * @returns + */ + registerFormatters(formatters,isGlobal=false) { + Object.entries(formatters).forEach(([language,fns]=>{ + Object.entries(fns).forEach(([name,formatter])=>{ + this.registerFormatter(name,formatter,{language}) + }) + })) + } /** * 注册默认文本信息加载器 * @param {Function} 必须是异步函数或者是返回Promise @@ -144,7 +160,7 @@ module.exports = class i18nScope { /** * 初始化格式化器 * 激活和默认语言的格式化器采用静态导入的形式,而没有采用异步块的形式,这是为了确保首次加载时的能马上读取,而减少延迟加载 - * _activeFormatters={$options:{...},$types:{...},[格式化器名称]:()=>{...},[格式化器名称]:()=>{...},...}} + * _activeFormatters={$config:{...},$types:{...},[格式化器名称]:()=>{...},[格式化器名称]:()=>{...},...}} */ _initFormatters(newLanguage){ this._activeFormatters = {} @@ -167,7 +183,7 @@ module.exports = class i18nScope { * 当切换语言时,格式化器应该切换到对应语言的格式化器 * * 重要需要处理: - * $options参数采用合并继承机制 + * $config参数采用合并继承机制 * * * @param {*} language @@ -192,19 +208,19 @@ module.exports = class i18nScope { } /** * 生成格式化器的配置参数,该参数由以下合并而成: - * - global.formatters[*].$options - * - global.formatters[language].$options - * - scope.activeFormatters.$options 当前优先 + * - global.formatters[*].$config + * - global.formatters[language].$config + * - scope.activeFormatters.$config 当前优先 */ _generateFormatterOptions(language){ let options try{ - options = Object.assign({},getByPath(this._global.formatters,`*.$options`,{})) - deepMixin(options,getByPath(this._global.formatters,`${language}.$options`,{})) - deepMixin(options,getByPath(this._activeFormatters,"$options",{})) + options = Object.assign({},getByPath(this._global.formatters,`*.$config`,{})) + deepMixin(options,getByPath(this._global.formatters,`${language}.$config`,{})) + deepMixin(options,getByPath(this._activeFormatters,"$config",{})) }catch(e){ if(this.debug) console.error(`Error while generate <${language}> formatter options: `,e) - if(!options) options = this._activeFormatters.$options || {} + if(!options) options = this._activeFormatters.$config || {} } return this._activeFormatterOptions = options } diff --git a/packages/runtime/utils.js b/packages/runtime/utils.js index 14e33c6..4af3e51 100644 --- a/packages/runtime/utils.js +++ b/packages/runtime/utils.js @@ -151,12 +151,12 @@ function toNumber(value,defualt=0) { * * @param {*} value 可以是数字也可以是字符串 * @param {*} division 分割符号位数,3代表每3个数字添加一个,号 - * @param {*} prefix 前缀,货币单位 - * @param {*} suffix 前缀,货币单位 + * @param {*} prefix 前缀 + * @param {*} suffix 后缀 * @param {*} precision 小数点精确到几位,0-自动 * @returns */ - function toCurrency(value,{division=3,prefix="",precision=0,suffix=""}={}){ + function toCurrency(value,{unit="",division=3,prefix="",precision=0,suffix=""}={}){ let [wholeValue,decimalValue] = String(value).split(".") let result = [] for(let i=0;i 12 ? hourNum - 12 : hourNum).padStart(2, "0")], // 01-12 12小时,两位数 + ["h", String(hourNum > 12 ? hourNum - 12 : hourNum)], // 1-12 12小时 + ["mm", minute.padStart(2, "0")], // 00-59 分钟,两位数 + ["m", minute], // 0-59 分钟 + ["ss", second.padStart(2, "0")], // 00-59 秒,两位数 + ["s", second], // 0-59 秒 + ["SSS", millisecond], // 000-999 毫秒,三位数 + ["SS", millisecond.substring(year.length - 2, year.length)], // 00-99 毫秒(十),两位数 + ["S",millisecond[millisecond.length - 1]], // 0-9 毫秒(百),一位数 + ["A", hour > 12 ? "PM" : "AM"], // AM / PM 上/下午,大写 + ["a", hour > 12 ? "pm" : "am"], // am / pm 上/下午,小写 + ] + vars.forEach(([key,value])=>result = replaceAll(result,key,value)) + return result +} +function formatTime(value,templ="HH:mm:ss"){ + const v = toDate(value) + const hourNum = v.getHours() + const hour = String(hourNum).minute = String(v.getMinutes()),second = String(v.getSeconds()),millisecond=String(v.getMilliseconds()) + let result = templ + const vars = [ + ["HH", hour.padStart(2, "0")], // 00-23 24小时,两位数 + ["H", hour], // 0-23 24小时 ["hh", String(hour > 12 ? hour - 12 : hour).padStart(2, "0")], // 01-12 12小时,两位数 - ["h", hour > 12 ? hour - 12 : hour], // 1-12 12小时 + ["h", String(hour > 12 ? hour - 12 : hour)], // 1-12 12小时 ["mm", minute.padStart(2, "0")], // 00-59 分钟,两位数 ["m", minute], // 0-59 分钟 ["ss", second.padStart(2, "0")], // 00-59 秒,两位数 @@ -250,10 +273,9 @@ function formatDatetime(value,templ="YYYY/MM/DD HH:mm:ss"){ ["A", hour > 12 ? "PM" : "AM"], // AM / PM 上/下午,大写 ["a", hour > 12 ? "pm" : "am"] // am / pm 上/下午,小写 ] - vars.forEach(([key,value])=>result = result.replace(key,value)) + vars.forEach(([key,value])=>result = replaceAll(result,key,value)) return result } - /** * 替换所有字符串 * 低版本ES未提供replaceAll,此函数用来替代 @@ -282,10 +304,23 @@ function replaceAll(str,findValue,replaceValue){ * * - 函数第一个参数是上一上格式化器的输出 * - 支持0-N个简单类型的入参 - * - 格式化器可以在格式化器的$options参数指定一个键值来配置不同语言时的参数 + * - 格式化器可以在格式化器的$config参数指定一个键值来配置不同语言时的参数 * - * createFormatter((value,prefix,suffix, division ,precision,options)=>{ - * + * "currency":createFormatter((value,prefix,suffix, division ,precision,options)=>{ + * // 无论在格式化入参数是多少个,经过处理后在此得到prefix,suffix, division ,precision参数已经是经过处理后的参数 + * 依次读取格式化器的参数合并: + * - 从当前激活格式化器的$config中读取配置参数 + * - 在t函数后传入参数 + * 比如currency格式化器支持4参数,其入参顺序是prefix,suffix, division ,precision + * 那么在t函数中可以使用以下五种入参数方式 + * {value | currency } //prefix=undefined,suffix=undefined, division=undefined ,precision=undefined + * {value | currency(prefix) } + * {value | currency(prefix,suffix) } + * {value | currency(prefix,suffix,division) } + * {value | currency(prefix,suffix,division,precision)} + * + * 经过createFormatter处理后,会从当前激活格式化器的$config中读取prefix,suffix, division ,precision参数作为默认参数 + * 然后t函数中的参数会覆盖默认参数,优先级更高 * }, * { * unit:"$", @@ -295,8 +330,9 @@ function replaceAll(str,findValue,replaceValue){ * precision * }, * { + * normalize:value=>{...}, * params:["prefix","suffix", "division" ,"precision"] // 声明参数顺序 - * optionKey:"currency" // 声明特定语言下的配置在$options.currency + * configKey:"currency" // 声明特定语言下的配置在$config.currency * } * ) * @@ -309,13 +345,13 @@ function replaceAll(str,findValue,replaceValue){ let opts = Object.assign({ normalize : null, // 对输入值进行规范化处理,如进行时间格式化时,为了提高更好的兼容性,支持数字时间戳/字符串/Date等,需要对输入值进行处理,如强制类型转换等 params : [], // 声明参数顺序 - optionKeyPath: null // 声明该格式化器在$options中的路径,支持简单的使用.的路径语法 + configKey : null // 声明该格式化器在$config中的路径,支持简单的使用.的路径语法 }) // 最后一个参数是传入activeFormatterOptions参数 const wrappedFn = function(value,...args){ let finalValue = value - // 1. 输入值规范处理,主要的类型转换等 + // 1. 输入值规范处理,主要是进行类型转换,确保输入的数据类型及相关格式的正确性,提高数据容错性 if(isFunction(opts.normalize)){ try{ finalValue = opts.normalize(finalValue) @@ -325,16 +361,16 @@ function replaceAll(str,findValue,replaceValue){ let activeFormatterOpts = args.length>0 ? args[args.length-1] : {} if(!isPlainObject( activeFormatterOpts)) activeFormatterOpts ={} // 3. 从当前语言的激活语言中读取配置参数 - const activeOptions =Object.assign({},defaultParams,getByPath(activeFormatterOpts,opts.optionKey,{})) - let finalArgs = opts.params.map(param=>getByPath(activeOptions,param,undefined)) - // 4. 将翻译函数执行格式化器时传入的参数具有高优先级 + const activeConfig =Object.assign({},defaultParams,getByPath(activeFormatterOpts,opts.configKey,{})) + let finalArgs = opts.params.map(param=>getByPath(activeConfig,param,undefined)) + // 4. 将翻译函数执行格式化器时传入的参数覆盖默认参数 for(let i =0; i=args.length-1) break // 最后一参数是配置 if(args[i]!==undefined) finalArgs[i] = args[i] } return fn(finalValue,...finalArgs,activeFormatterOpts) } - fn.paramCount = opts.paramCount + wrappedFn.configurable = true // 当函数是可配置时才在最后一个参数中传入$config return wrappedFn } @@ -352,6 +388,8 @@ module.exports ={ replaceAll, getByPath, getDataTypeName, + formatDatetime, + formatTime, toDate, toNumber, toCurrency, diff --git a/test/translate.test.js b/test/translate.test.js index 20e1a48..3584f84 100644 --- a/test/translate.test.js +++ b/test/translate.test.js @@ -1,7 +1,7 @@ const dayjs = require('dayjs'); -const { getInterpolatedVars, replaceInterpolatedVars , translate} = require('../packages/runtime/index.js') - -const messages = { +const { i18nScope, translate, getInterpolatedVars } = require('../packages/runtime/dist/runtime.cjs') + +const loaders = { zh:{ 1:"你好", 2:"现在是{}", @@ -16,88 +16,60 @@ const messages = { } } + +const formatters = { + zh:{ + $config:{}, + $types:{}, + book:(v)=>`《${v}》`, + }, + en:{ + $config:{}, + $types:{ }, + book:(v)=>`<${v}>`, + }, +} const idMap = { - "你好":1, - "现在是{}":2, - "我出生于{year}年,今年{age}岁":3, - "我有{}个朋友":4 + 1:"你好", + 2:"现在是{}", + 3:"我出生于{year}年,今年{age}岁" + 4:"我有{}个朋友" } +const languages = [ + { name: "zh", title: "中文" }, + { name: "en", title: "英文" }, + { name: "de", title: "德语" }, + { name: "jp", title: "日语" } +] +const scope = new i18nScope({ + id : "test", + defaultLanguage: "zh", + activeLanguage : "zh", + namespaces : {}, + default : loaders.zh, // 默认语言包 + messages : loaders.zh, // 当前语言包 + languages, // 语言配置 + idMap, // 消息id映射列表 + formatters, // 扩展自定义格式化器 + loaders // 语言包加载器 +}) -let scope1 ={ - defaultLanguage: "zh", // 默认语言名称 - default: messages.zh, - messages :messages.zh, - idMap, - formatters:{ // 当前作用域的格式化函数列表 - "*":{ - $types:{ - Date:(v)=>dayjs(v).format('YYYY-MM-DD HH:mm:ss'), - Boolean:(v)=>v?"True":"False", - }, - sum:(v,n=1)=>v+n, - double:(v)=>v*2, - upper:(v)=>v.toUpperCase(), - lower:(v)=>v.toLowerCase() - }, - zh:{ - $types:{ - Date:(v)=>dayjs(v).format('YYYY年MM月DD日 HH点mm分ss秒'), - Boolean:(v)=>v?"是":"否", - }, - book:(v)=>`《${v}》`, - }, - en:{ - $types:{ +const t = translate.bind(scope) - }, - book:(v)=>`<${v}>`, - }, - }, - loaders:{}, // 异步加载语言文件的函数列表 - global:{// 引用全局VoerkaI18n配置 - defaultLanguage: "zh", - activeLanguage: "zh", - languages:[ - {name:"zh",title:"中文",default:true}, - {name:"en",title:"英文"}, - {name:"de",title:"德语"}, - {name:"jp",title:"日语"} - ], - formatters:{ // 当前作用域的格式化函数列表 - "*":{ - $types:{ - - } - }, - zh:{ - $types:{ - - } - }, - en:{ - $types:{ - - } - }, - } - } -} - -const replaceVars = replaceInterpolatedVars.bind(scope1) -const t = translate.bind(scope1) - - -function changeLanguage(language){ - scope1.global.activeLanguage = language - scope1.messages = messages[language] - -} +// 适用于所有语言的格式化器,并且注册到全局 +scope.registerFormatters({ + "*":{ + sum : (v,n=1)=>v+n, + double: (v)=>v*2, + upper : (v)=>v.toUpperCase(), + } +},true) beforeEach(() => { - scope1.global.activeLanguage = "zh" // 切换到中文 + scope.change("zh") }); @@ -121,7 +93,6 @@ test("获取翻译内容中的插值变量",done=>{ expect(results[1].name).toEqual("city"); expect(results[1].formatters.length).toEqual(0); - done() }) @@ -136,7 +107,6 @@ test("获取翻译内容中定义了重复的插值变量",done=>{ expect(results[1].formatters[0].name).toEqual("x"); expect(results[1].formatters[0].args).toEqual([]); - expect(results[2].name).toEqual("a"); expect(results[2].formatters.length).toEqual(2); expect(results[2].formatters[0].name).toEqual("x"); @@ -148,45 +118,45 @@ test("获取翻译内容中定义了重复的插值变量",done=>{ }) test("替换翻译内容的位置插值变量",done=>{ - expect(replaceVars("{}{}{}",1,2,3)).toBe("123"); - expect(replaceVars("{a}{b}{c}",1,2,3)).toBe("123"); + expect(t("{}{}{}",1,2,3)).toBe("123"); + expect(t("{a}{b}{c}",1,2,3)).toBe("123"); // 定义了一些无效的格式化器,直接忽略 - expect(replaceVars("{a|xxx}{b|dd}{c|}",1,2,3)).toBe("123"); - expect(replaceVars("{a|xxx}{b|dd}{c|}",1,2,3,4,5,6)).toBe("123"); - expect(replaceVars("{ a|}{b|dd}{c|}{}",1,2,3)).toBe("123{}"); + expect(t("{a|xxx}{b|dd}{c|}",1,2,3)).toBe("123"); + expect(t("{a|xxx}{b|dd}{c|}",1,2,3,4,5,6)).toBe("123"); + expect(t("{ a|}{b|dd}{c|}{}",1,2,3)).toBe("123{}"); // 中文状态下true和false被转换成中文的"是"和"否" - expect(replaceVars("{}{}{}",1,"2",true)).toBe("12是"); - expect(replaceVars("{|double}{}{}",1,"2",true)).toBe("22是"); + expect(t("{}{}{}",1,"2",true)).toBe("12是"); + expect(trim("{|double}{}{}",1,"2",true)).toBe("22是"); done() }) test("替换翻译内容的命名插值变量",done=>{ - expect(replaceVars("{a}{b}{c}",{a:11,b:22,c:33})).toBe("112233"); - expect(replaceVars("{a}{b}{c}{a}{b}{c}",{a:1,b:"2",c:3})).toBe("123123"); + expect(t("{a}{b}{c}",{a:11,b:22,c:33})).toBe("112233"); + expect(t("{a}{b}{c}{a}{b}{c}",{a:1,b:"2",c:3})).toBe("123123"); done() }) test("命名插值变量使用格式化器",done=>{ // 提供无效的格式化器,直接忽略 - expect(replaceVars("{a|x}{b|x|y}{c|}",{a:1,b:2,c:3})).toBe("123"); - expect(replaceVars("{a|x}{b|x|y}{c|double}",{a:1,b:2,c:3})).toBe("126"); + expect(t("{a|x}{b|x|y}{c|}",{a:1,b:2,c:3})).toBe("123"); + expect(t("{a|x}{b|x|y}{c|double}",{a:1,b:2,c:3})).toBe("126"); // 默认的字符串格式化器,不需要定义使用字符串方法 - expect(replaceVars("{a|x}{b|x|y}{c|double}",{a:1,b:2,c:3})).toBe("126"); + expect(t("{a|x}{b|x|y}{c|double}",{a:1,b:2,c:3})).toBe("126"); // padStart格式化器是字符串的方法,不需要额外定义可以直接使用 - expect(replaceVars("{a|padStart(10)}",{a:"123"})).toBe(" 123"); - expect(replaceVars("{a|padStart(10)|trim}",{a:"123"})).toBe("123"); + expect(t("{a|padStart(10)}",{a:"123"})).toBe(" 123"); + expect(t("{a|padStart(10)|trim}",{a:"123"})).toBe("123"); done() }) test("命名插值变量使用格式化器",done=>{ // 提供无效的格式化器,直接忽略 - expect(replaceVars("{a|x}{b|x|y}{c|}",{a:1,b:2,c:3})).toBe("123"); - expect(replaceVars("{a|x}{b|x|y}{c|double}",{a:1,b:2,c:3})).toBe("126"); + expect(t("{a|x}{b|x|y}{c|}",{a:1,b:2,c:3})).toBe("123"); + expect(t("{a|x}{b|x|y}{c|double}",{a:1,b:2,c:3})).toBe("126"); // 默认的字符串格式化器,不需要定义使用字符串方法 - expect(replaceVars("{a|x}{b|x|y}{c|double}",{a:1,b:2,c:3})).toBe("126"); - expect(replaceVars("{a|padStart(10)}",{a:"123"})).toBe(" 123"); - expect(replaceVars("{a|padStart(10)|trim}",{a:"123"})).toBe("123"); + expect(t("{a|x}{b|x|y}{c|double}",{a:1,b:2,c:3})).toBe("126"); + expect(t("{a|padStart(10)}",{a:"123"})).toBe(" 123"); + expect(t("{a|padStart(10)|trim}",{a:"123"})).toBe("123"); done() }) @@ -194,11 +164,11 @@ test("命名插值变量使用格式化器",done=>{ test("切换到其他语言时的自动匹配同名格式化器",done=>{ // 默认的字符串类型的格式化器 - expect(replaceVars("{a}",{a:true})).toBe("是"); - expect(replaceVars("{name|book}是毛泽东思想的重要载体","毛泽东选集")).toBe("《毛泽东选集》是毛泽东思想的重要载体"); + expect(t("{a}",{a:true})).toBe("是"); + expect(t("{name|book}是毛泽东思想的重要载体","毛泽东选集")).toBe("《毛泽东选集》是毛泽东思想的重要载体"); changeLanguage("en") - expect(replaceVars("{a}",{a:false})).toBe("False"); - expect(replaceVars("{name|book}是毛泽东思想的重要载体","毛泽东选集")).toBe("<毛泽东选集>是毛泽东思想的重要载体"); + expect(t("{a}",{a:false})).toBe("False"); + expect(t("{name|book}是毛泽东思想的重要载体","毛泽东选集")).toBe("<毛泽东选集>是毛泽东思想的重要载体"); done() })