diff --git a/.umirc.ts b/.umirc.ts index 1197aa0..abca023 100644 --- a/.umirc.ts +++ b/.umirc.ts @@ -1,6 +1,15 @@ import { defineConfig } from 'dumi'; // more config: https://d.umijs.org/config + export default defineConfig({ title: 'VoerkaI18n全流程国际化解决方案', diff --git a/packages/runtime/formatters/en.js b/packages/runtime/formatters/en.js index b681e77..cc51056 100644 --- a/packages/runtime/formatters/en.js +++ b/packages/runtime/formatters/en.js @@ -115,11 +115,42 @@ const timeFormatter = Formatter((value,format,$config)=>{ }) // 货币格式化器, CNY $13,456.00 -const currencyFormatter = Formatter((value, symbol,prefix ,suffix, division,precision) =>{ - return toCurrency(value, { symbol,division, prefix, precision,suffix }) +/** + * { value | currency } + * { value | currency('long') } + * { value | currency('long',1) } 万元 + * { value | currency('long',2) } 亿元 + * { value | currency({symbol,unit,prefix,precision,suffix}) } + */ +const currencyFormatter = Formatter((value,...args) =>{ + let params = { + unit : 0, + radix : $config.radix, // 进制,即三位一进制,中文是是4位一进 + symbol : $config.symbol, // 符号 + 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) { + Object.assign(params,{format:'default'}) + }else if(args.length==2 && isPlainObject(args[0])){ + Object.assign(params,args[0]) + }else if(args.length==2){ + Object.assign(params,{format:args[0]}) + }else{ + Object.assign(params,{format:args[0],unit:args[1]}) + } + // 模板字符串 + if(params.format in $config){ + params.format = $config[params.format] + } + params.unitName =(Array.isArray($config.units) && params.unit> 0 && params.unit<$config.units.length) ? $config.units[params.unit] : "" + return toCurrency(value,params) },{ - normalize: toNumber, - params:["symbol","prefix","suffix", "division","precision"], + normalize: toNumber, configKey: "currency" }) @@ -156,10 +187,13 @@ module.exports = { }, }, currency : { - units : ["","Thousands","Millions","Billions","Trillions"], //千,百万,十亿,万亿 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"], //千,百万,十亿,万亿 + radix : 3, // 进制,即三位一进制,中文是是4位一进 symbol : "$", // 符号 prefix : "", // 前缀 suffix : "", // 后缀 diff --git a/packages/runtime/formatters/zh.js b/packages/runtime/formatters/zh.js index ef2aa2f..9c268ba 100644 --- a/packages/runtime/formatters/zh.js +++ b/packages/runtime/formatters/zh.js @@ -39,7 +39,8 @@ module.exports = { }, currency : { - units : ["万","亿","万亿","万万亿"] + units : ["","万","亿","万亿","万万亿"], + radix : 4, // 进制,即三位一进制,中文是是4位一进 symbol : "¥", prefix : "", suffix : "元", diff --git a/packages/runtime/index.js b/packages/runtime/index.js index 85c08de..a3c7980 100644 --- a/packages/runtime/index.js +++ b/packages/runtime/index.js @@ -1,515 +1,13 @@ -const {createFormatter,Formatter,getDataTypeName,isNumber,isPlainObject,deepMerge,isFunction,isNothing,deepMixin,replaceAll} = require("./utils") +const {createFormatter,Formatter,getDataTypeName,isNumber,isPlainObject,isFunction,isNothing,deepMerge,deepMixin} = require("./utils") +const {getInterpolatedVars,replaceInterpolatedVars} = require("./interpolate") const EventEmitter = require("./eventemitter") const inlineFormatters = require("./formatters") const i18nScope = require("./scope") +const { translate } = require("./translate") -// 用来提取字符里面的插值变量参数 , 支持管道符 { 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 - -// 插值变量字符串替换正则 - -//let varReplaceRegexp =String.raw`\{\s*(?{name}\.?\w*)\s*\}` - - -let varReplaceRegexp =String.raw`\{\s*{varname}\s*\}` - -// 提取匹配("a",1,2,'b',{..},[...]) -let formaterVarsRegexp = String.raw`((([\'\"])(.*?)\3)|(\w)|(\{.*?\})|(\[.*?\]))(?<=\s*[,\)]?\s*)` - -/** - * 考虑到通过正则表达式进行插件的替换可能较慢,因此提供一个简单方法来过滤掉那些 - * 不需要进行插值处理的字符串 - * 原理很简单,就是判断一下是否同时具有{和}字符,如果有则认为可能有插值变量,如果没有则一定没有插件变量,则就不需要进行正则匹配 - * 从而可以减少不要的正则匹配 - * 注意:该方法只能快速判断一个字符串不包括插值变量 - * @param {*} str - * @returns {boolean} true=可能包含插值变量, - */ -function hasInterpolation(str){ - return str.includes("{") && str.includes("}") -} const DataTypes = ["String","Number","Boolean","Object","Array","Function","Error","Symbol","RegExp","Date","Null","Undefined","Set","Map","WeakSet","WeakMap"] - -/** - 使用正则表达式对原始文本内容进行解析匹配后得到的便以处理的数组 - - formatters="| aaa(1,1) | bbb " - - 统一解析为 - - [ - [aaa,[1,1]], // [formatter'name,[args,...]] - [<格式化器名称>,[<参数>,<参数>,...]] - ] - - 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() - // 此处采用,分割参数,存在的问题,如果参数里面存在,会导致分解析参数错误,TODO: 采用正则表达式进行解析 - let args = argsContent=="" ? [] : argsContent.split(",").map(arg=>{ - arg = arg.trim() - if(isNumber(arg)){ // 数字 - return parseFloat(arg) - }else if((arg.startsWith('\"') && arg.endsWith('\"')) || (arg.startsWith('\'') && arg.endsWith('\'')) ){ - return arg.substr(1,arg.length-2) // 字符串 - }else if(["true" ,"false"].includes(arg.toLowerCase())){ - 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(isFunction(callback)){ - try{ - const finalValue = callback(varname,formatters,match[0]) - if(opts.replaceAll){ // 在某此版本上可能没有 - result=result.replaceAll(match[0],finalValue) - }else{ - result=result.replace(match[0],finalValue) - } - }catch{// callback函数可能会抛出异常,如果抛出异常,则中断匹配过程 - break - } - } - varWithPipeRegexp.lastIndex=0 - } - return result -} -/** - * 当传入的翻译内容不是一个字符串时,进行默认的转换 - * - * - 对函数则执行并取返回结果() - * - 对Array和Object使用JSON.stringify - * - 其他类型使用toString - * - * @param {*} value - * @returns - */ -function transformToString(value){ - let result = value - try{ - if(isFunction(result)) result = value() - if(!(typeof(result)==="string")){ - if(Array.isArray(result) || isPlainObject(result)){ - result = JSON.stringify(result) - }else{ - result = result.toString() - } - } - }catch{ - result = result.toString() - } - return result -} -/** - * 清空指定语言的缓存 - * @param {*} scope - * @param {*} activeLanguage - */ -function resetScopeCache(scope,activeLanguage=null){ - scope.$cache = {activeLanguage,typedFormatters:{},formatters:{}} -} -/** - * 取得指定数据类型的默认格式化器 - * - * 可以为每一个数据类型指定一个默认的格式化器,当传入插值变量时, - * 会自动调用该格式化器来对值进行格式化转换 - const formatters = { - "*":{ - $types:{...} // 在所有语言下只作用于特定数据类型的格式化器 - }, // 在所有语言下生效的格式化器 - zh:{ - $types:{ - [数据类型]:(value)=>{...} // 默认 - }, - [格式化器名称]:(value)=>{...}, - [格式化器名称]:(value)=>{...}, - [格式化器名称]:(value)=>{...}, - }, - en:{.....} - } - * @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 fallbackLanguage = scope.getLanguage(activeLanguage).fallback; - // 先在当前作用域中查找,再在全局查找 - const targets = [ - scope.activeFormatters, - scope.formatters[fallbackLanguage], // 如果指定了回退语言时,也在该回退语言中查找 - scope.global.formatters[activeLanguage], - scope.global.formatters["*"] - ] - for(const target of targets){ - if(!target) continue - if(isPlainObject(target.$types) && isFunction(target.$types[dataType])){ - return scope.$cache.typedFormatters[dataType] = target.$types[dataType] - } - } -} - -/** - * 获取指定名称的格式化器函数 - * - * 查找逻辑 - * - 在当前作用域中查找 - * - 在全局作用域中查找 - * - * @param {*} scope - * @param {*} activeLanguage 当前激活语言名称 - * @param {*} name 格式化器名称 - * @returns {Function} 格式化函数 - */ -function getFormatter(scope,activeLanguage,name){ - // 1. 从缓存中直接读取: 缓存格式化器引用,避免重复检索 - 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 fallbackLanguage = scope.getLanguage(activeLanguage).fallback - // 2. 先在当前作用域中查找,再在全局查找 formatters={$types,$config,[格式化器名称]:()=>{},[格式化器名称]:()=>{}} - const range = [ - scope.activeFormatters, - scope.formatters[fallbackLanguage], // 如果指定了回退语言时,也在该回退语言中查找 - scope.global.formatters[activeLanguage], // 适用于activeLanguage全局格式化器 - scope.global.formatters["*"], // 适用于所有语言的格式化器 - ] - for(const formatters of range){ - if(!formatters) continue - if(isFunction(formatters[name])) { - return scope.$cache.formatters[name] = formatters[name] - } - } -} -/** - * Checker是一种特殊的格式化器,会在特定的时间执行 - * - * Checker应该返回{value,next}用来决定如何执行下一个格式化器函数 - * - * - * @param {*} checker - * @param {*} value - * @returns - */ -function executeChecker(checker,value){ - let result ={ value, next:"skip"} - if(!isFunction(checker)) return result - try{ - const r = checker(value) - if(isPlainObject(r)) { - Object.assign(result,r) - }else{ - result.value = r - } - if(!["break","skip"].includes(result.next)) result.next="break" - }catch(e){ - - } - return result -} -/** - * 执行格式化器并返回结果 - * - * 格式化器this指向当前scope,并且最后一个参数是当前scope格式化器的$config - * - * 这样格式化器可以读取$config - * - * @param {*} value - * @param {Array[Function]} formatters 多个格式化器函数(经过包装过的)顺序执行,前一个输出作为下一个格式化器的输入 - */ -function executeFormatter(value,formatters,scope,template){ - if(formatters.length===0) return value - let result = value - // 1. 空值检查 - const emptyCheckerIndex = formatters.findIndex(func=>func.$name==='empty') - if(emptyCheckerIndex!=-1){ - const emptyChecker = formatters.splice(emptyCheckerIndex,1)[0] - const { value,next } = executeChecker(emptyChecker,result) - if(next == 'break') { - return value - }else{ - result = value - } - } - // 2. 错误检查 - const errorCheckerIndex = formatters.findIndex(func=>func.$name==='error') - let errorChecker - if(errorCheckerIndex!=-1){ - errorChecker = formatters.splice(errorCheckerIndex,1)[0] - if(result instanceof Error){ - result.formatter = formatter.$name - const { value,next } = executeChecker(errorChecker,result) - if(next == 'break') { - return value - }else{ - result = value - } - } - } - - // 3. 分别执行格式化器函数 - for(let formatter of formatters){ - try{ - result = formatter(result,scope.activeFormatterConfig) - }catch(e){ - e.formatter = formatter.$name - if(scope.debug) console.error(`Error while execute i18n formatter<${formatter.$name}> for ${template}: ${e.message} ` ) - if(isFunction(errorChecker)){ - const { value,next } = executeChecker(errorChecker,result) - if(next=="break"){ - if(value!==undefined) result = value - break - }else if(next=="skip"){ - continue - } - } - } - } - return result -} - - - -/** - * 添加默认的empty和error格式化器,用来提供默认的空值和错误处理逻辑 - * - * empty和error格式化器有且只能有一个,其他无效 - * - * @param {*} formatters - */ -function addDefaultFormatters(formatters){ - // 默认的空值处理逻辑: 转换为"",然后继续执行接下来的逻辑 - if(formatters.findIndex(([name])=>name=="empty")===-1){ - formatters.push(["empty",[]]) - } - // 默认的错误处理逻辑: 开启DEBUG时会显示ERROR:message;关闭DEBUG时会保持最近值不变然后中止后续执行 - if(formatters.findIndex(([name])=>name=="error")===-1){ - formatters.push(["error",[]]) - } -} - -/** - * - * 经parseFormatters解析t('{}')中的插值表达式中的格式化器后会得到 - * [[<格式化器名称>,[参数,参数,...]],[<格式化器名称>,[参数,参数,...]]]数组 - * - * 本函数将之传换为转化为调用函数链,形式如下: - * [(v)=>{...},(v)=>{...},(v)=>{...}] - * - * 并且会自动将当前激活语言的格式化器配置作为最后一个参数配置传入,这样格式化器函数就可以读取 - * - * @param {*} scope - * @param {*} activeLanguage - * @param {*} formatters - * @returns {Array} [(v)=>{...},(v)=>{...},(v)=>{...}] - * - */ -function wrapperFormatters(scope,activeLanguage,formatters){ - let wrappedFormatters = [] - addDefaultFormatters(formatters) - for(let [name,args] of formatters){ - let fn = getFormatter(scope,activeLanguage,name) - let formatter - // 格式化器无效或者没有定义时,查看当前值是否具有同名的原型方法,如果有则执行调用 - // 比如padStart格式化器是String的原型方法,不需要配置就可以直接作为格式化器调用 - if(isFunction(fn)){ - formatter = (value,config) => fn.call(scope,value,...args,config) - }else{ - formatter = (value) =>{ - if(isFunction(value[name])){ - return value[name](...args) - }else{ - return value - } - } - } - formatter.$name = name - wrappedFormatters.push(formatter) - } - return wrappedFormatters -} - -/** - * 将value经过格式化器处理后返回的结果 - * @param {*} scope - * @param {*} activeLanguage - * @param {*} formatters - * @param {*} value - * @returns - */ -function getFormattedValue(scope,activeLanguage,formatters,value,template){ - // 1. 取得格式化器函数列表,然后经过包装以传入当前格式化器的配置参数 - const formatterFuncs = wrapperFormatters(scope,activeLanguage,formatters) - // 3. 执行格式化器 - // EMPTY和ERROR是默认两个格式化器,如果只有两个则说明在t(...)中没有指定格式化器 - if(formatterFuncs.length==2){ - // 当没有格式化器时,查询是否指定了默认数据类型的格式化器,如果有则执行 - const defaultFormatter = getDataTypeDefaultFormatter(scope,activeLanguage,getDataTypeName(value)) - if(defaultFormatter){ - return executeFormatter(value,[defaultFormatter],scope,template) - } - }else{ - value = executeFormatter(value,formatterFuncs,scope,template) - } - 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 - let result=template - - // 没有变量插值则的返回原字符串 - 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,match)=>{ - let value = (varname in varValues) ? varValues[varname] : '' - return getFormattedValue(scope,activeLanguage,formatters,value,template) - }) - }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,match)=>{ - if(params.length>i){ - return getFormattedValue(scope,activeLanguage,formatters,params[i++],template) - }else{ - throw new Error() // 抛出异常,停止插值处理 - } - },{replaceAll:false}) - - } - return result -} - // 默认语言配置 const defaultLanguageSettings = { debug : true, @@ -522,119 +20,6 @@ const defaultLanguageSettings = { ] } -/** - * 文本id必须是一个数字 - * @param {*} content - * @returns - */ -function isMessageId(content){ - return isNumber(content) -} -/** - * 根据值的单数和复数形式,从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 - } -} - -/** - * 翻译函数 - * -* 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(isFunction(value)){ - 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 = isFunction(arg) ? 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 - let msgId = isMessageId(content) ? content : scope.idMap[content] - content = scope.messages[msgId] || 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 // 出错则返回原始文本 - } -} - /** * 多语言管理类 * @@ -780,7 +165,6 @@ module.exports ={ i18nScope, createFormatter, Formatter, - defaultLanguageSettings, getDataTypeName, isNumber, isNothing, diff --git a/packages/runtime/interpolate.js b/packages/runtime/interpolate.js new file mode 100644 index 0000000..9d6fa67 --- /dev/null +++ b/packages/runtime/interpolate.js @@ -0,0 +1,543 @@ +/** + * + * 处理翻译文本中的插件变量 + * + * 处理逻辑如下: + * + * 以"Now is { value | date | prefix('a') | suffix('b')}"为例: + * + * 1. 先判断一下输入是否包括{的}字符,如果是则说明可能存在插值变量,如果没有则说明一定不存在插值变量。 + * 这样做的目的是如果确认不存在插值变量时,就不需要后续的正则表表达式匹配提取过程。 + * 这对大部份没有采用插件变量的文本能提高性能。 + * 2. forEachInterpolatedVars采用varWithPipeRegexp正则表达式,先将文本提取出<变量名称>和<格式化器部分>, + * 即: + * 变量名称="value" + * formatters = "date | prefix('a') | suffix('b')" + * 3. 将"formatters"使用|转换为数组 ["date","prefix('a')","suffix('b')"] + * 4. parseFormatters依次对每一个格式化器进行遍历解析为: + * [ + * ["date",[]], + * ["prefix",['a']], + * ["suffix",['b']] + * ] + * 5. 然后wrapperFormatters从scope中读取对应的格式化器定义,将之转化为 + * [(value,config)=>{....},(value,config)=>{....},(value,config)=>{....}] + * 为优化性能,在从格式化器名称转换为函数过程中会进行缓存 + * 6. 最后只需要依次执行这些格式化化器函数即可 + * + * + */ + + + const {createFormatter,Formatter,getDataTypeName,isNumber,isPlainObject,isFunction,safeParseJson} = require("./utils") + const EventEmitter = require("./eventemitter") + const inlineFormatters = require("./formatters") + const i18nScope = require("./scope") + + + // 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter } + // 支持参数: { var | formatter(x,x,..) | formatter } + let varWithPipeRegexp = /\{\s*(?\w+)?(?(\s*\|\s*\w*(\(.*\)){0,1}\s*)*)\s*\}/g + // 插值变量字符串替换正则 + let varReplaceRegexp =String.raw`\{\s*{varname}\s*\}` + // 提取匹配("a",1,2,'b',{..},[...]) + const formaterVarsRegexp = String.raw`((([\'\"])(.*?)\3)|(\w)|(\{.*?\})|(\[.*?\]))(?<=\s*[,\)]?\s*)` + + /** + * 考虑到通过正则表达式进行插值的替换可能较慢 + * 因此提供一个简单方法来过滤掉那些不需要进行插值处理的字符串 + * 原理很简单,就是判断一下是否同时具有{和}字符,如果有则认为可能有插值变量,如果没有则一定没有插件变量,则就不需要进行正则匹配 + * 从而可以减少不要的正则匹配 + * 注意:该方法只能快速判断一个字符串不包括插值变量 + * @param {*} str + * @returns {boolean} true=可能包含插值变量 + */ + function hasInterpolation(str){ + return str.includes("{") && str.includes("}") + } + + /** + 使用正则表达式对原始文本内容进行解析匹配后得到的便以处理的数组 + + formatters="| aaa(1,1) | bbb " + + 统一解析为 + + [ + [aaa,[1,1]], // [formatter'name,[args,...]] + [<格式化器名称>,[<参数>,<参数>,...]] + ] + + 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 strParams = formatter.substr(firstIndex+1,lastIndex-firstIndex-1).trim() + // 解析出格式化的参数数组 + let params = parseFormaterParams(strParams) + // 返回[<格式化器名称>,[<参数>,<参数>,...] + return [formatter.substr(0,firstIndex),params] + }else{// 不带参数的格式化器 + return [formatter,[]] + } + }) + } + /** + * 解析格式化器的参数 + * + * 采用正则表达式解析,缺点是无法解析嵌套的{}和[],因此不能在格式化器参数中使用复杂嵌套格式的{}和[] + * + * @param {*} strParams + * @returns {Array} 返回参数值数组 [] + */ +function parseFormaterParams(strParams) { + let params = []; + let matched; + try{ + while ((matched = formatterParamsRegex.exec(strParams)) !== null) { + // 这对于避免零宽度匹配的无限循环是必要的 + if (matched.index === formatterParamsRegex.lastIndex) { + formatterParamsRegex.lastIndex++; + } + let value = matched[0] + if(value.trim()==''){ + value = null + }else if((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"'))){ + value = value.substring(1,value.length-1) + }else if((value.startsWith("{") && value.endsWith("}")) || (value.startsWith('[') && value.endsWith(']'))){ + try{ + value = safeParseJson(value) + }catch{} + }else if(["true","false","null"].includes(value)){ + value = JSON.parse(value) + }else if(isNumber(value)){ + value = parseFloat(value) + }else{ + value =String(value) + } + params.push(value) + } + }catch{ + + } + return params +} + /** + * 提取字符串中的插值变量 + * [ + // { + 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(isFunction(callback)){ + try{ + const finalValue = callback(varname,formatters,match[0]) + if(opts.replaceAll){ // 在某此版本上可能没有 + result=result.replaceAll(match[0],finalValue) + }else{ + result=result.replace(match[0],finalValue) + } + }catch{// callback函数可能会抛出异常,如果抛出异常,则中断匹配过程 + break + } + } + varWithPipeRegexp.lastIndex=0 + } + return result + } + + /** + * 清空指定语言的缓存 + * @param {*} scope + * @param {*} activeLanguage + */ +function resetScopeCache(scope,activeLanguage=null){ + scope.$cache = {activeLanguage,typedFormatters:{},formatters:{}} +} + + + /** + * 取得指定数据类型的默认格式化器 + * + * 可以为每一个数据类型指定一个默认的格式化器,当传入插值变量时, + * 会自动调用该格式化器来对值进行格式化转换 + const formatters = { + "*":{ + $types:{...} // 在所有语言下只作用于特定数据类型的格式化器 + }, // 在所有语言下生效的格式化器 + zh:{ + $types:{ + [数据类型]:(value)=>{...} // 默认 + }, + [格式化器名称]:(value)=>{...}, + [格式化器名称]:(value)=>{...}, + [格式化器名称]:(value)=>{...}, + }, + en:{.....} + } + * @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 fallbackLanguage = scope.getLanguage(activeLanguage).fallback; + // 先在当前作用域中查找,再在全局查找 + const targets = [ + scope.activeFormatters, + scope.formatters[fallbackLanguage], // 如果指定了回退语言时,也在该回退语言中查找 + scope.global.formatters[activeLanguage], + scope.global.formatters["*"] + ] + for(const target of targets){ + if(!target) continue + if(isPlainObject(target.$types) && isFunction(target.$types[dataType])){ + return scope.$cache.typedFormatters[dataType] = target.$types[dataType] + } + } + } + + /** + * 获取指定名称的格式化器函数 + * + * 查找逻辑 + * - 在当前作用域中查找 + * - 在全局作用域中查找 + * + * @param {*} scope + * @param {*} activeLanguage 当前激活语言名称 + * @param {*} name 格式化器名称 + * @returns {Function} 格式化函数 + */ + function getFormatter(scope,activeLanguage,name){ + // 1. 从缓存中直接读取: 缓存格式化器引用,避免重复检索 + 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 fallbackLanguage = scope.getLanguage(activeLanguage).fallback + // 2. 先在当前作用域中查找,再在全局查找 formatters={$types,$config,[格式化器名称]:()=>{},[格式化器名称]:()=>{}} + const range = [ + scope.activeFormatters, + scope.formatters[fallbackLanguage], // 如果指定了回退语言时,也在该回退语言中查找 + scope.global.formatters[activeLanguage], // 适用于activeLanguage全局格式化器 + scope.global.formatters["*"], // 适用于所有语言的格式化器 + ] + for(const formatters of range){ + if(!formatters) continue + if(isFunction(formatters[name])) { + return scope.$cache.formatters[name] = formatters[name] + } + } + } + /** + * Checker是一种特殊的格式化器,会在特定的时间执行 + * + * Checker应该返回{value,next}用来决定如何执行下一个格式化器函数 + * + * + * @param {*} checker + * @param {*} value + * @returns + */ + function executeChecker(checker,value){ + let result ={ value, next:"skip"} + if(!isFunction(checker)) return result + try{ + const r = checker(value) + if(isPlainObject(r)) { + Object.assign(result,r) + }else{ + result.value = r + } + if(!["break","skip"].includes(result.next)) result.next="break" + }catch(e){ + + } + return result + } + /** + * 执行格式化器并返回结果 + * + * 格式化器this指向当前scope,并且最后一个参数是当前scope格式化器的$config + * + * 这样格式化器可以读取$config + * + * @param {*} value + * @param {Array[Function]} formatters 多个格式化器函数(经过包装过的)顺序执行,前一个输出作为下一个格式化器的输入 + */ + function executeFormatter(value,formatters,scope,template){ + if(formatters.length===0) return value + let result = value + // 1. 空值检查 + const emptyCheckerIndex = formatters.findIndex(func=>func.$name==='empty') + if(emptyCheckerIndex!=-1){ + const emptyChecker = formatters.splice(emptyCheckerIndex,1)[0] + const { value,next } = executeChecker(emptyChecker,result) + if(next == 'break') { + return value + }else{ + result = value + } + } + // 2. 错误检查 + const errorCheckerIndex = formatters.findIndex(func=>func.$name==='error') + let errorChecker + if(errorCheckerIndex!=-1){ + errorChecker = formatters.splice(errorCheckerIndex,1)[0] + if(result instanceof Error){ + result.formatter = formatter.$name + const { value,next } = executeChecker(errorChecker,result) + if(next == 'break') { + return value + }else{ + result = value + } + } + } + + // 3. 分别执行格式化器函数 + for(let formatter of formatters){ + try{ + result = formatter(result,scope.activeFormatterConfig) + }catch(e){ + e.formatter = formatter.$name + if(scope.debug) console.error(`Error while execute i18n formatter<${formatter.$name}> for ${template}: ${e.message} ` ) + if(isFunction(errorChecker)){ + const { value,next } = executeChecker(errorChecker,result) + if(next=="break"){ + if(value!==undefined) result = value + break + }else if(next=="skip"){ + continue + } + } + } + } + return result + } + + + + /** + * 添加默认的empty和error格式化器,用来提供默认的空值和错误处理逻辑 + * + * empty和error格式化器有且只能有一个,其他无效 + * + * @param {*} formatters + */ + function addDefaultFormatters(formatters){ + // 默认的空值处理逻辑: 转换为"",然后继续执行接下来的逻辑 + if(formatters.findIndex(([name])=>name=="empty")===-1){ + formatters.push(["empty",[]]) + } + // 默认的错误处理逻辑: 开启DEBUG时会显示ERROR:message;关闭DEBUG时会保持最近值不变然后中止后续执行 + if(formatters.findIndex(([name])=>name=="error")===-1){ + formatters.push(["error",[]]) + } + } + + /** + * + * 经parseFormatters解析t('{}')中的插值表达式中的格式化器后会得到 + * [[<格式化器名称>,[参数,参数,...]],[<格式化器名称>,[参数,参数,...]]]数组 + * + * 本函数将之传换为转化为调用函数链,形式如下: + * [(v)=>{...},(v)=>{...},(v)=>{...}] + * + * 并且会自动将当前激活语言的格式化器配置作为最后一个参数配置传入,这样格式化器函数就可以读取 + * + * @param {*} scope + * @param {*} activeLanguage + * @param {*} formatters + * @returns {Array} [(v)=>{...},(v)=>{...},(v)=>{...}] + * + */ + function wrapperFormatters(scope,activeLanguage,formatters){ + let wrappedFormatters = [] + addDefaultFormatters(formatters) + for(let [name,args] of formatters){ + let fn = getFormatter(scope,activeLanguage,name) + let formatter + // 格式化器无效或者没有定义时,查看当前值是否具有同名的原型方法,如果有则执行调用 + // 比如padStart格式化器是String的原型方法,不需要配置就可以直接作为格式化器调用 + if(isFunction(fn)){ + formatter = (value,config) => fn.call(scope,value,...args,config) + }else{ + formatter = (value) =>{ + if(isFunction(value[name])){ + return value[name](...args) + }else{ + return value + } + } + } + formatter.$name = name + wrappedFormatters.push(formatter) + } + return wrappedFormatters + } + + /** + * 将value经过格式化器处理后返回的结果 + * @param {*} scope + * @param {*} activeLanguage + * @param {*} formatters + * @param {*} value + * @returns + */ + function getFormattedValue(scope,activeLanguage,formatters,value,template){ + // 1. 取得格式化器函数列表,然后经过包装以传入当前格式化器的配置参数 + const formatterFuncs = wrapperFormatters(scope,activeLanguage,formatters) + // 3. 执行格式化器 + // EMPTY和ERROR是默认两个格式化器,如果只有两个则说明在t(...)中没有指定格式化器 + if(formatterFuncs.length==2){ + // 当没有格式化器时,查询是否指定了默认数据类型的格式化器,如果有则执行 + const defaultFormatter = getDataTypeDefaultFormatter(scope,activeLanguage,getDataTypeName(value)) + if(defaultFormatter){ + return executeFormatter(value,[defaultFormatter],scope,template) + } + }else{ + value = executeFormatter(value,formatterFuncs,scope,template) + } + 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 + let result=template + + // 没有变量插值则的返回原字符串 + 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,match)=>{ + let value = (varname in varValues) ? varValues[varname] : '' + return getFormattedValue(scope,activeLanguage,formatters,value,template) + }) + }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,match)=>{ + if(params.length>i){ + return getFormattedValue(scope,activeLanguage,formatters,params[i++],template) + }else{ + throw new Error() // 抛出异常,停止插值处理 + } + },{replaceAll:false}) + + } + return result + } + + module.exports ={ + forEachInterpolatedVars, + getInterpolatedVars, // 获取指定字符串中的插件值变量列表 + replaceInterpolatedVars, // + createFormatter, + Formatter, + } + + \ No newline at end of file diff --git a/packages/runtime/translate.js b/packages/runtime/translate.js new file mode 100644 index 0000000..ffaa8d0 --- /dev/null +++ b/packages/runtime/translate.js @@ -0,0 +1,149 @@ +const {isNumber,isPlainObject,isFunction} = require("./utils") +const { replaceInterpolatedVars } = require("./interpolate") +/** + * 当传入的翻译内容不是一个字符串时,进行默认的转换 + * + * - 对函数则执行并取返回结果() + * - 对Array和Object使用JSON.stringify + * - 其他类型使用toString + * + * @param {*} value + * @returns + */ + function transformToString(value){ + let result = value + try{ + if(isFunction(result)) result = value() + if(!(typeof(result)==="string")){ + if(Array.isArray(result) || isPlainObject(result)){ + result = JSON.stringify(result) + }else{ + result = result.toString() + } + }else{ + return value + } + }catch{ + result = result.toString() + } + return result +} + +/** + * 文本id必须是一个数字 + * @param {*} content + * @returns + */ + function isMessageId(content){ + return isNumber(content) +} +/** + * 根据值的单数和复数形式,从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 + } +} + +/** + * 翻译函数 + * +* 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(isFunction(value)){ + 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 = isFunction(arg) ? 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 + let msgId = isMessageId(content) ? content : scope.idMap[content] + content = scope.messages[msgId] || 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 // 出错则返回原始文本 + } +} + + + +module.exports = { + translate +} \ No newline at end of file diff --git a/packages/runtime/utils.js b/packages/runtime/utils.js index 9388609..e13baa2 100644 --- a/packages/runtime/utils.js +++ b/packages/runtime/utils.js @@ -58,6 +58,32 @@ function isNothing(value){ return false } + +// 区配JSON字符串里面的非标的key,即key没有使用"字符包起来的键 +const bastardJsonKeyRegex = /(([\w\u4e00-\u9fa5])|(\'.*?\'))+(?=\s*\:)/g +/** + * 格式化器中的{a:1,b:2}形式的参数由于是非标准的JSON格式,采用JSON.parse会出错 + * 如果使用eval转换则存在安全隐患 + * 因此,本函数采用正则表达式来匹配KEY,然后为KEY自动添加""转换成标准JSON后再转换 + * @param {*} s + */ +function safeParseJson(str){ + let params = []; + let matched; + while ((matched = bastardJsonKeyRegex.exec(str)) !== null) { + if (matched.index === bastardJsonKeyRegex.lastIndex) { + bastardJsonKeyRegex.lastIndex++; + } + str = str.replace(new RegExp(`${matched[0]}\s*:`),key=>{ + key = key.substring(0,key.length-1).trim() + if(key.startsWith("'") && key.endsWith("'")){ + key = key.substring(1,key.length-1) + } + return `"${key}" :` + }) + } + return JSON.parse(str) + } /** * 深度合并对象 * @@ -153,22 +179,49 @@ function toNumber(value,defualt=0) { * @param {*} prefix 前缀 * @param {*} suffix 后缀 * @param {*} precision 小数点精确到几位,0-自动 + * @param {*} format 格式模块板字符串 * @returns */ - function toCurrency(value,{symbol="",division=3,prefix="",precision=0,suffix=""}={}){ + function toCurrency(value,params={}){ + const {symbol="",division=3,prefix="",precision=0,suffix="",unit=0,unitName="",radix=3,format="{symbol}{value}{unit}"} = params + + // 1. 分离出整数和小数部分 let [wholeValue,decimalValue] = String(value).split(".") + + // 2. 转换数制单位 比如将元转换到万元单位 + // 如果指定了unit单位,0-代表默认,1-N代表将小数点字向后移动radix*unit位 + // 比如 123456789.88 + // 当unit=1,radix=3时, == [123456,78988] + // 当unit=1,radix=3时, == [123,45678988] + if(unit>0 && radix>0){ + // 不足位数时补零 + if(wholeValue.length0) result.push(",") result.push(wholeValue[i]) } + // 4. 处理保留小数位数,即精度 if(decimalValue){ if(precision>0){ decimalValue = String(parseFloat(`0.${decimalValue}`).toFixed(precision)).split(".")[1] } result.push(`.${decimalValue}`) } - return `${prefix}${symbol}${result.join("")}${suffix}` + result = result.join("") + // 5. 模板替换 + result = format.replace("{value}",result) + .replace("{symbol}",symbol) + .replace("{prefix}",prefix) + .replace("{suffix}",suffix) + .replace("{unit}",unitName) + return result } /** @@ -357,7 +410,7 @@ function replaceAll(str,findValue,replaceValue){ function createFormatter(fn,options={},defaultParams={}){ let opts = Object.assign({ normalize : null, // 对输入值进行规范化处理,如进行时间格式化时,为了提高更好的兼容性,支持数字时间戳/字符串/Date等,需要对输入值进行处理,如强制类型转换等 - params : [], // 声明参数顺序 + params : null, // 可选的,声明参数顺序,如果是变参的,则需要传入null configKey : null // 声明该格式化器在$config中的路径,支持简单的使用.的路径语法 },options) @@ -375,12 +428,18 @@ function replaceAll(str,findValue,replaceValue){ if(!isPlainObject( activeFormatterConfigs)) activeFormatterConfigs ={} // 3. 从当前语言的激活语言中读取配置参数 const formatterConfig =Object.assign({},defaultParams,getByPath(activeFormatterConfigs,opts.configKey,{})) - let finalArgs = opts.params.map(param=>getByPath(formatterConfig,param,undefined)) - // 4. 将翻译函数执行格式化器时传入的参数覆盖默认参数 - for(let i =0; igetByPath(formatterConfig,param,undefined)) + // 4. 将翻译函数执行格式化器时传入的参数覆盖默认参数 + for(let i =0; i{ }) - 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) - }) +// 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) +// })