From b8f59c43bce01552386699d5ea245aea94162cf4 Mon Sep 17 00:00:00 2001 From: wxzhang Date: Fri, 12 Aug 2022 18:08:45 +0800 Subject: [PATCH] update formatters --- .umirc.ts | 2 +- packages/runtime/formatters/common.js | 158 +++++++++++++++++++++++++- packages/runtime/formatters/en.js | 18 +++ packages/runtime/index.js | 100 ++++++++++------ packages/runtime/scope.js | 46 +++----- packages/runtime/utils.js | 79 +++++++++++-- 6 files changed, 325 insertions(+), 78 deletions(-) diff --git a/.umirc.ts b/.umirc.ts index 18a3caa..856bf1a 100644 --- a/.umirc.ts +++ b/.umirc.ts @@ -7,7 +7,7 @@ export default defineConfig({ base:"/voerka-i18n/", publicPath:"/voerka-i18n/", mode: 'site', - logo: "/images/i18n.png", + logo: "/voerka-i18n/images/i18n.png", outputPath:"docs/dist", resolve:{ includes:["docs/src"] diff --git a/packages/runtime/formatters/common.js b/packages/runtime/formatters/common.js index 07b22fa..8dd3279 100644 --- a/packages/runtime/formatters/common.js +++ b/packages/runtime/formatters/common.js @@ -1,4 +1,7 @@ - /** +import { toNumber,isFunction } from "../utils" + + +/** * 字典格式化器 * 根据输入data的值,返回后续参数匹配的结果 * dict(data,,,,,,,...) @@ -34,7 +37,156 @@ if (args.length > 0 && (args.length % 2 !== 0)) return args[args.length - 1] return value } - + +/** + * + * 空值: null,undefined + * + * 当输入空值时的行为 + * + * { value | empty } == 转换显示为'' + * { value | empty('无') } == 无 + * { value | unit('KB') | empty('0') } == 0KB + * + * 有时在处理其他类型时,可能希望将0或者''也视为空值 + * { value | empty('没钱了') } == + * + * + * @param {*} value + * @param {String} escapeValue + * @param {*} options + */ + function empty(value,escapeValue,next) { + if(next===true) next = 'break' + let opts = Object.assign({escape:"",next:'ignore'},options.empty || {}) + if(!escapeValue) opts.escape = escapeValue + let emptyValues = [undefined,null] + if(Array.isArray(opts.values)) emptyValues.push(...opts.values) + if(emptyValues.includes(value)){ + return {value:opts.escape,next: opts.next} + }else{ + return value + } +} +empty.paramCount = 2 + +/** +* 当执行格式化器出错时的显示内容. + +{ value | error } == +{ value | error('') } == 显示空字符串 +{ value | error('ERROR') } == 显示ERROR字样 +{ value | error('ERROR:{ message}') } == 显示error.message +{ value | error('ERROR:{ error}') } == 显示error.constructor.name + +this--> scope实例 + + * @param {*} value + * @param {*} escapeValue + * @param {*} next 下一步的行为,取值,break,ignore + * @param {*} options 格式化器的全局配置参数 + * @returns + */ +function error(value,escapeValue,next,options) { + + if(value instanceof Error){ + try{ + let opts = Object.assign({escape:"",next:'break'},options.error || {}) + if(!escapeValue) opts.escape = escapeValue + if(!next) opts.next = next + return { + value : String(opts.escape).replace(/\{\s*message\s*\}/g,value.message) + .replace(/\{\s*error\s*\}/g,value.constructor.name) + next : opts.next + } + }cache(e){ + if(this.debug) console.error(`Error while execute formatter: ${e.message}`) + } + }else{ + return value + } +} +error.paramCount = 2 // 声明该格式化器支持两个参数 + +/** + * 添加前缀 + * @param {*} value + * @param {*} prefix + * @returns + */ +function prefix(value,prefix="") { + return prefix ? `${prefix}${value}` : value +} +/** + * 添加后缀 + * @param {*} value + * @param {*} suffix + * @returns + */ +function suffix(value,suffix="") { + return suffix ? `${value}${suffix}` : value +} + + const FILE_SIZE_SECTIONS = [ + 0, + 1024, + 1048576, + 1073741824, + 1099511627776, + 1125899906842624, + 1152921504606847000, + 1.1805916207174113e+21, + 1.2089258196146292e+24, + 1.2379400392853803e+27, + 1.2676506002282294e+30 + ] +const FILE_SIZE_BRIEF_UNITS = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB","NB","DB"] +const FILE_SIZE_WHOLE_UNITS = ["Bytes", "Kilobytes", "Megabytes", "Gigabytes", "TeraBytes", "PetaBytes", "ExaBytes", "ZetaBytes", "YottaBytes","DoggaBytes"] + //const getSizePoint= index=>index ==0 ? 0 : new Array(index).fill(0).reduce((pv,cv)=>pv*1024,1) + +/** + * 输出文件大小 + * + * { value | fileSize } + * { value | fileSize('KB') } + * { value | fileSize('MB') } + * + * @param {*} value + * @param {*} unit 单位,未指定时采用自动方式,即<1024用字节,1024v=FILE_SIZE_BRIEF_UNITS.length) unitIndex= 0 + let result = (unitIndex == 0 ? v : v / FILE_SIZE_SECTIONS[unitIndex]).toFixed(opts.precision) + if( unitIndex>0 && (v % FILE_SIZE_SECTIONS[unitIndex])!==0) result = result+"+" + // 去除尾部的0 + while(["0","."].includes(result[result.length-1])){ + result = result.substring(0, result.length-2) + } + return brief ? `${result} ${opts.brief[unitIndex]}` : `${result} ${opts.brief[whole]}` +} +fileSize.paramCount = 2 +fileSize.escape = 0 + module.exports = { - dict + dict, + prefix, + suffix, + filesize, + error, + empty } \ No newline at end of file diff --git a/packages/runtime/formatters/en.js b/packages/runtime/formatters/en.js index c4558d2..0762aed 100644 --- a/packages/runtime/formatters/en.js +++ b/packages/runtime/formatters/en.js @@ -5,6 +5,9 @@ const { toDate,toCurrency } = require("../utils") + + + module.exports = { // 配置参数: 格式化器函数的最后一个参数就是该配置参数 $options:{ @@ -25,6 +28,21 @@ number : { division : 3, precision : 2 + }, + empty:{ + //values : [], // 可选,定义空值,如果想让0,''也为空值,可以指定values=[0,''] + escape : "", // 当空值时显示的备用值 + next : null // 当空值时下一步的行为: break=中止;ignore=忽略 + }, + error : { + //当错误时显示的内容,支持的插值变量有message=错误信息,error=错误类名,也可以是一个返回上面内容的同步函数 + escape : "", // 默认当错误时显示空内容 + next : null // 当出错时下一步的行为: break=中止;ignore=忽略 + }, + fileSize:{ + //brief: ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB","NB","DB"], + //whole:["Bytes", "Kilobytes", "Megabytes", "Gigabytes", "TeraBytes", "PetaBytes", "ExaBytes", "ZetaBytes", "YottaBytes","DoggaBytes"], + //precision: 2 // 小数精度 } }, // 默认数据类型的格式化器 diff --git a/packages/runtime/index.js b/packages/runtime/index.js index 79fc29a..b11eda7 100644 --- a/packages/runtime/index.js +++ b/packages/runtime/index.js @@ -288,28 +288,51 @@ function getFormatter(scope,activeLanguage,name){ /** * 执行格式化器并返回结果 + * + * 格式化器this指向当前scope,并且最后一个参数是当前scope格式化器的$options + * + * 这样格式化器可以读取$options + * * @param {*} value - * @param {*} formatters 多个格式化器顺序执行,前一个输出作为下一个格式化器的输入 + * @param {Array[Function]} formatters 多个格式化器函数(经过包装过的)顺序执行,前一个输出作为下一个格式化器的输入 */ function executeFormatter(value,formatters,scope){ if(formatters.length===0) return value let result = value - try{ - for(let formatter of formatters){ - if(isFunction(formatter)) { - result = formatter.call(scope,result) - }else{// 如果碰到无效的格式化器,则跳过过续的格式化器 - return result + + const emptyChecker = formatters.find(func=>func.$name==='empty') + const errorChecker = formatters.find(func=>func.$name==='error') + + // 当输入是一个空值时 + if(emptyChecker){ + const { value,next } = emptyChecker(result) + if(next == 'break') return value + } + if(result instanceof Error && errorChecker){ + const { value,next } = errorChecker(result) + if(next == 'break') return value + } + + for(let formatter of formatters){ + try{ + result = formatter(result) + }catch(e){ + if(scope.debug) console.error(`Error while execute i18n formatter<${formatter.$name}> for ${value}: ${e.message} ` ) + const { value,next } = errorChecker(e) + if(next=="break"){ + if(value!==undefined) result = value + break + }else if(next=="ignore"){ + continue } - } - }catch(e){ - //console.error(`Error while execute i18n formatter for ${value}: ${e.message} ` ) - } + if(value!==undefined) result = value + } + } return result } /** * - * 将 [[格式化器名称,[参数,参数,...]],[格式化器名称,[参数,参数,...]]]格式化器转化为 + * 将[[格式化器名称,[参数,参数,...]],[格式化器名称,[参数,参数,...]]]格式化器包装转化为 * 格式化器的调用函数链 * * @param {*} scope @@ -318,29 +341,41 @@ function executeFormatter(value,formatters,scope){ * @returns {Array} [(v)=>{...},(v)=>{...},(v)=>{...}] * */ -function buildFormatters(scope,activeLanguage,formatters){ - let results = [] - for(let formatter of formatters){ - if(formatter[0]){ - const func = getFormatter(scope,activeLanguage,formatter[0]) +function wrapperFormatters(scope,activeLanguage,formatters){ + let wrappedFormatters = [] + for(let [name,args] of formatters){ + if(name){ + const func = getFormatter(scope,activeLanguage,name) if(isFunction(func)){ - results.push((v)=>{ - return func(v,...formatter[1]) - }) + let fn = (value) => { + // 每一个格式式化器均支持若干输入参数,但是在使用时,可以支持可选参数 + // 比currency(value,prefix,suffix, division,precision)支持4个参数,但是在使用时支持可选参数 + // {value | currency} 或 {value | currency('元')} 或 {value | currency('元',"整")} + // 为了让格式化器能比较方便地处理最后一个配置参数,当格式化器函数指定了paramCount参数来声明其支持的参数后 + // 在此就自动初始参数个数,这样在currency函数中,就可以使用这样的currency(value,prefix,suffix, division,precision,options) + // 不管开发者使用时的输入参数数是多少均可以保证在currency函数中总是可以得到有效的options参数 + if(func.paramCount && args.length < func.paramCount ){ + args.push(...new Array(parseInt(func.paramCount)-args.length).fill(undefined)) + } + return func.call(scope,value,...args,scope.activeFormatterOptions) + } + fn.$name = name + wrappedFormatters.push(fn) }else{ // 格式化器无效或者没有定义时,查看当前值是否具有同名的原型方法,如果有则执行调用 // 比如padStart格式化器是String的原型方法,不需要配置就可以直接作为格式化器调用 - results.push((v)=>{ - if(isFunction(v[formatter[0]])){ - return v[formatter[0]].call(v,...formatter[1]) + wrappedFormatters.push((value)=>{ + if(isFunction(value[name])){ + // 最后一个参数是当前作用域的格式化器配置参数 + return value[name](value,...args) }else{ - return v + return value } }) } } } - return results + return wrappedFormatters } /** @@ -353,14 +388,15 @@ function buildFormatters(scope,activeLanguage,formatters){ */ 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.length==0){ - formatterFuncs.splice(0,0,defaultFormatter) - } + const formatterFuncs = wrapperFormatters(scope,activeLanguage,formatters) // 3. 执行格式化器 + if(formatterFuncs.length==0){ + // 当没有格式化器时,查询是否指定了默认数据类型的格式化器,如果有则执行 + const defaultFormatter = getDataTypeDefaultFormatter(scope,activeLanguage,getDataTypeName(value)) + if(defaultFormatter){ + return executeFormatter(value,[defaultFormatter],scope) + } + } value = executeFormatter(value,formatterFuncs,scope) return value } diff --git a/packages/runtime/scope.js b/packages/runtime/scope.js index fc0e71a..8268712 100644 --- a/packages/runtime/scope.js +++ b/packages/runtime/scope.js @@ -57,49 +57,29 @@ module.exports = class i18nScope { this.register(callback); } // 作用域 - get id() { - return this._id; - } + get id() {return this._id;} // 调试开关 - get debug() { - return this._debug; - } + get debug() {return this._debug;} // 默认语言名称 - get defaultLanguage() { - return this._defaultLanguage; - } + get defaultLanguage() {return this._defaultLanguage;} // 默认语言名称 - get activeLanguage() { - return this._activeLanguage; - } + get activeLanguage() {return this._activeLanguage;} // 默认语言包 - get default() { - return this._default; - } + get default() {return this._default;} // 当前语言包 - get messages() { - return this._messages; - } + get messages() {return this._messages; } // 消息id映射列表 - get idMap() { - return this._idMap; - } + get idMap() {return this._idMap;} // 当前作用域的格式化器 {:{$types,$options,[格式化器名称]:()=>{},[格式化器名称]:()=>{}}} - get formatters() { - return this._formatters; - } + get formatters() { return this._formatters;} // 当前作用域支持的语言列表[{name,title,fallback}] - get languages() { - return this._languages; - } + get languages() {return this._languages;} // 异步加载语言文件的函数列表 - get loaders() { - return this._loaders; - } + get loaders() { return this._loaders;} // 引用全局VoerkaI18n配置,注册后自动引用 - get global() { - return this._global; - } + get global() { return this._global;} + // 当前格式化器配置参数 + get activeFormatterOptions(){return this._activeFormatterOptions} /** * 在全局注册作用域 * @param {*} callback 注册成功后的回调 diff --git a/packages/runtime/utils.js b/packages/runtime/utils.js index b7cbe58..06ae20d 100644 --- a/packages/runtime/utils.js +++ b/packages/runtime/utils.js @@ -197,16 +197,77 @@ function getByPath(obj,path,defaultValue){ return cur } -/** - * 返回value相对rel的相对时间 - * - * 如:12分钟前, 6秒前, 1小时 - * - * - * - */ -function relativeTime(value, rel){ +// YY 18 年,两位数 +// YYYY 2018 年,四位数 +// M 1-12 月,从1开始 +// MM 01-12 月,两位数字 +// MMM Jan-Dec 月,英文缩写 +// D 1-31 日 +// DD 01-31 日,两位数 +// H 0-23 24小时 +// HH 00-23 24小时,两位数 +// h 1-12 12小时 +// hh 01-12 12小时,两位数 +// m 0-59 分钟 +// mm 00-59 分钟,两位数 +// s 0-59 秒 +// ss 00-59 秒,两位数 +// S 0-9 毫秒(百),一位数 +// SS 00-99 毫秒(十),两位数 +// SSS 000-999 毫秒,三位数 +// Z -05:00 UTC偏移 +// ZZ -0500 UTC偏移,两位数 +// A AM / PM 上/下午,大写 +// a am / pm 上/下午,小写 +// Do 1st... 31st 月份的日期与序号 + +function formatDatetime(value,templ="YYYY/MM/DD HH:mm:ss"){ + const v = toDate(value) + const year =String(v.getFullYear()),month = String(v.getMonth()+1),weekday=String(v.getDay()),day=String(v.getDate()) + const minute = String(v.getMinutes()),second = String(v.getSeconds()),millisecond=String(v.getMilliseconds()),hour = String(v.getHours()) + const time = String(v.getTime()) + let result = templ + const vars = [ + ["YYYY", year], // 2018 年,四位数 + ["YY", year.substring(year.length - 2, year.length)], // 18 年,两位数 + ["MMM", ""], // Jan-Dec 月,英文缩写 + ["MM", month.padStart(2, "0")], // 01-12 月,两位数字 + ["M", month], // 1-12 月,从1开始 + ["DD", day.padStart(2, "0")], // 01-31 日,两位数 + ["D", day], // 1-31 日 + ["HH", String(hour).padStart(2, "0")], // 00-23 24小时,两位数 + ["H", String(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小时 + ["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 = result.replace(key,value)) + return result +} + + +/** + * 创建格式化器 + */ +function createFormatter(fn,meta={}){ + let opts = Object.assign({ + checker : false, // true时代表该格式化器是作为校验器存在,会在执行每一次格式化器函数后调用进行检查 + paramCount : 0, // 声明该该格式化器具有几个参数,0代表未知 + error : 0, // 当执行格式化化器函数出错时的行为,取值0-break,1-skip,2-default + empty : false, // 当输入为空时的输出 + default : null // 当出错默认输出 + },meta) + Object.entries(opts).forEach(([key,value])=>fn[key] = value) + return fn } module.exports ={