diff --git a/.umirc.ts b/.umirc.ts index f588ed5..463708a 100644 --- a/.umirc.ts +++ b/.umirc.ts @@ -14,13 +14,13 @@ export default defineConfig({ }, locales: [['zh-CN', '中文']], scripts:[` - var _hmt = _hmt || []; - (function() { - var hm = document.createElement("script"); - hm.src = "https://hm.baidu.com/hm.js?06dc18a1ffc1c69ab1445ba8020ded5b"; - var s = document.getElementsByTagName("script")[0]; - s.parentNode.insertBefore(hm, s); - })(); + var _hmt = _hmt || []; + (function() { + var hm = document.createElement("script"); + hm.src = "https://hm.baidu.com/hm.js?14df7a33942125aa14ad60ee1cdb1940"; + var s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); + })(); `], navs:[ { diff --git a/packages/runtime/formatters/en.js b/packages/runtime/formatters/en.js index cc51056..c9348fd 100644 --- a/packages/runtime/formatters/en.js +++ b/packages/runtime/formatters/en.js @@ -3,9 +3,7 @@ * */ - const { toDate,toCurrency,toNumber,formatDatetime,formatTime,Formatter } = require("../utils") - - + const { toDate,toCurrency,toNumber,isPlainObject,formatDatetime,formatTime,Formatter } = require("../utils") /** * 日期格式化器 @@ -123,26 +121,44 @@ const timeFormatter = Formatter((value,format,$config)=>{ * { value | currency({symbol,unit,prefix,precision,suffix}) } */ const currencyFormatter = Formatter((value,...args) =>{ + // 1. 最后一个参数是格式化器的参数,不同语言不一样 + let $config = args[args.length-1] + // 2. 从语言配置中读取默认参数 let params = { unit : 0, - radix : $config.radix, // 进制,即三位一进制,中文是是4位一进 - symbol : $config.symbol, // 符号 + radix : $config.radix, // 进制,取值,0-4, + symbol : $config.symbol, // 符号,即三位一进制,中文是是4位一进 prefix : $config.prefix, // 前缀 suffix : $config.suffix, // 后缀 division : $config.division, // ,分割位 precision : $config.precision, // 精度 format : $config.format, // 模板字符串 - } - let $config = args[args.length-1] - if(args.length==1) { + } + // 3. 从格式化器中传入的参数具有最高优先级,覆盖默认参数 + if(args.length==1) { // 无参调用 Object.assign(params,{format:'default'}) - }else if(args.length==2 && isPlainObject(args[0])){ + }else if(args.length==2 && isPlainObject(args[0])){ // 一个参数且是{} Object.assign(params,args[0]) - }else if(args.length==2){ + }else if(args.length==2){ + // 一个字符串参数,只能是default,long,short, 或者是一个模板字符串,如"{symbol}{value}{unit}" Object.assign(params,{format:args[0]}) - }else{ + }else if(args.length==3){// 2个参数,分别是format,unit Object.assign(params,{format:args[0],unit:args[1]}) + }else if(args.length==4){// 2个参数,分别是format,unit,precision + Object.assign(params,{format:args[0],unit:args[1],precision:args[2]}) + } + // 4. 检查参数正确性 + params.unit = parseInt(params.unit) || 0 + if(params.unit>4) params.unit = 4 + if(params.unit<0) params.unit = 0 + // 当指定unit大于0时取消小数点精度控制 + // 例 value = 12345678.99 默认情况下精度是2,如果unit=1,则显示1234.47+, + // 将params.precision=0取消精度限制就可以显示1234.567899万,从而保证完整的精度 + // 除非显示将precision设置为>2的值 + if(params.unit>0 && params.precision==2){ + params.precision = 0 } + // 模板字符串 if(params.format in $config){ params.format = $config[params.format] @@ -188,17 +204,16 @@ module.exports = { }, currency : { default : "{symbol}{value}{unit}", - long : "{prefix}{symbol}{value}{unit}{suffix}", + long : "{prefix} {symbol}{value}{unit}{suffix}", short : "{symbol}{value}{unit}", - auto : "{symbol}{value} {unit}", //-- - units : ["","Thousands","Millions","Billions","Trillions"], //千,百万,十亿,万亿 + units : [""," thousands"," millions"," billions"," trillions"], //千,百万,十亿,万亿 radix : 3, // 进制,即三位一进制,中文是是4位一进 symbol : "$", // 符号 - prefix : "", // 前缀 + prefix : "USD", // 前缀 suffix : "", // 后缀 division : 3, // ,分割位 - precision : 2, // 精度 + precision : 2, // 精度 }, number : { diff --git a/packages/runtime/formatters/zh.js b/packages/runtime/formatters/zh.js index 9c268ba..7574242 100644 --- a/packages/runtime/formatters/zh.js +++ b/packages/runtime/formatters/zh.js @@ -42,7 +42,7 @@ module.exports = { units : ["","万","亿","万亿","万万亿"], radix : 4, // 进制,即三位一进制,中文是是4位一进 symbol : "¥", - prefix : "", + prefix : "RMB", suffix : "元", division : 4, precision : 2 diff --git a/packages/runtime/interpolate.js b/packages/runtime/interpolate.js index 9d6fa67..464d179 100644 --- a/packages/runtime/interpolate.js +++ b/packages/runtime/interpolate.js @@ -41,8 +41,8 @@ // 插值变量字符串替换正则 let varReplaceRegexp =String.raw`\{\s*{varname}\s*\}` // 提取匹配("a",1,2,'b',{..},[...]) - const formaterVarsRegexp = String.raw`((([\'\"])(.*?)\3)|(\w)|(\{.*?\})|(\[.*?\]))(?<=\s*[,\)]?\s*)` - + const formatterParamsRegex = /((([\'\"])(.*?)\3)|(\{.*?\})|(\[.*?\])|([\d]+\.?[\d]?)|((true|false|null)(?=[,\b\s]))|([\w\.]+)|((?<=,)\s*(?=,)))(?<=\s*[,\)]?\s*)/g; + /** * 考虑到通过正则表达式进行插值的替换可能较慢 * 因此提供一个简单方法来过滤掉那些不需要进行插值处理的字符串 @@ -84,6 +84,7 @@ let result = formatters.trim().substr(1).trim().split("|").map(r=>r.trim()) // 2. 解析格式化器参数 return result.map(formatter=>{ + if(formatter=="") return null let firstIndex = formatter.indexOf("(") let lastIndex = formatter.lastIndexOf(")") if(firstIndex!==-1 && lastIndex!==-1){ // 带参数的格式化器 @@ -96,7 +97,7 @@ }else{// 不带参数的格式化器 return [formatter,[]] } - }) + }).filter(formatter=> Array.isArray(formatter)) } /** * 解析格式化器的参数 @@ -186,7 +187,7 @@ function parseFormaterParams(strParams) { * @param {Function(<变量名称>,[formatters],match[0])} callback * @returns 返回替换后的字符串 */ - function forEachInterpolatedVars(str,callback,options={}){ + function forEachInterpolatedVars(str,replacer,options={}){ let result=str, match let opts = Object.assign({ replaceAll:true, // 是否替换所有插值变量,当使用命名插值时应置为true,当使用位置插值时应置为false @@ -196,9 +197,9 @@ function parseFormaterParams(strParams) { const varname = match.groups.varname || "" // 解析格式化器和参数 = [,[,[,,...]]] const formatters = parseFormatters(match.groups.formatters) - if(isFunction(callback)){ + if(isFunction(replacer)){ try{ - const finalValue = callback(varname,formatters,match[0]) + const finalValue = replacer(varname,formatters,match[0]) if(opts.replaceAll){ // 在某此版本上可能没有 result=result.replaceAll(match[0],finalValue) }else{ diff --git a/packages/runtime/utils.js b/packages/runtime/utils.js index e13baa2..8c05e4b 100644 --- a/packages/runtime/utils.js +++ b/packages/runtime/utils.js @@ -59,8 +59,8 @@ function isNothing(value){ } -// 区配JSON字符串里面的非标的key,即key没有使用"字符包起来的键 -const bastardJsonKeyRegex = /(([\w\u4e00-\u9fa5])|(\'.*?\'))+(?=\s*\:)/g +// 区配JSON字符串里面的非标的key,即key没有使用"字符包起来的键 +const bastardJsonKeyRegex = /((?<=:\s*)(\'.*?\')+)|((([\w\u4e00-\u9fa5])|(\'.*?\'))+(?=\s*\:))/g /** * 格式化器中的{a:1,b:2}形式的参数由于是非标准的JSON格式,采用JSON.parse会出错 * 如果使用eval转换则存在安全隐患 @@ -74,12 +74,11 @@ function safeParseJson(str){ if (matched.index === bastardJsonKeyRegex.lastIndex) { bastardJsonKeyRegex.lastIndex++; } - str = str.replace(new RegExp(`${matched[0]}\s*:`),key=>{ - key = key.substring(0,key.length-1).trim() + str = str.replace(new RegExp(matched[0]),key=>{ if(key.startsWith("'") && key.endsWith("'")){ key = key.substring(1,key.length-1) } - return `"${key}" :` + return `"${key}"` }) } return JSON.parse(str) @@ -183,36 +182,40 @@ function toNumber(value,defualt=0) { * @returns */ function toCurrency(value,params={}){ - const {symbol="",division=3,prefix="",precision=0,suffix="",unit=0,unitName="",radix=3,format="{symbol}{value}{unit}"} = params + let {symbol="",division=3,prefix="",precision=2,suffix="",unit=0,unitName="",radix=3,format="{symbol}{value}{unit}"} = params // 1. 分离出整数和小数部分 - let [wholeValue,decimalValue] = String(value).split(".") - + let [wholeDigits,decimalDigits] = String(value).split(".") // 2. 转换数制单位 比如将元转换到万元单位 // 如果指定了unit单位,0-代表默认,1-N代表将小数点字向后移动radix*unit位 // 比如 123456789.88 - // 当unit=1,radix=3时, == [123456,78988] - // 当unit=1,radix=3时, == [123,45678988] + // 当unit=1,radix=3时, == [123456,78988] // [整数,小数] + // 当unit=2,radix=3时, == [123,45678988] // [整数,小数] if(unit>0 && radix>0){ // 不足位数时补零 - if(wholeValue.length0) result.push(",") - result.push(wholeValue[i]) + for(let i=0;i0) result.push(",") + result.push(wholeDigits[i]) } // 4. 处理保留小数位数,即精度 - if(decimalValue){ - if(precision>0){ - decimalValue = String(parseFloat(`0.${decimalValue}`).toFixed(precision)).split(".")[1] + if(decimalDigits){ + // 如果precision是一个数字,则进行四舍五入处理 + if(isNumber(precision) && precision>0){// 四舍五入处理 + let finalBits = decimalDigits.length // 四舍五入前的位数 + decimalDigits = String(parseFloat(`0.${decimalDigits}`).toFixed(precision)).split(".")[1] + //如果经过四舍五入处理后的位数小于,代表精度进行舍去,则未尾显示+符号 + if(finalBits > decimalDigits.length) decimalDigits+="+" } - result.push(`.${decimalValue}`) + result.push(`.${decimalDigits}`) } result = result.join("") // 5. 模板替换 @@ -369,9 +372,10 @@ function replaceAll(str,findValue,replaceValue){ * * - 函数第一个参数是上一上格式化器的输出 * - 支持0-N个简单类型的入参 + * - 可以是定参,也可以变参 * - 格式化器可以在格式化器的$config参数指定一个键值来配置不同语言时的参数 * - * "currency":createFormatter((value,prefix,suffix, division ,precision,options)=>{ + * "currency":createFormatter((value,prefix,suffix, division ,precision,$config)=>{ * // 无论在格式化入参数是多少个,经过处理后在此得到prefix,suffix, division ,precision参数已经是经过处理后的参数 * 依次读取格式化器的参数合并: * - 创建格式化时的defaultParams参数 diff --git a/test/translate.test.js b/test/translate.test.js index 234d468..4bd016f 100644 --- a/test/translate.test.js +++ b/test/translate.test.js @@ -13,6 +13,7 @@ function toLanguageIdMap(values,startIndex=0){ return result },{}) } +// 显示两个数组哪一行不同 function diffArray(arr1,arr2){ let diffs = [] arr1.forEach((v,i)=>{ @@ -214,7 +215,7 @@ const expectEnDatetimes =[ "Now time: 36", // { value | time('ss') }" ] -const MONEY = 123456789.8848 +const MONEY = 123456789.88 const zhMoneys = [ "商品价格: { value | currency}", // 默认格式 // long @@ -230,49 +231,67 @@ const zhMoneys = [ "商品价格: { value | currency('short',3)}", // 短格式 Billions "商品价格: { value | currency('short',4)}", // 短格式 Trillions - // // 自定义货币格式 - // "商品价格: { value | currency({symbol:'#¥'})}", - // "商品价格: { value | currency({symbol:'#¥',prefix:'人民币'})}", - // "商品价格: { value | currency({symbol:'#¥',prefix:'人民币',suffix:'整'})}", - // "商品价格: { value | currency({symbol:'#¥',prefix:'人民币',suffix:'整',precision:4})}", - // "商品价格: { value | currency({symbol:'#¥',suffix:'整',precision:4, unit:2})}", + // 自定义货币格式 + "商品价格: { value | currency({symbol:'¥¥'})}", + "商品价格: { value | currency({symbol:'¥¥',prefix:'人民币:'})}", + "商品价格: { value | currency({symbol:'¥¥',prefix:'人民币:',suffix:'元整'})}", + "商品价格: { value | currency({symbol:'¥¥',prefix:'人民币:',suffix:'元整',unit:2})}", + "商品价格: { value | currency({symbol:'¥¥',prefix:'人民币:',suffix:'元整',unit:2,precision:4})}" ] const expectZhMoneys =[ - "商品价格: ¥1,2345,6789.88", // { value | currency } + "商品价格: ¥1,2345,6789.88", // { value | currency } // long - "商品价格: ¥1,2345,6789.88元", // { value | currency('long')} - "商品价格: ¥1,2345,68万元", // { value | currency('long',1)} - "商品价格: ¥1.23亿元", // { value | currency('long',2)} - "商品价格: ¥0.00万亿元", // { value | currency('long',3)} - "商品价格: ¥0.00万万亿元", // { value | currency('long',4)} + "商品价格: RMB ¥1,2345,6789.88元", // { value | currency('long')} + "商品价格: RMB ¥1,2345.678988万元", // { value | currency('long',1)} + "商品价格: RMB ¥1.2345678988亿元", // { value | currency('long',2)} + "商品价格: RMB ¥0.00012345678988万亿元", // { value | currency('long',3)} + "商品价格: RMB ¥0.000000012345678988万万亿元", // { value | currency('long',4)} // short - "商品价格: ¥1,2345,6789.88", // { value | currency('short')} - "商品价格: ¥1,2345,68万", // { value | currency('short',1)} - "商品价格: ¥1.23亿", // { value | currency('short',2)} - "商品价格: ¥0.00万亿", // { value | currency('short',3)} - "商品价格: ¥0.00万万亿", // { value | currency('short',4)} + "商品价格: ¥1,2345,6789.88", // { value | currency('short')} + "商品价格: ¥1,2345.678988万", // { value | currency('short',1)} + "商品价格: ¥1.2345678988亿", // { value | currency('short',2)} + "商品价格: ¥0.00012345678988万亿", // { value | currency('short',3)} + "商品价格: ¥0.000000012345678988万万亿", // { value | currency('short',4)} + // 自定义货币格式 + "商品价格: ¥¥1,2345,6789.88", + "商品价格: 人民币:¥¥1,2345,6789.88", + "商品价格: 人民币:¥¥1,2345,6789.88元整", + "商品价格: 人民币:¥¥1,2345.678988万元", + "商品价格: 人民币:¥¥1.2346亿元整" ] + + + const enMoneys = [ - "Price: { value | currency }", // 默认格式,由语言配置指定,不同的语言不一样 - "Price: { value | currency('long')}", // 长格式 - "Price: { value | currency('short')}", // 短格式 - "Price: { value | currency('$')}", // 指定货币符号 - "Price: { value | currency('$','USD')}", // 指定货币符号+前缀 - "Price: { value | currency('$','USD','')}", // 指定货币符号+前缀+后缀 - "Price: { value | currency('$','USD','',3)}", // 指定货币符号+前缀+后缀+分割位 - "Price: { value | currency('$','USD','',3,3)}", // 指定货币符号+前缀+后缀+分割位+精度 + "Price: { value | currency }", // 默认格式 + // long + "Price: { value | currency('long') }", // 长格式 + "Price: { value | currency('long',1) }", // 长格式: 万元 + "Price: { value | currency('long',2) }", // 长格式: 亿 + "Price: { value | currency('long',3) }", // 长格式: 万亿 + "Price: { value | currency('long',4) }", // 长格式: 万万亿 + // short + "Price: { value | currency('short') }", // 短格式 + "Price: { value | currency('short',1) }", // 短格式 Thousands + "Price: { value | currency('short',2) }", // 短格式 Millions + "Price: { value | currency('short',3) }", // 短格式 Billions + "Price: { value | currency('short',4) }", // 短格式 Trillions ] const expectEnMoneys =[ - "Price: $123,456,789.88", // { value | currency } - "Price: $123,456,789.88", // { value | currency('long')} - "Price: $123,456,789.88", // { value | currency('short')} - "Price: $123,456,789.88", // { value | currency('$')} - "Price: USD$1,2345,6789.88", // { value | currency('$','USD')} - "Price: USD$1,2345,6789.88", // { value | currency('$','USD','元')} - "Price: USD$123,456,789.885", // { value | currency('$','USD','元',3)} - "Price: USD$123,456,789.885", //{ value | currency('$','USD','元',3,3)} - + "Price: $123,456,789.88", // { value | currency } + // long + "Price: USD $123,456,789.88", // { value | currency('long')} + "Price: USD $123,456.78988 thousands", // { value | currency('long',1)} + "Price: USD $123.45678988 millions", // { value | currency('long',2)} + "Price: USD $0.12345678988 billions", // { value | currency('long',3)} + "Price: USD $0.00012345678988 trillions", // { value | currency('long',4)} + // short + "Price: $123,456,789.88", // { value | currency('short')} + "Price: $123,456.78988 thousands", // { value | currency('short',1)} + "Price: $123.45678988 millions", // { value | currency('short',2)} + "Price: $0.12345678988 billions", // { value | currency('short',3)} + "Price: $0.00012345678988 trillions", // { value | currency('short',4)} ] @@ -452,30 +471,31 @@ test("切换到其他语言时的自动匹配同名格式化器",async ()=>{ test("位置插值翻译文本内容",async ()=>{ const now = new Date() expect(t("你好")).toBe("你好"); - expect(t("现在是{ value | }",now)).toBe(`现在是${dayjs(now).format('YYYY年MM月DD日 HH点mm分ss秒')}`); + expect(t("现在是{ value | }",now)).toBe(`现在是${dayjs(now).format('YYYY/M/D HH:mm:ss')}`); // 经babel自动码换后,文本内容会根据idMap自动转为id expect(t("1")).toBe("你好"); - expect(t("2",now)).toBe(`现在是${dayjs(now).format('YYYY年MM月DD日 HH点mm分ss秒')}`); + expect(t("2",now)).toBe(`现在是${dayjs(now).format('YYYY/M/D HH:mm:ss')}`); await scope.change("en") expect(t("你好")).toBe("hello"); - expect(t("现在是{ value | }",now)).toBe(`Now is ${dayjs(now).format('YYYY/MM/DD HH:mm:ss')}`); + expect(t("现在是{ value | }",now)).toBe(`Now is ${dayjs(now).format('YYYY/M/D HH:mm:ss')}`); expect(t("1")).toBe("hello"); - expect(t("2",now)).toBe(`Now is ${dayjs(now).format('YYYY/MM/DD HH:mm:ss')}`); + expect(t("2",now)).toBe(`Now is ${dayjs(now).format('YYYY/M/D HH:mm:ss')}`); }) test("命名插值翻译文本内容",async ()=>{ const now = new Date() expect(t("你好")).toBe("你好"); - expect(t("现在是{ value | }",now)).toBe(`现在是${dayjs(now).format('YYYY年MM月DD日 HH点mm分ss秒')}`); + expect(t("现在是{ value | }",now)).toBe(`现在是${dayjs(now).format('YYYY/M/D HH:mm:ss')}`); await scope.change("en") expect(t("你好")).toBe("hello"); - expect(t("现在是{ value | }",now)).toBe(`Now is ${dayjs(now).format('YYYY/MM/DD HH:mm:ss')}`); + expect(t("现在是{ value | }",now)).toBe(`Now is ${dayjs(now).format('YYYY/M/D HH:mm:ss')}`); + // 使用idMap expect(t("1")).toBe("hello"); - expect(t("2",now)).toBe(`Now is ${dayjs(now).format('YYYY/MM/DD HH:mm:ss')}`); + expect(t("2",now)).toBe(`Now is ${dayjs(now).format('YYYY/M/D HH:mm:ss')}`); }) @@ -511,12 +531,10 @@ test("日期时间格式化器",async ()=>{ }) -// test("货币格式化器",async ()=>{ - - -// let zhTranslatedResults = zhDatetimes.map(v=>t(v,NOW)) -// expect(zhTranslatedResults).toStrictEqual(expectZhDatetimes) -// await scope.change("en") -// let enTranslatedResults = zhDatetimes.map(v=>t(v,NOW)) -// expect(enTranslatedResults).toStrictEqual(expectEnDatetimes) -// }) + test("货币格式化器",async ()=>{ + let zhMoneysResults = zhMoneys.map(v=>t(v,MONEY)) + expect(zhMoneysResults).toStrictEqual(expectZhMoneys) + await scope.change("en") + let enMoneysResults = enMoneys.map(v=>t(v,MONEY)) + expect(enMoneysResults).toStrictEqual(expectEnMoneys) + })