feat: 开始使用typescript
重构runtime
This commit is contained in:
parent
9521f48ebe
commit
d5dcae4c8a
@ -1,25 +0,0 @@
|
||||
module.exports = {
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"useBuiltIns": "usage",
|
||||
"debug": false,
|
||||
"modules": false,
|
||||
"corejs":{
|
||||
"version":"3.27.1",
|
||||
"proposals": true
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"@babel/plugin-transform-runtime",
|
||||
{
|
||||
"corejs":3,
|
||||
"proposals": true
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
@ -1,129 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* 处理中文数字和货币相关
|
||||
*
|
||||
*/
|
||||
|
||||
const { isNumber } = require('../utils')
|
||||
const { FlexFormatter,Formatter } = require('../formatter')
|
||||
|
||||
const CN_DATETIME_UNITS = ["年","季度","月","周","日","小时","分钟","秒","毫秒","微秒"]
|
||||
const CN_WEEK_DAYS = ["星期日","星期一","星期二","星期三","星期四","星期五","星期六"]
|
||||
const CN_SHORT_WEEK_DAYS =["周日","周一","周二","周三","周四","周五","周六"]
|
||||
const CN_MONTH_NAMES= ["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"]
|
||||
const CN_SHORT_MONTH_NAMES = ["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"]
|
||||
|
||||
const CN_NUMBER_DIGITS = ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"]
|
||||
const CN_NUMBER_UNITS = ['', '十', '百', '千', '万', '十', '百', '千', '亿', '十', '百', '千', '兆', '十', '百', '千', '京', '十', '百', '千', '垓']
|
||||
const CN_NUMBER_BIG_DIGITS = ["零", '壹', '貳', '參', '肆', '伍', '陸', '柒', '捌', '玖']
|
||||
const CN_NUMBER_BIG_UNITS = ['', '拾', '佰', '仟', '萬', '拾', '佰', '仟', '億', '拾', '佰', '仟', '兆', '拾', '佰', '仟', '京', '拾', '佰', '仟', '垓']
|
||||
|
||||
/**
|
||||
*
|
||||
* 将数字转换为中文数字
|
||||
*
|
||||
* 注意会忽略掉小数点后面的数字
|
||||
*
|
||||
* @param {*} value 数字
|
||||
* @param {*} isBig 是否大写数字
|
||||
* @returns
|
||||
*/
|
||||
function toChineseNumber(value,isBig) {
|
||||
if(!isNumber(value)) return value;
|
||||
let [wholeValue,decimalValue] = String(value).split(".") // 处理小数点
|
||||
const DIGITS = isBig ? CN_NUMBER_BIG_DIGITS : CN_NUMBER_DIGITS
|
||||
const UNITS = isBig ? CN_NUMBER_BIG_UNITS : CN_NUMBER_UNITS
|
||||
let result = ''
|
||||
if(wholeValue.length==1) return DIGITS[parseInt(wholeValue)]
|
||||
for(let i=wholeValue.length-1; i>=0; i--){
|
||||
let bit = parseInt(wholeValue[i])
|
||||
let digit = DIGITS[bit]
|
||||
let unit = UNITS[wholeValue.length-i-1]
|
||||
if(bit==0){
|
||||
let preBit =i< wholeValue.length ? parseInt(wholeValue[i+1]) : null// 上一位
|
||||
let isKeyBits = ((wholeValue.length-i-1) % 4)==0
|
||||
if(preBit && preBit!=0 && !isKeyBits) result = "零" + result
|
||||
if(isKeyBits) result = UNITS[wholeValue.length-i-1] + result
|
||||
}else{
|
||||
result=`${digit}${unit}` + result
|
||||
}
|
||||
}
|
||||
if(isBig){
|
||||
result = result.replace("垓京","垓")
|
||||
.replace("京兆","京")
|
||||
.replace("兆億","兆")
|
||||
.replace("億萬","億")
|
||||
.replace("萬仟","萬")
|
||||
}else{
|
||||
result = result.replace("垓京","垓")
|
||||
.replace("京兆","京")
|
||||
.replace("兆亿","兆")
|
||||
.replace("亿万","亿")
|
||||
.replace("万千","万")
|
||||
if(result.startsWith("一十")) result=result.substring(1)
|
||||
}
|
||||
return result // 中文数字忽略小数部分
|
||||
}
|
||||
|
||||
function toChineseBigNumber(value) {
|
||||
return toChineseNumber(value,true)
|
||||
}
|
||||
|
||||
const chineseNumberFormatter = Formatter((value,isBig,$config)=>{
|
||||
return toChineseNumber(value,isBig,$config)
|
||||
},{
|
||||
params:["isBig"]
|
||||
})
|
||||
|
||||
/**
|
||||
* 转换为中文大写货币
|
||||
* @param {*} value
|
||||
* @param {*} division 分割符号位数,3代表每3个数字添加一个,号
|
||||
* @param {*} prefix 前缀
|
||||
* @param {*} suffix 后缀
|
||||
* @param {*} showWhole 显示
|
||||
*/
|
||||
function toChineseCurrency(value,{big,prefix,unit,suffix}={},$config){
|
||||
let [wholeValue,decimalValue] = String(value).split(".")
|
||||
let result
|
||||
if(big){
|
||||
result = toChineseBigNumber(wholeValue)+unit
|
||||
}else{
|
||||
result = toChineseNumber(wholeValue)+unit
|
||||
}
|
||||
if(decimalValue){
|
||||
if(decimalValue[0]) {
|
||||
let bit0 = parseInt(decimalValue[0])
|
||||
result =result+ (big ? CN_NUMBER_BIG_DIGITS[bit0] : CN_NUMBER_DIGITS[bit0])+"角"
|
||||
}
|
||||
if(decimalValue[1]){
|
||||
let bit1 = parseInt(decimalValue[1])
|
||||
result =result+ (big ? CN_NUMBER_BIG_DIGITS[bit1] : CN_NUMBER_DIGITS[bit1])+"分"
|
||||
}
|
||||
}
|
||||
return prefix+result+suffix
|
||||
}
|
||||
|
||||
const rmbFormater = FlexFormatter((value,params,$config)=>{
|
||||
return toChineseCurrency(value,params,$config)
|
||||
},{
|
||||
params:["big","prefix","unit","suffix"],
|
||||
configKey:"rmb"
|
||||
})
|
||||
|
||||
module.exports ={
|
||||
toChineseCurrency,
|
||||
toChineseNumber,
|
||||
toChineseBigNumber,
|
||||
rmbFormater,
|
||||
chineseNumberFormatter,
|
||||
CN_DATETIME_UNITS,
|
||||
CN_WEEK_DAYS,
|
||||
CN_SHORT_WEEK_DAYS,
|
||||
CN_MONTH_NAMES,
|
||||
CN_SHORT_MONTH_NAMES,
|
||||
CN_NUMBER_DIGITS,
|
||||
CN_NUMBER_UNITS,
|
||||
CN_NUMBER_BIG_DIGITS,
|
||||
CN_NUMBER_BIG_UNITS
|
||||
}
|
@ -1,256 +0,0 @@
|
||||
/**
|
||||
|
||||
处理日期时间相关
|
||||
|
||||
*/
|
||||
const { isFunction,replaceAll,toDate } = require('../utils')
|
||||
const { Formatter } = require('../formatter');
|
||||
|
||||
/**
|
||||
* 获取一天中的时间段
|
||||
* @param {*} hour 小时,取值0-23
|
||||
* @param {*} options
|
||||
* @returns
|
||||
*/
|
||||
function getTimeSlot(hour,caseStyle = 0){
|
||||
if(hour<0 && hour>23) hour = 0
|
||||
const timeSlots = this.timeSlots
|
||||
const slots = [0,...timeSlots.slots,24]
|
||||
let slotIndex = slots.findIndex(v=>v>hour) - 1
|
||||
return caseStyle == 0 ? timeSlots.lowerCases[slotIndex] : timeSlots.upperCases[slotIndex]
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模板格式化日期时间
|
||||
*
|
||||
|
||||
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 月份的日期与序号
|
||||
t 小写时间段,如am,pm
|
||||
T 大写时间段段,如上午、中午、下午
|
||||
|
||||
|
||||
* 重点: this-->当前格化器的配置参数
|
||||
*
|
||||
* 因此可以通过this.month.long来读取长格式模板
|
||||
*
|
||||
*
|
||||
* @param {*} value
|
||||
* @param {*} template
|
||||
* @param {Boolean} onlyTime 仅格式化时间
|
||||
* @param {*} options = {month:[<短月份名称>,<短月份名称>],timeSlots:{}}
|
||||
* @returns
|
||||
*/
|
||||
function formatDatetime(value,template="YYYY/MM/DD HH:mm:ss",onlyTime=false){
|
||||
const $config = this.datetime// this->指向的是当前语言的格式化化器配置
|
||||
const v = toDate(value)
|
||||
const hour = v.getHours(),Hour = String(hour).padStart(2, "0")
|
||||
const hour12 = hour > 12 ? hour - 12 : hour ,Hour12 = String(hour12).padStart(2, "0")
|
||||
const minute = String(v.getMinutes()),second = String(v.getSeconds()),millisecond=String(v.getMilliseconds())
|
||||
let vars = [
|
||||
["HH", Hour], // 00-23 24小时,两位数
|
||||
["H", hour], // 0-23 24小时
|
||||
["hh", Hour12], // 01-12 12小时,两位数
|
||||
["h", hour12], // 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 毫秒,三位数
|
||||
["A", hour > 12 ? "PM" : "AM"], // AM / PM 上/下午,大写
|
||||
["a", hour > 12 ? "pm" : "am"], // am / pm 上/下午,小写
|
||||
["t", getTimeSlot.call($config,hour,0)], // 小写时间段,如上午、中午、下午
|
||||
["T", getTimeSlot.call($config,hour,1)], // 大写时间段,如上午、中午、下午
|
||||
]
|
||||
if(!onlyTime){
|
||||
const year =String(v.getFullYear()),month = v.getMonth(),weekday=v.getDay(),day=String(v.getDate())
|
||||
vars.push(...[
|
||||
["YYYY", year], // 2018 年,四位数
|
||||
["YY", year.substring(2)], // 18年,两位数
|
||||
["MMM", $config.month.short[month]], // Jan-Dec月,缩写
|
||||
["MM", String(month+1).padStart(2, "0")], // 01-12月,两位数字
|
||||
["M", month+1], // 1-12 月,从1开始
|
||||
["DD", day.padStart(2, "0")], // 01-31 日,两位数
|
||||
["D", day], // 1-31 日
|
||||
["d",weekday], // 0-6 一周中的一天,星期天是 0
|
||||
["dd",$config.weekday.short[weekday]], // Su-Sa 最简写的星期几
|
||||
["ddd",$config.weekday.short[weekday]], // Sun-Sat 简写的星期几
|
||||
["dddd",$config.weekday.long[weekday]], // Sunday-Saturday 星期几,英文全称
|
||||
])
|
||||
}
|
||||
let result = template
|
||||
vars.forEach(([k,v])=>result = replaceAll(result,k,v))
|
||||
return result
|
||||
}
|
||||
/**
|
||||
* 只格式化时间
|
||||
* @param {*} value
|
||||
* @param {*} template
|
||||
* @returns
|
||||
*/
|
||||
function formatTime(value,template="HH:mm:ss"){
|
||||
return formatDatetime.call(this,value,template,true)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 该类型的格式化器具有以下特点:
|
||||
*
|
||||
* 1. 接受一个format参数,
|
||||
* 2. format参数取值可以是若干预设的值,如long,short等,也可能是一个模板字符串
|
||||
* 3. 当format值时,如果定义在$config[configKey]里面,代表了$config[configKey][format]是一个模板字符串
|
||||
* 4. 如果!(format in $config[configKey]),则代表format值是一个模板字符串
|
||||
* 5. 如果format in presets, 则要求presets[format ]是一个(value)=>{....},直接返回
|
||||
*
|
||||
**/
|
||||
function createDateTimeFormatter(options={},transformer){
|
||||
let opts = Object.assign({presets:{}},options)
|
||||
return Formatter(function(value,format,$config){
|
||||
if((format in opts.presets) && isFunction(opts.presets[format])){
|
||||
return opts.presets[format](value)
|
||||
}else if((format in $config)){
|
||||
format = $config[format]
|
||||
}else if(format == "number"){
|
||||
return value
|
||||
}
|
||||
try{// this指向的是activeFormatter.$config
|
||||
return format==null ? value : transformer.call(this,value,format)
|
||||
}catch(e){
|
||||
return value
|
||||
}
|
||||
},opts)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 日期格式化器
|
||||
* - format取值:local,long,short,iso,gmt,utc,<模板字符串>
|
||||
* - 默认值由$config.datetime.date.format指定
|
||||
*/
|
||||
const dateFormatter = createDateTimeFormatter({
|
||||
normalize: toDate,
|
||||
params : ["format"],
|
||||
configKey: "datetime.date",
|
||||
presets : {
|
||||
local: value=>value.toLocaleString(),
|
||||
iso : value=>value.toISOString(),
|
||||
utc : value=>value.toUTCString(),
|
||||
gmt : value=>value.toGMTString()
|
||||
}
|
||||
},formatDatetime)
|
||||
|
||||
|
||||
/**
|
||||
* 季度格式化器
|
||||
* - format: long,short,number
|
||||
* - 默认值是 short
|
||||
*/
|
||||
const quarterFormatter = createDateTimeFormatter({
|
||||
normalize : value=>{
|
||||
const month = value.getMonth() + 1
|
||||
return Math.floor( ( month % 3 == 0 ? ( month / 3 ) : (month / 3 + 1 ) ))
|
||||
},
|
||||
params : ["format"],
|
||||
configKey: "datetime.quarter"
|
||||
},(quarter,format)=>format[quarter-1])
|
||||
|
||||
/**
|
||||
* 月份格式化器
|
||||
* - format: long,short,number
|
||||
* - 默认值是 short
|
||||
*/
|
||||
const monthFormatter = createDateTimeFormatter({
|
||||
normalize: value=> value.getMonth() + 1,
|
||||
params : ["format"],
|
||||
configKey: "datetime.month"
|
||||
},(month,format)=>format[month-1])
|
||||
|
||||
/**
|
||||
* 周格式化器
|
||||
* - format: long,short,number
|
||||
* - 默认值是 long
|
||||
*/
|
||||
const weekdayFormatter = createDateTimeFormatter({
|
||||
normalize: value=> value.getDay(),
|
||||
params : ["format"],
|
||||
configKey: "datetime.weekday"
|
||||
},(day,format)=>format[day])
|
||||
|
||||
/**
|
||||
* 时间格式化器
|
||||
* - format取值:local,long,short,timestamp,<模板字符串>
|
||||
* - 默认值由$config.datetime.time.format指定
|
||||
*/
|
||||
const timeFormatter = createDateTimeFormatter({
|
||||
normalize : toDate,
|
||||
params : ["format"],
|
||||
configKey : "datetime.time",
|
||||
presets : {
|
||||
local : value=>value.toLocaleTimeString(),
|
||||
timestamp: value=>value.getTime()
|
||||
}
|
||||
},formatTime)
|
||||
|
||||
/**
|
||||
* 返回相对于rel的时间
|
||||
* @param {*} value
|
||||
* @param {*} baseTime 基准时间,默认是相对现在
|
||||
*/
|
||||
// 对应:秒,分钟,小时,天,周,月,年的毫秒数,月取30天,年取365天概数
|
||||
const TIME_SECTIONS = [1000,60000,3600000,86400000,604800000,2592000000,31536000000,Number.MAX_SAFE_INTEGER]
|
||||
const relativeTimeFormatter = Formatter((value,baseTime,$config)=>{
|
||||
const { units,now,before,base = Date.now() , after } = $config
|
||||
let ms = value.getTime()
|
||||
let msBase = (baseTime instanceof Date) ? baseTime.getTime() : toDate(base).getTime()
|
||||
let msDiff = ms - msBase
|
||||
let msIndex = TIME_SECTIONS.findIndex(x=>Math.abs(msDiff) < x) - 1
|
||||
if(msIndex < 0) msIndex = 0
|
||||
if(msIndex > TIME_SECTIONS.length-1 ) msIndex = TIME_SECTIONS.length-1
|
||||
if(msDiff==0){
|
||||
return now
|
||||
}else if(msDiff<0){// 之前
|
||||
let result = parseInt(Math.abs(msDiff) / TIME_SECTIONS[msIndex])
|
||||
return before.replace("{value}",result).replace("{unit}",units[msIndex])
|
||||
}else{// 之后
|
||||
let result = parseInt(Math.abs(msDiff) / TIME_SECTIONS[msIndex])
|
||||
return after.replace("{value}",result).replace("{unit}",units[msIndex])
|
||||
}
|
||||
},{
|
||||
normalize:toDate,
|
||||
params:["base"],
|
||||
configKey:"datetime.relativeTime"
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
toDate,
|
||||
formatTime,
|
||||
formatDatetime,
|
||||
createDateTimeFormatter,
|
||||
relativeTimeFormatter,
|
||||
dateFormatter,
|
||||
quarterFormatter,
|
||||
monthFormatter,
|
||||
weekdayFormatter,
|
||||
timeFormatter
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* 简单的事件触发器
|
||||
*
|
||||
*/
|
||||
module.exports = class EventEmitter{
|
||||
constructor(){
|
||||
this._callbacks = []
|
||||
}
|
||||
on(callback){
|
||||
if(this._callbacks.includes(callback)) return
|
||||
this._callbacks.push(callback)
|
||||
}
|
||||
off(callback){
|
||||
for(let i=0;i<this._callbacks.length;i++){
|
||||
if(this._callbacks[i]===callback ){
|
||||
this._callbacks.splice(i,1)
|
||||
}
|
||||
}
|
||||
}
|
||||
offAll(){
|
||||
this._callbacks = []
|
||||
}
|
||||
async emit(...args){
|
||||
if(Promise.allSettled){
|
||||
await Promise.allSettled(this._callbacks.map(cb=>cb(...args)))
|
||||
}else{
|
||||
await Promise.all(this._callbacks.map(cb=>cb(...args)))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,359 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* 解析格式化器
|
||||
*
|
||||
* 解析{ varname | formater(...params) }中的params部分
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
const { getByPath,isNumber,isFunction,isPlainObject,escapeRegexpStr,safeParseJson } = require("./utils")
|
||||
|
||||
/**
|
||||
使用正则表达式对原始文本内容进行解析匹配后得到的便以处理的数组
|
||||
|
||||
formatters="| aaa(1,1) | bbb "
|
||||
|
||||
统一解析为
|
||||
|
||||
[
|
||||
[aaa,[1,1]], // [<格式化器名称>,[args,...]]
|
||||
[<格式化器名称>,[<参数>,<参数>,...]]
|
||||
]
|
||||
|
||||
formatters="| aaa(1,1,"dddd") | bbb "
|
||||
|
||||
特别注意:
|
||||
- 目前对参数采用简单的split(",")来解析,因此如果参数中包括了逗号等会影响解析的字符时,可能导致错误
|
||||
例如aaa(1,1,"dd,,dd")形式的参数
|
||||
在此场景下基本够用了,如果需要支持更复杂的参数解析,可以后续考虑使用正则表达式来解析
|
||||
- 如果参数是{},[],则尝试解决为对象和数组,但是里面的内容无法支持复杂和嵌套数据类型
|
||||
|
||||
@param {String} formatters
|
||||
|
||||
@returns [ [<格式化器名称>,[<参数>,<参数>,...],[<格式化器名称>,[<参数>,<参数>,...]],...]
|
||||
*/
|
||||
function parseFormatters(formatters) {
|
||||
if (!formatters) return [];
|
||||
// 1. 先解析为 ["aaa()","bbb"]形式
|
||||
let result = formatters.trim().substring(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) { //参数的格式化器
|
||||
// 带参数的格式化器: 取括号中的参数字符串部分
|
||||
const strParams = formatter.substring(firstIndex + 1, lastIndex).trim();
|
||||
// 解析出格式化的参数数组
|
||||
let params = parseFormaterParams(strParams);
|
||||
// 返回[<格式化器名称>,[<参数>,<参数>,...]
|
||||
return [formatter.substring(0, firstIndex), params];
|
||||
} else { // 不带参数的格式化器
|
||||
return [formatter, []];
|
||||
}
|
||||
}).filter((formatter) => Array.isArray(formatter));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 生成可以解析指定标签的正则表达式
|
||||
*
|
||||
* getNestingParamsRegex() -- 能解析{}和[]
|
||||
* getNestingParamsRegex(["<b>","</b>"]),
|
||||
*
|
||||
* @param {...any} tags
|
||||
* @returns
|
||||
*/
|
||||
function getNestingParamsRegex(...tags){
|
||||
if(tags.length==0){
|
||||
tags.push(["{","}"])
|
||||
tags.push(["[","]"])
|
||||
}
|
||||
const tagsRegexs = tags.map(([beginTag,endTag])=>{
|
||||
return `(${escapeRegexpStr(beginTag)}1%.*?%1${escapeRegexpStr(endTag)})`
|
||||
})
|
||||
return formatterNestingParamsRegex.replace("__TAG_REGEXP__",tagsRegexs.length > 0 ? tagsRegexs.join("|")+"|" : "")
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 遍历字符串中的 beginTag和endTag,添加辅助序号
|
||||
*
|
||||
* @param {*} str
|
||||
* @param {*} beginTag
|
||||
* @param {*} endTag
|
||||
* @returns
|
||||
*/
|
||||
function addTagFlags(str,beginTag="{",endTag="}"){
|
||||
let i = 0
|
||||
let flagIndex = 0
|
||||
while(i<str.length){
|
||||
let beginChars = str.slice(i,i+beginTag.length)
|
||||
let endChars = str.slice(i,i+endTag.length)
|
||||
if(beginChars==beginTag){
|
||||
flagIndex++
|
||||
str = str.substring(0,i+beginTag.length) + `${flagIndex}%` + str.substring(i+beginTag.length)
|
||||
i+=beginTag.length + String(flagIndex).length+1
|
||||
continue
|
||||
}
|
||||
if(endChars==endTag){
|
||||
if(flagIndex>0){
|
||||
str = str.substring(0,i) + `%${flagIndex}` + str.substring(i)
|
||||
}
|
||||
i+= endTag.length + String(flagIndex).length +1
|
||||
flagIndex--
|
||||
continue
|
||||
}
|
||||
i++
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加标签组辅助标识
|
||||
*
|
||||
* addTagHelperFlags("sss",["<div>","</div>"]
|
||||
*
|
||||
* @param {*} str
|
||||
* @param {...any} tags 默认已包括{},[]
|
||||
*/
|
||||
function addTagHelperFlags(str,...tags){
|
||||
if(tags.length==0){
|
||||
tags.push(["{","}"])
|
||||
tags.push(["[","]"])
|
||||
}
|
||||
tags.forEach(tag=>{
|
||||
if(str.includes(tag[0]) && str.includes(tag[1])){
|
||||
str = addTagFlags(str,...tag)
|
||||
}
|
||||
})
|
||||
return str
|
||||
}
|
||||
|
||||
function removeTagFlags(str,beginTag,endTag){
|
||||
const regexs = [
|
||||
[beginTag,new RegExp(escapeRegexpStr(beginTag)+"\\d+%")],
|
||||
[endTag,new RegExp("%\\d+"+escapeRegexpStr(endTag))]
|
||||
]
|
||||
regexs.forEach(([tag,regex])=>{
|
||||
let matched
|
||||
while ((matched = regex.exec(str)) !== null) {
|
||||
if (matched.index === regex.lastIndex) regex.lastIndex++;
|
||||
str = str.replace(regex,tag)
|
||||
}
|
||||
})
|
||||
return str
|
||||
}
|
||||
|
||||
function removeTagHelperFlags(str,...tags){
|
||||
if(tags.length==0){
|
||||
tags.push(["{","}"])
|
||||
tags.push(["[","]"])
|
||||
}
|
||||
tags.forEach(([beginTag,endTag])=>{
|
||||
if(str.includes(beginTag) && str.includes(endTag)){
|
||||
str = removeTagFlags(str,beginTag,endTag)
|
||||
}
|
||||
})
|
||||
return str
|
||||
}
|
||||
|
||||
// 提取匹配("a",1,2,'b',{..},[...]),不足:当{}嵌套时无法有效匹配
|
||||
// const formatterParamsRegex = /((([\'\"])(.*?)\3)|(\{.*?\})|(\[.*?\])|([\d]+\.?[\d]?)|((true|false|null)(?=[,\b\s]))|([\w\.]+)|((?<=,)\s*(?=,)))(?<=\s*[,\)]?\s*)/g;
|
||||
|
||||
// 支持解析嵌套的{}和[]参数, 前提是:字符串需要经addTagHelperFlags操作后,会在{}[]等位置添加辅助字符
|
||||
// 解析嵌套的{}和[]参数基本原理:在{}[]等位置添加辅助字符,然后使用正则表达式匹配,匹配到的字符串中包含辅助字符,然后再去除辅助字符
|
||||
const formatterNestingParamsRegex = String.raw`((([\'\"])(.*?)\3))|__TAG_REGEXP__([\d]+\.?[\d]?)|((true|false|null)(?=[,\b\s]))|([\w\.]+)|((?<=,)\s*(?=,))(?<=\s*[,\)]?\s*)`
|
||||
|
||||
/**
|
||||
* 解析格式化器的参数,即解析使用,分割的函数参数
|
||||
*
|
||||
* 采用正则表达式解析
|
||||
* 支持number,boolean,null,String,{},[]的参数,可以识别嵌套的{}和[]
|
||||
*
|
||||
* @param {*} strParams 格式化器参数字符串,即formater(<...参数....>)括号里面的参数,使用,分割
|
||||
* @returns {Array} 返回参数值数组 []
|
||||
*/
|
||||
function parseFormaterParams(strParams) {
|
||||
let params = [];
|
||||
let matched;
|
||||
// 1. 预处理: 处理{}和[]嵌套问题,增加嵌套标识
|
||||
strParams = addTagHelperFlags(strParams)
|
||||
try{
|
||||
let regex =new RegExp(getNestingParamsRegex(),"g")
|
||||
while ((matched = regex.exec(strParams)) !== null) {
|
||||
// 这对于避免零宽度匹配的无限循环是必要的
|
||||
if (matched.index === regex.lastIndex) {
|
||||
regex.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)
|
||||
value = removeTagHelperFlags(value)
|
||||
}else if((value.startsWith("{") && value.endsWith("}")) || (value.startsWith('[') && value.endsWith(']'))){
|
||||
try{
|
||||
value = removeTagHelperFlags(value)
|
||||
value = safeParseJson(value)
|
||||
}catch{}
|
||||
}else if(["true","false","null"].includes(value)){
|
||||
value = JSON.parse(value)
|
||||
}else if(isNumber(value)){
|
||||
value = parseFloat(value)
|
||||
}else{
|
||||
value = removeTagHelperFlags(String(value))
|
||||
}
|
||||
params.push(value)
|
||||
}
|
||||
}catch{
|
||||
|
||||
}
|
||||
return params
|
||||
}
|
||||
/**
|
||||
* 创建格式化器
|
||||
*
|
||||
* 格式化器是一个普通的函数,具有以下特点:
|
||||
*
|
||||
* - 函数第一个参数是上一上格式化器的输出
|
||||
* - 支持0-N个简单类型的入参
|
||||
* - 可以是定参,也可以变参
|
||||
* - 格式化器可以在格式化器的$config参数指定一个键值来配置不同语言时的参数
|
||||
*
|
||||
* "currency":createFormatter((value,prefix,suffix, division ,precision,$config)=>{
|
||||
* // 无论在格式化入参数是多少个,经过处理后在此得到prefix,suffix, division ,precision参数已经是经过处理后的参数
|
||||
* 依次读取格式化器的参数合并:
|
||||
* - 创建格式化时的defaultParams参数
|
||||
* - 从当前激活格式化器的$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:"$",
|
||||
* prefix,
|
||||
* suffix,
|
||||
* division,
|
||||
* precision
|
||||
* },
|
||||
* {
|
||||
* normalize:value=>{...},
|
||||
* params:["prefix","suffix", "division" ,"precision"] // 声明参数顺序
|
||||
* configKey:"currency" // 声明特定语言下的配置在$config.currency
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @param {*} fn
|
||||
* @param {*} options 配置参数
|
||||
* @param {*} defaultParams 可选默认值
|
||||
* @returns
|
||||
*/
|
||||
function createFormatter(fn,options={},defaultParams={}){
|
||||
let opts = Object.assign({
|
||||
normalize : null, // 对输入值进行规范化处理,如进行时间格式化时,为了提高更好的兼容性,支持数字时间戳/字符串/Date等,需要对输入值进行处理,如强制类型转换等
|
||||
params : null, // 可选的,声明参数顺序,如果是变参的,则需要传入null
|
||||
configKey : null // 声明该格式化器在$config中的路径,支持简单的使用.的路径语法
|
||||
},options)
|
||||
|
||||
// 最后一个参数是传入activeFormatterConfig参数
|
||||
// 并且格式化器的this指向的是activeFormatterConfig
|
||||
const $formatter = function(value,...args){
|
||||
let finalValue = value
|
||||
// 1. 输入值规范处理,主要是进行类型转换,确保输入的数据类型及相关格式的正确性,提高数据容错性
|
||||
if(isFunction(opts.normalize)){
|
||||
try{
|
||||
finalValue = opts.normalize(finalValue)
|
||||
}catch{}
|
||||
}
|
||||
// 2. 读取activeFormatterConfig
|
||||
let activeFormatterConfigs = args.length>0 ? args[args.length-1] : {}
|
||||
if(!isPlainObject( activeFormatterConfigs)) activeFormatterConfigs ={}
|
||||
// 3. 从当前语言的激活语言中读取配置参数
|
||||
const formatterConfig =Object.assign({},defaultParams,getByPath(activeFormatterConfigs,opts.configKey,{}))
|
||||
let finalArgs
|
||||
if(opts.params==null){// 如果格式化器支持变参,则需要指定params=null
|
||||
finalArgs = args.slice(0,args.length-1)
|
||||
}else{ // 具有固定的参数个数
|
||||
finalArgs = opts.params.map(param=>{
|
||||
let paramValue = getByPath(formatterConfig,param,undefined)
|
||||
return isFunction(paramValue) ? paramValue(finalValue) : paramValue
|
||||
})
|
||||
// 4. 将翻译函数执行格式化器时传入的参数覆盖默认参数
|
||||
for(let i =0; i<finalArgs.length;i++){
|
||||
if(i==args.length-1) break // 最后一参数是配置
|
||||
if(args[i]!==undefined) finalArgs[i] = args[i]
|
||||
}
|
||||
}
|
||||
return fn.call(this,finalValue,...finalArgs,formatterConfig)
|
||||
}
|
||||
$formatter.configurable = true
|
||||
return $formatter
|
||||
}
|
||||
|
||||
const Formatter = createFormatter
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 创建支持弹性参数的格式化器
|
||||
* 弹性参数指的是该格式式化器支持两种传参数形式:
|
||||
* - 位置参数: { value | format(a,b,c,d,e) }
|
||||
* - 对象参数: { value | format({a,b,c,e}) }
|
||||
*
|
||||
* 弹性参数的目的是只需要位置参数中的第N参数时,简化传参
|
||||
* 例:
|
||||
* 上例中,我们只需要传入e参数,则不得不使用{ value | format(,,,,e) }
|
||||
* 弹性参数允许使用 { value | format({e:<value>}) }
|
||||
*
|
||||
* 请参阅currencyFormatter用法
|
||||
*
|
||||
* @param {*} fn
|
||||
* @param {*} options
|
||||
* @param {*} defaultParams
|
||||
*/
|
||||
const createFlexFormatter = function(fn,options={},defaultParams={}){
|
||||
const $flexFormatter = Formatter(function(value,...args){
|
||||
// 1. 最后一个参数是格式化器的参数,不同语言不一样
|
||||
let $config = args[args.length-1]
|
||||
// 2. 从语言配置中读取默认参数
|
||||
let finalParams = options.params.reduce((r,name) =>{
|
||||
r[name] = $config[name]==undefined ? defaultParams[name] : $config[name]
|
||||
return r
|
||||
} ,{})
|
||||
// 3. 从格式化器中传入的参数具有最高优先级,覆盖默认参数
|
||||
if(args.length==2 && isPlainObject(args[0])){ // 一个参数且是{}
|
||||
Object.assign(finalParams,{format:"custom"},args[0])
|
||||
}else{ // 位置参数,如果是空则代表
|
||||
for(let i=0; i<args.length-1; i++){
|
||||
if(args[i]!==undefined) finalParams[options.params[i]] = args[i]
|
||||
}
|
||||
}
|
||||
return fn.call(this,value,finalParams,$config)
|
||||
},{...options,params:null}) // 变参工式化器需要指定params=null
|
||||
return $flexFormatter
|
||||
}
|
||||
|
||||
|
||||
|
||||
const FlexFormatter = createFlexFormatter
|
||||
|
||||
module.exports = {
|
||||
createFormatter,
|
||||
createFlexFormatter,
|
||||
Formatter,
|
||||
FlexFormatter,
|
||||
parseFormatters
|
||||
|
||||
}
|
162
packages/runtime/index.d.ts
vendored
162
packages/runtime/index.d.ts
vendored
@ -1,162 +0,0 @@
|
||||
declare global {
|
||||
export type VoerkaI18nILanguageMessages = Record<string, string>
|
||||
export interface VoerkaI18nLanguagePack {
|
||||
[key: string]: VoerkaI18nILanguageMessages
|
||||
}
|
||||
export interface VoerkaI18nManagerSettings {
|
||||
debug?: boolean
|
||||
defaultLanguage: string
|
||||
activeLanguage: string
|
||||
formatters: VoerkI18nFormatters
|
||||
languages: VoerkaI18nLanguage[]
|
||||
}
|
||||
|
||||
export class VoerkaI18nManager {
|
||||
constructor(settings: VoerkaI18nManagerSettings)
|
||||
get settings(): Required<VoerkaI18nManagerSettings> // 配置参数
|
||||
get scopes(): VoerkaI18nScope[] // 注册的报有i18nScope实例q
|
||||
get activeLanguage(): string // 当前激活语言名称
|
||||
get defaultLanguage(): string // 默认语言名称
|
||||
get languages(): VoerkaI18nSupportedLanguages // 支持的语言列表
|
||||
get formatters(): VoerkI18nFormatters // 内置格式化器{*:{$config,$types,...},zh:{$config,$types,...},en:{$config,$types,...}}
|
||||
get defaultMessageLoader(): VoerkI18nLoader // 默认语言包加载器
|
||||
// 通过默认加载器加载文件
|
||||
loadMessagesFromDefaultLoader(newLanguage: string, scope: VoerkaI18nScope): Promise<VoerkaI18nILanguageMessages>
|
||||
change(language: string): Promise<void>
|
||||
register(scope: VoerkaI18nScope): Promise<void>
|
||||
registerFormatter(name: string, formatter: VoerkI18nFormatter, options?: { language: string | string[] }): void
|
||||
registerDefaultLoader(fn: VoerkI18nLoader): void
|
||||
refresh(): Promise<void>
|
||||
}
|
||||
|
||||
export type Voerkai18nIdMap = Record<string, number>
|
||||
|
||||
export interface VoerkaI18nLanguage {
|
||||
name: string
|
||||
title?: string
|
||||
default?: boolean
|
||||
fallback?: string
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type VoerkI18nFormatter = (value: string, ...args: any[]) => string
|
||||
export type VoerkI18nFormatterConfigs = Record<string, any>
|
||||
|
||||
export type VoerkI18nFormatters = Record<string, ({
|
||||
$types?: Record<string, VoerkI18nFormatter>
|
||||
$config?: Record<string, string>
|
||||
} & {
|
||||
[key: string]: VoerkI18nFormatter
|
||||
}) | (() => Promise<any>)>
|
||||
|
||||
export type VoerkI18nLoader = () => Awaited<Promise<any>>
|
||||
export interface VoerkI18nLoaders {
|
||||
[key: string]: VoerkI18nLoader
|
||||
}
|
||||
|
||||
export interface VoerkaVoerkaI18nScopeOptions {
|
||||
id?: string
|
||||
debug?: boolean
|
||||
languages: VoerkaI18nLanguage[]
|
||||
defaultLanguage: string // 默认语言名称
|
||||
activeLanguage: string // 当前语言名称
|
||||
default: VoerkaI18nILanguageMessages // 默认语言包
|
||||
messages: VoerkaI18nILanguageMessages // 当前语言包
|
||||
idMap: Voerkai18nIdMap // 消息id映射列表
|
||||
formatters: VoerkI18nFormatters // 当前作用域的格式化函数列表{<lang>: {$types,$config,[格式化器名称]: () => {},[格式化器名称]: () => {}}}
|
||||
loaders: VoerkI18nLoaders; // 异步加载语言文件的函数列表
|
||||
}
|
||||
|
||||
export var VoerkaI18n: VoerkaI18nManager
|
||||
}
|
||||
|
||||
export type TranslateMessageVars = number | boolean | string | Function | Date
|
||||
export interface VoerkaI18nTranslate {
|
||||
(message: string, ...args: TranslateMessageVars[]): string
|
||||
(message: string, vars?: Record<string, TranslateMessageVars>): string
|
||||
}
|
||||
export interface VoerkaI18nSupportedLanguages {
|
||||
[key: string]: VoerkaI18nLanguage
|
||||
}
|
||||
export class VoerkaI18nScope {
|
||||
constructor(options: VoerkaVoerkaI18nScopeOptions, callback?: Function)
|
||||
get id(): string // 作用域唯一id
|
||||
get debug(): boolean // 调试开关
|
||||
get defaultLanguage(): string // 默认语言名称
|
||||
get activeLanguage(): string // 默认语言名称
|
||||
get default(): VoerkaI18nILanguageMessages // 默认语言包
|
||||
get messages(): VoerkaI18nILanguageMessages // 当前语言包
|
||||
get idMap(): Voerkai18nIdMap // 消息id映射列表
|
||||
get languages(): VoerkaI18nSupportedLanguages // 当前作用域支持的语言列表[{name,title,fallback}]
|
||||
get loaders(): VoerkI18nLoaders // 异步加载语言文件的函数列表
|
||||
get global(): VoerkaI18nManager // 引用全局VoerkaI18n配置,注册后自动引用
|
||||
get formatters(): VoerkI18nFormatters // 当前作用域的所有格式化器定义 {<语言名称>: {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () => {}}}
|
||||
get activeFormatters(): VoerkI18nFormatters // 当前作用域激活的格式化器定义 {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () = >{}}
|
||||
get activeFormatterConfig(): VoerkI18nFormatterConfigs // 当前格式化器合并后的配置参数,参数已经合并了全局格式化器中的参数
|
||||
get t(): VoerkaI18nTranslate
|
||||
get translate(): VoerkaI18nTranslate
|
||||
/**
|
||||
* 在全局注册作用域当前作用域
|
||||
* @param {*} callback 注册成功后的回调
|
||||
*/
|
||||
register(callback: Function): void
|
||||
/**
|
||||
* 注册格式化器
|
||||
*/
|
||||
registerFormatter(name: string, formatter: VoerkI18nFormatter, { language, global }: { language: string | string[], global: boolean }): void
|
||||
/**
|
||||
* 注册多种格式化器
|
||||
*/
|
||||
registerFormatters(formatters: VoerkI18nFormatters, asGlobal?: boolean): void
|
||||
/**
|
||||
* 注册默认文本信息加载器
|
||||
*/
|
||||
registerDefaultLoader(fn: Function): void
|
||||
/**
|
||||
* 获取指定语言信息
|
||||
* @param {*} language
|
||||
* @returns
|
||||
*/
|
||||
getLanguage(language: string): VoerkaI18nILanguageMessages
|
||||
hasLanguage(language: string): boolean
|
||||
refresh(newLanguage: string): Promise<void>
|
||||
on(): void
|
||||
off(): void
|
||||
offAll(): void
|
||||
change(language: string): Promise<void>
|
||||
}
|
||||
export type VoerkI18nFormatterConfigs = Record<string, any>
|
||||
// 翻译函数
|
||||
export var translate: {
|
||||
(message: string, ...args: (string | Function)[]): string
|
||||
(message: string, vars?: Record<string, any>): string
|
||||
}
|
||||
export interface CreateFormatterOptions {
|
||||
normalize?: (value: any) => any // 对输入值进行规范化处理,如进行时间格式化时,为了提高更好的兼容性,支持数字时间戳/字符串/Date等,需要对输入值进行处理,如强制类型转换等
|
||||
params?: string[] | null // 可选的,声明参数顺序,如果是变参的,则需要传入null
|
||||
configKey?: string // 声明该格式化器在$config中的路径,支持简单的使用.的路径语法
|
||||
}
|
||||
|
||||
export type Primitive = string | number | boolean | null | undefined
|
||||
|
||||
export interface FormatterDefine {
|
||||
(this: VoerkI18nFormatterConfigs, value: any, ...args: Primitive[]): string
|
||||
(this: VoerkI18nFormatterConfigs, value: any, arg1: Primitive, $config: VoerkI18nFormatterConfigs): string
|
||||
(this: VoerkI18nFormatterConfigs, value: any, arg1: Primitive, arg2: Primitive, $config: VoerkI18nFormatterConfigs): string
|
||||
(this: VoerkI18nFormatterConfigs, value: any, arg1: Primitive, arg2: Primitive, arg3: Primitive, $config: VoerkI18nFormatterConfigs): string
|
||||
configurable?: boolean
|
||||
}
|
||||
// 创建格式化器
|
||||
export type CreateFormatterType = (fn: Function, options: CreateFormatterOptions, defaultParams: Record<string, any>) => FormatterDefine
|
||||
export var createFormatter: CreateFormatterType
|
||||
export var Formatter: CreateFormatterType
|
||||
|
||||
export type CreateFlexFormatterType = (fn: Function, options: CreateFormatterOptions, defaultParams: Record<string, any>) => FormatterDefine
|
||||
export var createFlexFormatter: CreateFlexFormatterType
|
||||
export var FlexFormatter: CreateFlexFormatterType
|
||||
export var getDataTypeName: (value: any) => string
|
||||
export var toDate: (value: any) => Date | number
|
||||
export var toNumber: (value: any, defaultValue: number) => number
|
||||
export var toBoolean: (value: any) => boolean
|
||||
|
@ -21,9 +21,11 @@
|
||||
"author": "wxzhang",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.7",
|
||||
"@babel/runtime-corejs3": "latest",
|
||||
"core-js": "^3.27.1",
|
||||
"@babel/runtime": "^7.20.7"
|
||||
"flex-tools": "^1.0.72",
|
||||
"string.prototype.replaceall": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.17.6",
|
||||
@ -38,7 +40,8 @@
|
||||
"jest": "^27.5.1",
|
||||
"rollup": "^2.69.0",
|
||||
"rollup-plugin-clear": "^2.0.7",
|
||||
"rollup-plugin-terser": "^7.0.2"
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"vitest": "^0.29.8"
|
||||
},
|
||||
"lastPublish": "2023-03-30T08:53:24+08:00"
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
|
||||
import clear from 'rollup-plugin-clear'
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
// import resolve from "@rollup/plugin-node-resolve";
|
||||
import { terser } from "rollup-plugin-terser";
|
||||
import { babel } from '@rollup/plugin-babel';
|
||||
|
||||
export default [
|
||||
{
|
||||
input: './index.js',
|
||||
output: [
|
||||
{
|
||||
file: 'dist/index.esm.js',
|
||||
format:"esm",
|
||||
exports:"default",
|
||||
esModule:"if-default-prop",
|
||||
sourcemap:true
|
||||
},
|
||||
{
|
||||
file: 'dist/index.cjs',
|
||||
exports:"default",
|
||||
format:"cjs",
|
||||
sourcemap:true
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
//resolve(),
|
||||
commonjs(),
|
||||
babel({
|
||||
babelHelpers:"runtime",
|
||||
exclude: 'node_modules/**'
|
||||
}),
|
||||
clear({targets:["dist"]}),
|
||||
// terser()
|
||||
],
|
||||
external:["@babel/runtime"]
|
||||
}
|
||||
]
|
0
packages/runtime/src/__tests__/index.test.ts
Normal file
0
packages/runtime/src/__tests__/index.test.ts
Normal file
25
packages/runtime/src/datatypes/chinese.ts
Normal file
25
packages/runtime/src/datatypes/chinese.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
*
|
||||
* 处理中文数字和货币相关
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
import { FlexFormatter, Formatter } from '../formatter'
|
||||
import { toChineseNumber } from "flex-tools/chinese/toChineseNumber"
|
||||
import { toChineseCurrency } from "flex-tools/chinese/toChineseCurrency"
|
||||
|
||||
|
||||
export const chineseNumberFormatter = Formatter((value: number, isBig: boolean, $config: any) => {
|
||||
return toChineseNumber(value, isBig)
|
||||
}, {
|
||||
params: ["isBig"]
|
||||
})
|
||||
|
||||
|
||||
export const rmbFormater = FlexFormatter((value:number | string, params : any, $config:any) => {
|
||||
return toChineseCurrency(value, params, $config)
|
||||
}, {
|
||||
params: ["big", "prefix", "unit", "suffix"],
|
||||
configKey: "rmb"
|
||||
})
|
@ -4,8 +4,11 @@
|
||||
*
|
||||
*/
|
||||
|
||||
const { isNumber, toNumber, getByPath } = require('../utils')
|
||||
const { FlexFormatter } = require("../formatter")
|
||||
import { toNumber } from '../utils'
|
||||
import { get as getByPath } from "flex-tools/object/get"
|
||||
import { isNumber } from "flex-tools/typecheck/isNumber"
|
||||
import { FlexFormatter } from "../formatter"
|
||||
|
||||
|
||||
/**
|
||||
* 为字符串按bits位添加一个,
|
||||
@ -13,7 +16,7 @@ const { FlexFormatter } = require("../formatter")
|
||||
* @param {*} bits
|
||||
* @returns
|
||||
*/
|
||||
function addSplitChars(str,bits=3){
|
||||
function addSplitChars(str:string,bits:number=3){
|
||||
let regexp = new RegExp(String.raw`(?!^)(?=(\d{${bits}})+$)`,"g")
|
||||
let r = str.replace(regexp,",")
|
||||
if(r.startsWith(",")) r = r.substring(1)
|
||||
@ -33,7 +36,7 @@ function addSplitChars(str,bits=3){
|
||||
* @param {*} format 格式模块板字符串
|
||||
* @returns
|
||||
*/
|
||||
function toCurrency(value,params={},$config={}){
|
||||
export function toCurrency(value:string|number ,params:Record<string,any>={},$config={}){
|
||||
let {symbol="",division=3,prefix="",precision=2,suffix="",unit=0,radix=3,format="{symbol}{value}{unit}"} = params
|
||||
|
||||
// 1. 分离出整数和小数部分
|
||||
@ -47,7 +50,7 @@ function addSplitChars(str,bits=3){
|
||||
// 不足位数时补零
|
||||
if(wholeDigits.length<radix*unit) wholeDigits = new Array(radix*unit-wholeDigits.length+1).fill(0).join("")+ wholeDigits
|
||||
// 将整数的最后radix*unit字符移到小数部分前面
|
||||
decimalDigits=wholeDigits.substring(wholeDigits,wholeDigits.length-radix*unit)+decimalDigits
|
||||
decimalDigits=wholeDigits.substring(Number(wholeDigits),wholeDigits.length-radix*unit)+decimalDigits
|
||||
wholeDigits = wholeDigits.substring(0,wholeDigits.length-radix*unit)
|
||||
if(wholeDigits=="") wholeDigits = "0"
|
||||
}
|
||||
@ -67,14 +70,18 @@ function addSplitChars(str,bits=3){
|
||||
result.push(`.${decimalDigits}`)
|
||||
}
|
||||
// 5. 模板替换
|
||||
const unitName = getByPath($config,`units`,[])[unit] || ""
|
||||
const unitName = getByPath($config,`units`,{defaultValue:[]})[unit] || ""
|
||||
return format.replace("{value}",result.join(""))
|
||||
.replace("{symbol}",symbol)
|
||||
.replace("{prefix}",prefix)
|
||||
.replace("{suffix}",suffix)
|
||||
.replace("{unit}",unitName)
|
||||
}
|
||||
const currencyFormatter = FlexFormatter((value,params={},$config)=>{
|
||||
|
||||
/**
|
||||
* 货币格式化器
|
||||
*/
|
||||
export const currencyFormatter = FlexFormatter((value:string | number,params:Record<string,any>={},$config:any)=>{
|
||||
// format可以取预设值的名称,如long,short等
|
||||
if(params.format in $config){
|
||||
params.format = $config[params.format]
|
||||
@ -99,8 +106,4 @@ const currencyFormatter = FlexFormatter((value,params={},$config)=>{
|
||||
unit:0 // 小数点精度控制,0代表不控制
|
||||
})
|
||||
|
||||
|
||||
module.exports = {
|
||||
toCurrency,
|
||||
currencyFormatter
|
||||
}
|
||||
|
133
packages/runtime/src/datatypes/datetime.ts
Normal file
133
packages/runtime/src/datatypes/datetime.ts
Normal file
@ -0,0 +1,133 @@
|
||||
/**
|
||||
|
||||
处理日期时间相关
|
||||
|
||||
*/
|
||||
const { isFunction,replaceAll,toDate } = require('../utils')
|
||||
const { Formatter } = require('../formatter');
|
||||
import { formatDateTime } from "flex-tools/misc/formatDateTime"
|
||||
import { relativeTime } from "flex-tools/misc/relativeTime"
|
||||
import { assignObject } from "flex-tools/object/assignObject"
|
||||
|
||||
|
||||
|
||||
function formatTime(value:number ,template="HH:mm:ss"){
|
||||
return formatDateTime(value,template,{
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 该类型的格式化器具有以下特点:
|
||||
*
|
||||
* 1. 接受一个format参数,
|
||||
* 2. format参数取值可以是若干预设的值,如long,short等,也可能是一个模板字符串
|
||||
* 3. 当format值时,如果定义在$config[configKey]里面,代表了$config[configKey][format]是一个模板字符串
|
||||
* 4. 如果!(format in $config[configKey]),则代表format值是一个模板字符串
|
||||
* 5. 如果format in presets, 则要求presets[format ]是一个(value)=>{....},直接返回
|
||||
*
|
||||
**/
|
||||
export type FormatterTransformer = (value:any,format:string)=>string
|
||||
export function createDateTimeFormatter(options={},transformer:FormatterTransformer){
|
||||
let opts = assignObject({presets:{}},options)
|
||||
return Formatter(function(value,format,$config){
|
||||
if((format in opts.presets) && isFunction(opts.presets[format])){
|
||||
return opts.presets[format](value)
|
||||
}else if((format in $config)){
|
||||
format = $config[format]
|
||||
}else if(format == "number"){
|
||||
return value
|
||||
}
|
||||
try{// this指向的是activeFormatter.$config
|
||||
return format==null ? value : transformer.call(this,value,format)
|
||||
}catch(e){
|
||||
return value
|
||||
}
|
||||
},opts)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 日期格式化器
|
||||
* - format取值:local,long,short,iso,gmt,utc,<模板字符串>
|
||||
* - 默认值由$config.datetime.date.format指定
|
||||
*/
|
||||
const dateFormatter = createDateTimeFormatter({
|
||||
normalize: toDate,
|
||||
params : ["format"],
|
||||
configKey: "datetime.date",
|
||||
presets : {
|
||||
local: value=>value.toLocaleString(),
|
||||
iso : value=>value.toISOString(),
|
||||
utc : value=>value.toUTCString(),
|
||||
gmt : value=>value.toGMTString()
|
||||
}
|
||||
},formatDatetime)
|
||||
|
||||
|
||||
/**
|
||||
* 季度格式化器
|
||||
* - format: long,short,number
|
||||
* - 默认值是 short
|
||||
*/
|
||||
const quarterFormatter = createDateTimeFormatter({
|
||||
normalize : value=>{
|
||||
const month = value.getMonth() + 1
|
||||
return Math.floor( ( month % 3 == 0 ? ( month / 3 ) : (month / 3 + 1 ) ))
|
||||
},
|
||||
params : ["format"],
|
||||
configKey: "datetime.quarter"
|
||||
},(quarter,format)=>format[quarter-1])
|
||||
|
||||
/**
|
||||
* 月份格式化器
|
||||
* - format: long,short,number
|
||||
* - 默认值是 short
|
||||
*/
|
||||
const monthFormatter = createDateTimeFormatter({
|
||||
normalize: (value:Date)=> value.getMonth() + 1,
|
||||
params : ["format"],
|
||||
configKey: "datetime.month"
|
||||
},(month,format)=>format[month-1])
|
||||
|
||||
/**
|
||||
* 周格式化器
|
||||
* - format: long,short,number
|
||||
* - 默认值是 long
|
||||
*/
|
||||
const weekdayFormatter = createDateTimeFormatter({
|
||||
normalize: (value:Date)=> value.getDay(),
|
||||
params : ["format"],
|
||||
configKey: "datetime.weekday"
|
||||
},(day,format)=>format[day])
|
||||
|
||||
/**
|
||||
* 时间格式化器
|
||||
* - format取值:local,long,short,timestamp,<模板字符串>
|
||||
* - 默认值由$config.datetime.time.format指定
|
||||
*/
|
||||
const timeFormatter = createDateTimeFormatter({
|
||||
normalize : toDate,
|
||||
params : ["format"],
|
||||
configKey : "datetime.time",
|
||||
presets : {
|
||||
local : (value:Date)=>value.toLocaleTimeString(),
|
||||
timestamp: (value:Date)=>value.getTime()
|
||||
}
|
||||
},formatTime)
|
||||
|
||||
/**
|
||||
* 返回相对于rel的时间
|
||||
* @param {*} value
|
||||
* @param {*} baseTime 基准时间,默认是相对现在
|
||||
*/
|
||||
const relativeTimeFormatter = Formatter((value:Date,baseTime:Date,$config:any)=>{
|
||||
//const { units,now,before,base = Date.now() , after } = $config
|
||||
return relativeTime(value, $config)
|
||||
},{
|
||||
normalize:toDate,
|
||||
params:["base"],
|
||||
configKey:"datetime.relativeTime"
|
||||
})
|
||||
|
@ -20,8 +20,4 @@ const numberFormartter = Formatter(function(value,precision,division,$config){
|
||||
configKey: "number"
|
||||
})
|
||||
|
||||
|
||||
|
||||
module.exports = {
|
||||
numberFormartter
|
||||
}
|
||||
|
32
packages/runtime/src/eventemitter.ts
Normal file
32
packages/runtime/src/eventemitter.ts
Normal file
@ -0,0 +1,32 @@
|
||||
/**
|
||||
*
|
||||
* 简单的事件触发器
|
||||
*
|
||||
*/
|
||||
export class EventEmitter{
|
||||
#callbacks:Function[]
|
||||
constructor(){
|
||||
this.#callbacks = []
|
||||
}
|
||||
on(callback:Function){
|
||||
if(this.#callbacks.includes(callback)) return
|
||||
this.#callbacks.push(callback)
|
||||
}
|
||||
off(callback:Function){
|
||||
for(let i=0;i<this.#callbacks.length;i++){
|
||||
if(this.#callbacks[i]===callback ){
|
||||
this.#callbacks.splice(i,1)
|
||||
}
|
||||
}
|
||||
}
|
||||
offAll(){
|
||||
this.#callbacks = []
|
||||
}
|
||||
async emit(...args:any[]){
|
||||
if(Promise.allSettled){
|
||||
await Promise.allSettled(this.#callbacks.map(cb=>cb(...args)))
|
||||
}else{
|
||||
await Promise.all(this.#callbacks.map(cb=>cb(...args)))
|
||||
}
|
||||
}
|
||||
}
|
366
packages/runtime/src/formatter.ts
Normal file
366
packages/runtime/src/formatter.ts
Normal file
@ -0,0 +1,366 @@
|
||||
/**
|
||||
*
|
||||
* 解析格式化器
|
||||
*
|
||||
* 解析{ varname | formater(...params) }中的params部分
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
import { escapeRegexpStr } from "./utils"
|
||||
import { get as getByPath } from "flex-tools/object/get"
|
||||
import { isNumber } from "flex-tools/typecheck/isNumber"
|
||||
import { isFunction } from "flex-tools/typecheck/isFunction"
|
||||
import { isPlainObject } from "flex-tools/typecheck/isPlainObject"
|
||||
import { safeParseJson } from "flex-tools/object/safeParseJson"
|
||||
import { assignObject } from "flex-tools/object/assignObject"
|
||||
|
||||
/**
|
||||
使用正则表达式对原始文本内容进行解析匹配后得到的便以处理的数组
|
||||
|
||||
formatters="| aaa(1,1) | bbb "
|
||||
|
||||
统一解析为
|
||||
|
||||
[
|
||||
[aaa,[1,1]], // [<格式化器名称>,[args,...]]
|
||||
[<格式化器名称>,[<参数>,<参数>,...]]
|
||||
]
|
||||
|
||||
formatters="| aaa(1,1,"dddd") | bbb "
|
||||
|
||||
特别注意:
|
||||
- 目前对参数采用简单的split(",")来解析,因此如果参数中包括了逗号等会影响解析的字符时,可能导致错误
|
||||
例如aaa(1,1,"dd,,dd")形式的参数
|
||||
在此场景下基本够用了,如果需要支持更复杂的参数解析,可以后续考虑使用正则表达式来解析
|
||||
- 如果参数是{},[],则尝试解决为对象和数组,但是里面的内容无法支持复杂和嵌套数据类型
|
||||
|
||||
@param {String} formatters
|
||||
|
||||
@returns [ [<格式化器名称>,[<参数>,<参数>,...],[<格式化器名称>,[<参数>,<参数>,...]],...]
|
||||
*/
|
||||
|
||||
//@returns [ [<格式化器名称>,[<参数>,<参数>,...],[<格式化器名称>,[<参数>,<参数>,...]],...]
|
||||
export type FormatterDefines = ([string, string[]])[]
|
||||
export function parseFormatters(formatters: string): FormatterDefines {
|
||||
if (!formatters) return [];
|
||||
// 1. 先解析为 ["aaa()","bbb"]形式
|
||||
let result = formatters.trim().substring(1).trim().split("|").map((r) => r.trim());
|
||||
// 2. 解析格式化器参数
|
||||
return result.map((formatter: string) => {
|
||||
if (formatter == "") return null;
|
||||
let firstIndex = formatter.indexOf("(");
|
||||
let lastIndex = formatter.lastIndexOf(")");
|
||||
if (firstIndex !== -1 && lastIndex !== -1) { //参数的格式化器
|
||||
// 带参数的格式化器: 取括号中的参数字符串部分
|
||||
const strParams = formatter.substring(firstIndex + 1, lastIndex).trim();
|
||||
// 解析出格式化的参数数组
|
||||
let params = parseFormaterParams(strParams);
|
||||
// 返回[<格式化器名称>,[<参数>,<参数>,...]
|
||||
return [formatter.substring(0, firstIndex), params];
|
||||
} else { // 不带参数的格式化器
|
||||
return [formatter, []];
|
||||
}
|
||||
}).filter((formatter) => Array.isArray(formatter)) as FormatterDefines
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 生成可以解析指定标签的正则表达式
|
||||
*
|
||||
* getNestingParamsRegex() -- 能解析{}和[]
|
||||
* getNestingParamsRegex(["<b>","</b>"]),
|
||||
*
|
||||
* @param {...any} tags
|
||||
* @returns
|
||||
*/
|
||||
function getNestingParamsRegex(...tags: ([string, string])[]) {
|
||||
if (tags.length == 0) {
|
||||
tags.push(["{", "}"])
|
||||
tags.push(["[", "]"])
|
||||
}
|
||||
const tagsRegexs = tags.map(([beginTag, endTag]) => {
|
||||
return `(${escapeRegexpStr(beginTag)}1%.*?%1${escapeRegexpStr(endTag)})`
|
||||
})
|
||||
return formatterNestingParamsRegex.replace("__TAG_REGEXP__", tagsRegexs.length > 0 ? tagsRegexs.join("|") + "|" : "")
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 遍历字符串中的 beginTag和endTag,添加辅助序号
|
||||
*
|
||||
* @param {*} str
|
||||
* @param {*} beginTag
|
||||
* @param {*} endTag
|
||||
* @returns
|
||||
*/
|
||||
function addTagFlags(str: string, beginTag = "{", endTag = "}") {
|
||||
let i = 0
|
||||
let flagIndex = 0
|
||||
while (i < str.length) {
|
||||
let beginChars = str.slice(i, i + beginTag.length)
|
||||
let endChars = str.slice(i, i + endTag.length)
|
||||
if (beginChars == beginTag) {
|
||||
flagIndex++
|
||||
str = str.substring(0, i + beginTag.length) + `${flagIndex}%` + str.substring(i + beginTag.length)
|
||||
i += beginTag.length + String(flagIndex).length + 1
|
||||
continue
|
||||
}
|
||||
if (endChars == endTag) {
|
||||
if (flagIndex > 0) {
|
||||
str = str.substring(0, i) + `%${flagIndex}` + str.substring(i)
|
||||
}
|
||||
i += endTag.length + String(flagIndex).length + 1
|
||||
flagIndex--
|
||||
continue
|
||||
}
|
||||
i++
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// 指<div></div>成对标签
|
||||
export type TagPair = [string, string]
|
||||
/**
|
||||
* 增加标签组辅助标识
|
||||
*
|
||||
* addTagHelperFlags("sss",["<div>","</div>"]
|
||||
*
|
||||
* @param {*} str
|
||||
* @param {...any} tags 默认已包括{},[]
|
||||
*/
|
||||
function addTagHelperFlags(str: string, ...tags: TagPair[]) {
|
||||
if (tags.length == 0) {
|
||||
tags.push(["{", "}"])
|
||||
tags.push(["[", "]"])
|
||||
}
|
||||
tags.forEach(tag => {
|
||||
if (str.includes(tag[0]) && str.includes(tag[1])) {
|
||||
str = addTagFlags(str, ...tag)
|
||||
}
|
||||
})
|
||||
return str
|
||||
}
|
||||
|
||||
function removeTagFlags(str: string, beginTag: string, endTag: string) {
|
||||
const regexs:([string,RegExp])[] = [
|
||||
[beginTag, new RegExp(escapeRegexpStr(beginTag) + "\\d+%")],
|
||||
[endTag, new RegExp("%\\d+" + escapeRegexpStr(endTag))]
|
||||
]
|
||||
regexs.forEach(([tag, regex]) => {
|
||||
let matched
|
||||
while ((matched = regex.exec(str)) !== null) {
|
||||
if (matched.index === regex.lastIndex) regex.lastIndex++;
|
||||
str = str.replace(regex, tag)
|
||||
}
|
||||
})
|
||||
return str
|
||||
}
|
||||
|
||||
function removeTagHelperFlags(str: string, ...tags: TagPair[]) {
|
||||
if (tags.length == 0) {
|
||||
tags.push(["{", "}"])
|
||||
tags.push(["[", "]"])
|
||||
}
|
||||
tags.forEach(([beginTag, endTag]) => {
|
||||
if (str.includes(beginTag) && str.includes(endTag)) {
|
||||
str = removeTagFlags(str, beginTag, endTag)
|
||||
}
|
||||
})
|
||||
return str
|
||||
}
|
||||
|
||||
// 提取匹配("a",1,2,'b',{..},[...]),不足:当{}嵌套时无法有效匹配
|
||||
// const formatterParamsRegex = /((([\'\"])(.*?)\3)|(\{.*?\})|(\[.*?\])|([\d]+\.?[\d]?)|((true|false|null)(?=[,\b\s]))|([\w\.]+)|((?<=,)\s*(?=,)))(?<=\s*[,\)]?\s*)/g;
|
||||
|
||||
// 支持解析嵌套的{}和[]参数, 前提是:字符串需要经addTagHelperFlags操作后,会在{}[]等位置添加辅助字符
|
||||
// 解析嵌套的{}和[]参数基本原理:在{}[]等位置添加辅助字符,然后使用正则表达式匹配,匹配到的字符串中包含辅助字符,然后再去除辅助字符
|
||||
const formatterNestingParamsRegex = String.raw`((([\'\"])(.*?)\3))|__TAG_REGEXP__([\d]+\.?[\d]?)|((true|false|null)(?=[,\b\s]))|([\w\.]+)|((?<=,)\s*(?=,))(?<=\s*[,\)]?\s*)`
|
||||
|
||||
/**
|
||||
* 解析格式化器的参数,即解析使用,分割的函数参数
|
||||
*
|
||||
* 采用正则表达式解析
|
||||
* 支持number,boolean,null,String,{},[]的参数,可以识别嵌套的{}和[]
|
||||
*
|
||||
* @param {*} strParams 格式化器参数字符串,即formater(<...参数....>)括号里面的参数,使用,分割
|
||||
* @returns {Array} 返回参数值数组 []
|
||||
*/
|
||||
function parseFormaterParams(strParams: string): any[] {
|
||||
let params = [];
|
||||
let matched;
|
||||
// 1. 预处理: 处理{}和[]嵌套问题,增加嵌套标识
|
||||
strParams = addTagHelperFlags(strParams)
|
||||
try {
|
||||
let regex = new RegExp(getNestingParamsRegex(), "g")
|
||||
while ((matched = regex.exec(strParams)) !== null) {
|
||||
// 这对于避免零宽度匹配的无限循环是必要的
|
||||
if (matched.index === regex.lastIndex) {
|
||||
regex.lastIndex++;
|
||||
}
|
||||
let value: any = matched[0]
|
||||
if (value.trim() == '') {
|
||||
value = null
|
||||
} else if ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"'))) {
|
||||
value = value.substring(1, value.length - 1)
|
||||
value = removeTagHelperFlags(value)
|
||||
} else if ((value.startsWith("{") && value.endsWith("}")) || (value.startsWith('[') && value.endsWith(']'))) {
|
||||
try {
|
||||
value = removeTagHelperFlags(value)
|
||||
value = safeParseJson(value)
|
||||
} catch { }
|
||||
} else if (["true", "false", "null"].includes(value)) {
|
||||
value = JSON.parse(value)
|
||||
} else if (isNumber(value)) {
|
||||
value = parseFloat(value)
|
||||
} else {
|
||||
value = removeTagHelperFlags(String(value))
|
||||
}
|
||||
params.push(value)
|
||||
}
|
||||
} catch { }
|
||||
return params
|
||||
}
|
||||
/**
|
||||
* 创建格式化器
|
||||
*
|
||||
* 格式化器是一个普通的函数,具有以下特点:
|
||||
*
|
||||
* - 函数第一个参数是上一上格式化器的输出
|
||||
* - 支持0-N个简单类型的入参
|
||||
* - 可以是定参,也可以变参
|
||||
* - 格式化器可以在格式化器的$config参数指定一个键值来配置不同语言时的参数
|
||||
*
|
||||
* "currency":createFormatter((value,prefix,suffix, division ,precision,$config)=>{
|
||||
* // 无论在格式化入参数是多少个,经过处理后在此得到prefix,suffix, division ,precision参数已经是经过处理后的参数
|
||||
* 依次读取格式化器的参数合并:
|
||||
* - 创建格式化时的defaultParams参数
|
||||
* - 从当前激活格式化器的$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:"$",
|
||||
* prefix,
|
||||
* suffix,
|
||||
* division,
|
||||
* precision
|
||||
* },
|
||||
* {
|
||||
* normalize:value=>{...},
|
||||
* params:["prefix","suffix", "division" ,"precision"] // 声明参数顺序
|
||||
* configKey:"currency" // 声明特定语言下的配置在$config.currency
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @param {*} fn
|
||||
* @param {*} options 配置参数
|
||||
* @param {*} defaultParams 可选默认值
|
||||
* @returns
|
||||
*/
|
||||
export type Formatter = (this: any, value: string, ...args: any[]) => string
|
||||
export interface FormatterOptions {
|
||||
normalize?: (value: string) => string // 对输入值进行规范化处理,如进行时间格式化时,为了提高更好的兼容性,支持数字时间戳/字符串/Date等,需要对输入值进行处理,如强制类型转换等
|
||||
params?: Record<string, any>, // 可选的,声明参数顺序,如果是变参的,则需要传入null
|
||||
configKey?: string // 声明该格式化器在$config中的路径,支持简单的使用.的路径语法
|
||||
}
|
||||
|
||||
export function createFormatter(fn: Formatter, options?: FormatterOptions, defaultParams?: Record<string, any>) {
|
||||
let opts = assignObject({
|
||||
normalize: null, // 对输入值进行规范化处理,如进行时间格式化时,为了提高更好的兼容性,支持数字时间戳/字符串/Date等,需要对输入值进行处理,如强制类型转换等
|
||||
params: null, // 可选的,声明参数顺序,如果是变参的,则需要传入null
|
||||
configKey: null // 声明该格式化器在$config中的路径,支持简单的使用.的路径语法
|
||||
}, options)
|
||||
|
||||
// 最后一个参数是传入activeFormatterConfig参数
|
||||
// 并且格式化器的this指向的是activeFormatterConfig
|
||||
const $formatter = function (this: any, value: string, ...args: any[]) {
|
||||
let finalValue = value
|
||||
// 1. 输入值规范处理,主要是进行类型转换,确保输入的数据类型及相关格式的正确性,提高数据容错性
|
||||
if (isFunction(opts.normalize)) {
|
||||
try {
|
||||
finalValue = opts.normalize(finalValue)
|
||||
} catch { }
|
||||
}
|
||||
// 2. 读取activeFormatterConfig
|
||||
let activeFormatterConfigs = args.length > 0 ? args[args.length - 1] : {}
|
||||
if (!isPlainObject(activeFormatterConfigs)) activeFormatterConfigs = {}
|
||||
// 3. 从当前语言的激活语言中读取配置参数
|
||||
const formatterConfig = assignObject({}, defaultParams, getByPath(activeFormatterConfigs, opts.configKey, { defaultValue: {} }))
|
||||
let finalArgs
|
||||
if (opts.params == null) {// 如果格式化器支持变参,则需要指定params=null
|
||||
finalArgs = args.slice(0, args.length - 1)
|
||||
} else { // 具有固定的参数个数
|
||||
finalArgs = opts.params.map((param: string) => {
|
||||
let paramValue = getByPath(formatterConfig, param, undefined)
|
||||
return isFunction(paramValue) ? paramValue(finalValue) : paramValue
|
||||
})
|
||||
// 4. 将翻译函数执行格式化器时传入的参数覆盖默认参数
|
||||
for (let i = 0; i < finalArgs.length; i++) {
|
||||
if (i == args.length - 1) break // 最后一参数是配置
|
||||
if (args[i] !== undefined) finalArgs[i] = args[i]
|
||||
}
|
||||
}
|
||||
return fn.call(this, finalValue, ...finalArgs, formatterConfig)
|
||||
}
|
||||
$formatter.configurable = true
|
||||
return $formatter
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建支持弹性参数的格式化器
|
||||
* 弹性参数指的是该格式式化器支持两种传参数形式:
|
||||
* - 位置参数: { value | format(a,b,c,d,e) }
|
||||
* - 对象参数: { value | format({a,b,c,e}) }
|
||||
*
|
||||
* 弹性参数的目的是只需要位置参数中的第N参数时,简化传参
|
||||
* 例:
|
||||
* 上例中,我们只需要传入e参数,则不得不使用{ value | format(,,,,e) }
|
||||
* 弹性参数允许使用 { value | format({e:<value>}) }
|
||||
*
|
||||
* 请参阅currencyFormatter用法
|
||||
*
|
||||
* @param {*} fn
|
||||
* @param {*} options
|
||||
* @param {*} defaultParams
|
||||
*/
|
||||
export const createFlexFormatter = function (fn: Formatter, options: FormatterOptions, defaultParams?: Record<string, any>) {
|
||||
const opts = assignObject({
|
||||
params: {}
|
||||
}, options)
|
||||
const $flexFormatter = Formatter(function (this: any, value: string, ...args: string[]) {
|
||||
// 1. 最后一个参数总是代表格式化器的参数,不同语言不一样
|
||||
let $config = args[args.length - 1] as unknown as Record<string, any>
|
||||
// 2. 从语言配置中读取默认参数
|
||||
let finalParams = (options.params || {}).reduce((r: Record<string, any>, name: string) => {
|
||||
r[name] = $config[name] == undefined ? (defaultParams || {})[name] : $config[name]
|
||||
return r
|
||||
}, {})
|
||||
// 3. 从格式化器中传入的参数具有最高优先级,覆盖默认参数
|
||||
if (args.length == 2 && isPlainObject(args[0])) { // 一个参数且是{}
|
||||
Object.assign(finalParams, { format: "custom" }, args[0])
|
||||
} else { // 位置参数,如果是空则代表
|
||||
for (let i = 0; i < args.length - 1; i++) {
|
||||
if (args[i] !== undefined) finalParams[opts.params[i]] = args[i]
|
||||
}
|
||||
}
|
||||
return fn.call(this, value, finalParams, $config)
|
||||
}, { ...options, params: null }) // 变参工式化器需要指定params=null
|
||||
return $flexFormatter
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const FlexFormatter = createFlexFormatter
|
||||
|
@ -2,12 +2,12 @@
|
||||
* 日期时间格式化器
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
const { isFunction,isPlainObject} = require("../utils")
|
||||
const { Formatter } = require("../formatter")
|
||||
const { dateFormatter,quarterFormatter,monthFormatter,weekdayFormatter,timeFormatter,relativeTimeFormatter } = require("../datatypes/datetime")
|
||||
const { numberFormartter } = require("../datatypes/numeric")
|
||||
const { currencyFormatter } = require("../datatypes/currency")
|
||||
import { Formatter } fom "../formatter"
|
||||
import { dateFormatter,quarterFormatter,monthFormatter,weekdayFormatter,timeFormatter,relativeTimeFormatter } from "../datatypes/datetime"
|
||||
import { numberFormartter } from "../datatypes/numeric"
|
||||
import { currencyFormatter } from "../datatypes/currency"
|
||||
|
||||
|
||||
|
||||
@ -70,7 +70,7 @@ const { currencyFormatter } = require("../datatypes/currency")
|
||||
// })
|
||||
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
// 配置参数
|
||||
$config:{
|
||||
datetime : {
|
@ -3,14 +3,14 @@
|
||||
* 被注册到全局语言管理器
|
||||
*/
|
||||
|
||||
const enFormatters = require("./en")
|
||||
const zhFormatters = require("./zh")
|
||||
const defaultFormatters = require("./default")
|
||||
|
||||
module.exports = {
|
||||
import enFormatters from "./en"
|
||||
import zhFormatters from "./zh"
|
||||
import defaultFormatters from "./default"
|
||||
|
||||
export default {
|
||||
"*":{
|
||||
...enFormatters,
|
||||
...defaultFormatters
|
||||
},
|
||||
zh:zhFormatters
|
||||
}
|
||||
}
|
8
packages/runtime/src/index.ts
Normal file
8
packages/runtime/src/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import replaceAll from "string.prototype.replaceall"
|
||||
replaceAll.shim()
|
||||
|
||||
export * from "./types"
|
||||
export * from "./manager"
|
||||
export * from "./scope"
|
||||
export * from "./formatter"
|
||||
export * from "./utils"
|
@ -28,8 +28,12 @@
|
||||
*
|
||||
*/
|
||||
|
||||
const { getDataTypeName,isPlainObject,isFunction,replaceAll } = require("./utils");
|
||||
const { parseFormatters } = require("./formatter")
|
||||
import { getDataTypeName } from "./utils"
|
||||
import { isNumber } from "flex-tools/typecheck/isNumber"
|
||||
import { isPlainObject } from "flex-tools/typecheck/isPlainObject"
|
||||
import { isFunction } from "flex-tools/typecheck/isFunction"
|
||||
import { parseFormatters } from "./formatter"
|
||||
import { VoerkaI18nScope } from "./scope"
|
||||
|
||||
// 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter }
|
||||
// 支持参数: { var | formatter(x,x,..) | formatter }
|
||||
@ -100,7 +104,7 @@ function getInterpolatedVars(str) {
|
||||
* @param {Boolean} replaceAll 是否替换所有插值变量,当使用命名插值时应置为true,当使用位置插值时应置为false
|
||||
* @returns 返回替换后的字符串
|
||||
*/
|
||||
function forEachInterpolatedVars(str, replacer, options = {}) {
|
||||
function forEachInterpolatedVars(str:string, replacer, options = {}) {
|
||||
let result = str, matched;
|
||||
let opts = Object.assign({replaceAll: true },options);
|
||||
varWithPipeRegexp.lastIndex = 0;
|
||||
@ -134,7 +138,7 @@ function forEachInterpolatedVars(str, replacer, options = {}) {
|
||||
* @param {*} scope
|
||||
* @param {*} activeLanguage
|
||||
*/
|
||||
function resetScopeCache(scope, activeLanguage = null) {
|
||||
function resetScopeCache(scope:VoerkaI18nScope, activeLanguage:string | null) {
|
||||
scope.$cache = { activeLanguage, typedFormatters: {}, formatters: {} };
|
||||
}
|
||||
|
||||
@ -201,7 +205,7 @@ function getDataTypeDefaultFormatter(scope, activeLanguage, dataType) {
|
||||
* @param {*} name 格式化器名称
|
||||
* @returns {Function} 格式化函数
|
||||
*/
|
||||
function getFormatter(scope, activeLanguage, name) {
|
||||
function getFormatter(scope:VoerkaI18nScope, activeLanguage:string, name:string) {
|
||||
// 1. 从缓存中直接读取: 缓存格式化器引用,避免重复检索
|
||||
if (!scope.$cache) resetScopeCache(scope);
|
||||
if (scope.$cache.activeLanguage === activeLanguage) {
|
||||
@ -422,7 +426,7 @@ function getFormattedValue(scope, activeLanguage, formatters, value, template) {
|
||||
* @param {*} template
|
||||
* @returns
|
||||
*/
|
||||
function replaceInterpolatedVars(template, ...args) {
|
||||
export function replaceInterpolatedVars(this:VoerkaI18nScope,template:string, ...args:any[]) {
|
||||
const scope = this;
|
||||
// 当前激活语言
|
||||
const activeLanguage = scope.global.activeLanguage;
|
@ -1,11 +1,10 @@
|
||||
const {DataTypes,getDataTypeName,isFunction,deepMerge,toBoolean} = require("./utils")
|
||||
const {createFormatter,Formatter,FlexFormatter,createFlexFormatter} = require("./formatter")
|
||||
const { toDate } = require("./datatypes/datetime")
|
||||
const { toNumber } = require("./datatypes/numeric")
|
||||
const EventEmitter = require("./eventemitter")
|
||||
const inlineFormatters = require("./formatters")
|
||||
const VoerkaI18nScope = require("./scope")
|
||||
const { translate } = require("./translate")
|
||||
import { isFunction } from "flex-tools/typecheck/isFunction"
|
||||
import { deepMerge } from "flex-tools/object/deepMerge"
|
||||
import {DataTypes} from "./utils"
|
||||
import { EventEmitter } from "./eventemitter"
|
||||
import inlineFormatters from "./formatters"
|
||||
import { VoerkaI18nLanguage, VoerkaI18nScope, VoerkaI18nFormatters, VoerkI18nMessageLoader, VoerkaI18nFormatter } from "./scope"
|
||||
|
||||
|
||||
// 默认语言配置
|
||||
const defaultLanguageSettings = {
|
||||
@ -17,8 +16,15 @@ const defaultLanguageSettings = {
|
||||
{name:"zh",title:"中文",default:true},
|
||||
{name:"en",title:"英文"}
|
||||
]
|
||||
}
|
||||
} as VoerkaI18nManagerOptions
|
||||
|
||||
export interface VoerkaI18nManagerOptions {
|
||||
debug?: boolean
|
||||
defaultLanguage: string
|
||||
activeLanguage: string
|
||||
formatters: VoerkaI18nFormatters
|
||||
languages: VoerkaI18nLanguage[]
|
||||
}
|
||||
/**
|
||||
* 多语言管理类
|
||||
*
|
||||
@ -34,37 +40,43 @@ const defaultLanguageSettings = {
|
||||
* VoerkaI18n.off("change",(language)=>{})
|
||||
*
|
||||
* */
|
||||
class VoerkaI18nManager extends EventEmitter{
|
||||
constructor(settings={}){
|
||||
|
||||
export class VoerkaI18nManager extends EventEmitter{
|
||||
static instance?:VoerkaI18nManager
|
||||
#options?:Required<VoerkaI18nManagerOptions>
|
||||
#scopes:VoerkaI18nScope[] = []
|
||||
#defaultMessageLoader?:VoerkI18nMessageLoader
|
||||
constructor(options?:VoerkaI18nManagerOptions){
|
||||
super()
|
||||
if(VoerkaI18nManager.instance!=null){
|
||||
if(VoerkaI18nManager.instance){
|
||||
return VoerkaI18nManager.instance;
|
||||
}
|
||||
VoerkaI18nManager.instance = this;
|
||||
this._settings = deepMerge(defaultLanguageSettings,settings)
|
||||
this._scopes=[] // 保存VoerkaI18nScope实例
|
||||
this._defaultMessageLoader = null // 默认语言包加载器
|
||||
this.#options = deepMerge(defaultLanguageSettings,options) as Required<VoerkaI18nManagerOptions>
|
||||
this.#scopes=[] // 保存VoerkaI18nScope实例
|
||||
}
|
||||
get settings(){ return this._settings } // 配置参数
|
||||
get scopes(){ return this._scopes } // 注册的报有VoerkaI18nScope实例q
|
||||
get activeLanguage(){ return this._settings.activeLanguage} // 当前激活语言 名称
|
||||
get defaultLanguage(){ return this._settings.defaultLanguage} // 默认语言名称
|
||||
get languages(){ return this._settings.languages} // 支持的语言列表
|
||||
get formatters(){ return this._settings.formatters } // 内置格式化器{*:{$config,$types,...},zh:{$config,$types,...},en:{$config,$types,...}}
|
||||
get defaultMessageLoader(){ return this._defaultMessageLoader} // 默认语言包加载器
|
||||
get debug(){return this.#options!.debug}
|
||||
get options(){ return this.#options! } // 配置参数
|
||||
get scopes(){ return this.#scopes } // 注册的报有VoerkaI18nScope实例q
|
||||
get activeLanguage(){ return this.#options!.activeLanguage} // 当前激活语言 名称
|
||||
get defaultLanguage(){ return this.#options!.defaultLanguage} // 默认语言名称
|
||||
get languages(){ return this.#options!.languages} // 支持的语言列表
|
||||
get formatters(){ return this.#options!.formatters } // 内置格式化器{*:{$config,$types,...},zh:{$config,$types,...},en:{$config,$types,...}}
|
||||
get defaultMessageLoader(){ return this.#defaultMessageLoader} // 默认语言包加载器
|
||||
|
||||
// 通过默认加载器加载文件
|
||||
async loadMessagesFromDefaultLoader(newLanguage,scope){
|
||||
if(!isFunction(this._defaultMessageLoader)) return //throw new Error("No default message loader specified")
|
||||
return await this._defaultMessageLoader.call(scope,newLanguage,scope)
|
||||
async loadMessagesFromDefaultLoader(newLanguage:string,scope:VoerkaI18nScope){
|
||||
if(this.#defaultMessageLoader && isFunction(this.#defaultMessageLoader)){
|
||||
return await this.#defaultMessageLoader.call(scope,newLanguage,scope)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 切换语言
|
||||
*/
|
||||
async change(language){
|
||||
if(this.languages.findIndex(lang=>lang.name === language)!==-1 || isFunction(this._defaultMessageLoader)){
|
||||
async change(language:string){
|
||||
if(this.languages.findIndex(lang=>lang.name === language)!==-1 || isFunction(this.#defaultMessageLoader)){
|
||||
await this._refreshScopes(language) // 通知所有作用域刷新到对应的语言包
|
||||
this._settings.activeLanguage = language
|
||||
this.#options!.activeLanguage = language
|
||||
await this.emit(language) // 触发语言切换事件
|
||||
return language
|
||||
}else{
|
||||
@ -75,9 +87,9 @@ const defaultLanguageSettings = {
|
||||
* 当切换语言时调用此方法来加载更新语言包
|
||||
* @param {*} newLanguage
|
||||
*/
|
||||
async _refreshScopes(newLanguage){
|
||||
async _refreshScopes(newLanguage:string){
|
||||
try{
|
||||
const scopeRefreshers = this._scopes.map(scope=>{
|
||||
const scopeRefreshers = this.#scopes.map(scope=>{
|
||||
return scope.refresh(newLanguage)
|
||||
})
|
||||
if(Promise.allSettled){
|
||||
@ -85,7 +97,7 @@ const defaultLanguageSettings = {
|
||||
}else{
|
||||
await Promise.all(scopeRefreshers)
|
||||
}
|
||||
}catch(e){
|
||||
}catch(e:any){
|
||||
console.warn("Error while refreshing i18n scopes:",e.message)
|
||||
}
|
||||
}
|
||||
@ -98,11 +110,11 @@ const defaultLanguageSettings = {
|
||||
*
|
||||
* @param {*} scope
|
||||
*/
|
||||
async register(scope){
|
||||
async register(scope:VoerkaI18nScope){
|
||||
if(!(scope instanceof VoerkaI18nScope)){
|
||||
throw new TypeError("Scope must be an instance of VoerkaI18nScope")
|
||||
}
|
||||
this._scopes.push(scope)
|
||||
this.#scopes.push(scope)
|
||||
await scope.refresh(this.activeLanguage)
|
||||
}
|
||||
/**
|
||||
@ -119,16 +131,15 @@ const defaultLanguageSettings = {
|
||||
|
||||
* @param {*} formatter
|
||||
language : 声明该格式化器适用语言
|
||||
isGlobal : 注册到全局
|
||||
*/
|
||||
registerFormatter(name,formatter,{language="*"}={}){
|
||||
registerFormatter(name:string,formatter:VoerkaI18nFormatter,{language="*"}:{language:string | string[] | '*'}){
|
||||
if(!isFunction(formatter) || typeof(name)!=="string"){
|
||||
throw new TypeError("Formatter must be a function")
|
||||
}
|
||||
language = Array.isArray(language) ? language : (language ? language.split(",") : [])
|
||||
language.forEach(lng=>{
|
||||
if(DataTypes.includes(name)){
|
||||
this.formatters[lng].$types[name] = formatter
|
||||
this.formatters[lng].$types![name] = formatter
|
||||
}else{
|
||||
this.formatters[lng][name] = formatter
|
||||
}
|
||||
@ -137,37 +148,23 @@ const defaultLanguageSettings = {
|
||||
/**
|
||||
* 注册默认文本信息加载器
|
||||
*/
|
||||
registerDefaultLoader(fn){
|
||||
registerDefaultLoader(fn:VoerkI18nMessageLoader){
|
||||
if(!isFunction(fn)) throw new Error("The default loader must be a async function or promise returned")
|
||||
this._defaultMessageLoader = fn
|
||||
this.#defaultMessageLoader = fn
|
||||
this.refresh()
|
||||
}
|
||||
async refresh(){
|
||||
try{
|
||||
let requests = this._scopes.map(scope=>scope.refresh())
|
||||
let requests = this.#scopes.map(scope=>scope.refresh())
|
||||
if(Promise.allSettled){
|
||||
await Promise.allSettled(requests)
|
||||
}else{
|
||||
await Promise.all(requests)
|
||||
}
|
||||
}catch(e){
|
||||
if(this._debug) console.error(`Error while refresh voerkai18n scopes:${e.message}`)
|
||||
}catch(e:any){
|
||||
if(this.debug) console.error(`Error while refresh voerkai18n scopes:${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports ={
|
||||
toDate,
|
||||
toNumber,
|
||||
toBoolean,
|
||||
deepMerge,
|
||||
VoerkaI18nManager,
|
||||
translate,
|
||||
VoerkaI18nScope,
|
||||
createFormatter,
|
||||
Formatter,
|
||||
createFlexFormatter,
|
||||
FlexFormatter,
|
||||
getDataTypeName
|
||||
}
|
||||
|
@ -1,23 +1,55 @@
|
||||
const { DataTypes,isPlainObject, isFunction, getByPath, deepMixin,deepClone } = require("./utils");
|
||||
const { translate } = require("./translate")
|
||||
import { DataTypes } from "./utils"
|
||||
import { isPlainObject } from "flex-tools/typecheck/isPlainObject"
|
||||
import { isFunction } from "flex-tools/typecheck/isFunction"
|
||||
import { deepClone } from "flex-tools/object/deepClone"
|
||||
import { get as getByPath } from "flex-tools/object/get"
|
||||
import { translate } from "./translate"
|
||||
import { deepMerge } from "flex-tools/object/deepMerge"
|
||||
import { assignObject } from "flex-tools/object/assignObject"
|
||||
import type {VoerkaI18nManager } from "./manager"
|
||||
import type { VoerkaI18nFormatterConfigs, VoerkaI18nFormatters, Voerkai18nIdMap, VoerkaI18nLanguage, VoerkaI18nLanguageMessages, VoerkaI18nLanguagePack, VoerkaI18nScopeCache, VoerkaI18nTranslate, VoerkI18nLoaders } from "./types"
|
||||
|
||||
module.exports = class VoerkaI18nScope {
|
||||
constructor(options = {}, callback) {
|
||||
this._id = options.id || Date.now().toString() + parseInt(Math.random() * 1000);
|
||||
this._debug = options.debug == undefined ? process && process.env && process.env.NODE_ENV === "development" : options.debug; // 当出错时是否在控制台台输出错误信息
|
||||
this._languages = options.languages; // 当前作用域支持的语言列表
|
||||
this._defaultLanguage = options.defaultLanguage || "zh"; // 默认语言名称
|
||||
this._activeLanguage = options.activeLanguage; // 当前语言名称
|
||||
this._default = options.default; // 默认语言包
|
||||
this._messages = options.messages; // 当前语言包
|
||||
this._idMap = options.idMap; // 消息id映射列表
|
||||
this._formatters = options.formatters; // 当前作用域的格式化函数列表{<lang>: {$types,$config,[格式化器名称]: () => {},[格式化器名称]: () => {}}}
|
||||
this._loaders = options.loaders; // 异步加载语言文件的函数列表
|
||||
this._global = null; // 引用全局VoerkaI18n配置,注册后自动引用
|
||||
this._patchMessages = {}; // 语言包补丁信息{<language>: {....},<language>:{....}}
|
||||
this._refreshing = false; // 正在加载语言包标识
|
||||
export interface VoerkaI18nScopeOptions {
|
||||
id?: string
|
||||
debug?: boolean
|
||||
languages: VoerkaI18nLanguage[] // 当前作用域支持的语言列表
|
||||
defaultLanguage: string // 默认语言名称
|
||||
activeLanguage: string // 当前语言名称
|
||||
default: VoerkaI18nLanguageMessages // 默认语言包
|
||||
messages: VoerkaI18nLanguageMessages // 当前语言包
|
||||
idMap: Voerkai18nIdMap // 消息id映射列表
|
||||
formatters: VoerkaI18nFormatters // 当前作用域的格式化函数列表{<lang>: {$types,$config,[格式化器名称]: () => {},[格式化器名称]: () => {}}}
|
||||
loaders: VoerkI18nLoaders; // 异步加载语言文件的函数列表
|
||||
}
|
||||
|
||||
export class VoerkaI18nScope {
|
||||
#options:Required<VoerkaI18nScopeOptions>
|
||||
#global:VoerkaI18nManager // 引用全局VoerkaI18nManager配置,注册后自动引用
|
||||
#refreshing:boolean = false
|
||||
#patchMessages:VoerkaI18nLanguagePack = {}
|
||||
#t:VoerkaI18nTranslate
|
||||
#activeFormatters:VoerkaI18nFormatters ={}
|
||||
#activeFormatterConfig: VoerkaI18nFormatterConfigs={}
|
||||
#cache:VoerkaI18nScopeCache ={}
|
||||
#messages:VoerkaI18nLanguageMessages
|
||||
constructor(options:VoerkaI18nScopeOptions, callback:Function) {
|
||||
this.#options = assignObject({
|
||||
id : Date.now().toString() + parseInt(String(Math.random() * 1000)),
|
||||
debug : false,
|
||||
languages : {}, // 当前作用域支持的语言列表
|
||||
defaultLanguage: "zh", // 默认语言名称
|
||||
activeLanguage : "zh", // 当前语言名称
|
||||
default : {}, // 默认语言包
|
||||
messages : {}, // 当前语言包
|
||||
idMap : {}, // 消息id映射列表
|
||||
formatters : {}, // 当前作用域的格式化函数列表{<lang>: {$types,$config,[格式化器名称]: () => {},[格式化器名称]: () => {}}}
|
||||
loaders : {} // 异步加载语言文件的函数列表
|
||||
},options) as Required<VoerkaI18nScopeOptions>
|
||||
|
||||
this.#patchMessages = {}; // 语言包补丁信息{<language>: {....},<language>:{....}}
|
||||
this.#refreshing = false; // 正在加载语言包标识
|
||||
// 用来缓存格式化器的引用,当使用格式化器时可以直接引用,减少检索遍历
|
||||
this.$cache = {
|
||||
this.#cache = {
|
||||
activeLanguage : null,
|
||||
typedFormatters: {},
|
||||
formatters : {},
|
||||
@ -27,53 +59,49 @@ module.exports = class VoerkaI18nScope {
|
||||
if (!globalThis.VoerkaI18n) {
|
||||
const { VoerkaI18nManager } = require("./");
|
||||
globalThis.VoerkaI18n = new VoerkaI18nManager({
|
||||
debug : this._debug,
|
||||
defaultLanguage: this._defaultLanguage,
|
||||
activeLanguage : this._activeLanguage,
|
||||
debug : this.debug,
|
||||
defaultLanguage: this.defaultLanguage,
|
||||
activeLanguage : this.activeLanguage,
|
||||
languages : options.languages,
|
||||
});
|
||||
}
|
||||
this._t = translate.bind(this)
|
||||
this._global = globalThis.VoerkaI18n;
|
||||
this.#t = translate.bind(this)
|
||||
this.#global = globalThis.VoerkaI18n as unknown as VoerkaI18nManager;
|
||||
this._initFormatters(this.activeLanguage) // 初始化活动的格式化器
|
||||
this._mergePatchedMessages(); // 从本地缓存中读取并合并补丁语言包
|
||||
this._patch(this._messages, this.activeLanguage); // 延后执行补丁命令,该命令会向远程下载补丁包
|
||||
this._patch(this.messages, this.activeLanguage); // 延后执行补丁命令,该命令会向远程下载补丁包
|
||||
this.register(callback); // 在全局注册作用域
|
||||
}
|
||||
get id() {return this._id;} // 作用域唯一id
|
||||
get debug() {return this._debug;} // 调试开关
|
||||
get defaultLanguage() {return this._defaultLanguage;} // 默认语言名称
|
||||
get activeLanguage() {return this._global.activeLanguage;} // 默认语言名称
|
||||
get default() {return this._default;} // 默认语言包
|
||||
get messages() {return this._messages; } // 当前语言包
|
||||
get idMap() {return this._idMap;} // 消息id映射列表
|
||||
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,$config,[格式化器名称]: () = >{},[格式化器名称]: () => {}}}
|
||||
get activeFormatters() {return this._activeFormatters} // 当前作用域激活的格式化器定义 {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () = >{}}
|
||||
get activeFormatterConfig(){return this._activeFormatterConfig} // 当前格式化器合并后的配置参数,参数已经合并了全局格式化器中的参数
|
||||
get translate(){return this._t}
|
||||
get t(){return this._t}
|
||||
get id() {return this.#options.id;} // 作用域唯一id
|
||||
get debug() {return this.#options.debug;} // 调试开关
|
||||
get defaultLanguage() {return this.#options.defaultLanguage;} // 默认语言名称
|
||||
get activeLanguage() {return this.#global.activeLanguage;} // 默认语言名称
|
||||
get default() {return this.#options.default;} // 默认语言包
|
||||
get messages() {return this.#options.messages; } // 当前语言包
|
||||
get idMap() {return this.#options.idMap;} // 消息id映射列表
|
||||
get languages() {return this.#options.languages;} // 当前作用域支持的语言列表[{name,title,fallback}]
|
||||
get loaders() { return this.#options.loaders;} // 异步加载语言文件的函数列表
|
||||
get global() { return this.#global;} // 引用全局VoerkaI18n配置,注册后自动引用
|
||||
get formatters() { return this.#options.formatters;} // 当前作用域的所有格式化器定义 {<语言名称>: {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () => {}}}
|
||||
get activeFormatters() {return this.#activeFormatters} // 当前作用域激活的格式化器定义 {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () = >{}}
|
||||
get activeFormatterConfig(){return this.#activeFormatterConfig} // 当前格式化器合并后的配置参数,参数已经合并了全局格式化器中的参数
|
||||
get cache(){return this.#cache }
|
||||
get translate(){return this.#t}
|
||||
get t(){return this.#t}
|
||||
|
||||
/**
|
||||
* 对输入的语言配置进行处理
|
||||
* - 将en配置为默认回退语言
|
||||
*/
|
||||
_initiLanguages(){
|
||||
if(!Array.isArray(this._languages)){
|
||||
if(!Array.isArray(this.languages)){
|
||||
console.warn("[VoerkaI18n] 无效的语言配置")
|
||||
this._languages = [
|
||||
{
|
||||
name: "zh",
|
||||
title: "中文"
|
||||
},
|
||||
{
|
||||
name: "en",
|
||||
title: "英文"
|
||||
}
|
||||
this.#options.languages = [
|
||||
{name: "zh",title: "中文"},
|
||||
{name: "en",title: "英文"}
|
||||
]
|
||||
}
|
||||
Object.entries(this._languages).forEach(([name,language])=>{
|
||||
Object.entries(this.languages).forEach(([name,language])=>{
|
||||
if(!language.fallback) language.fallback = "en"
|
||||
})
|
||||
}
|
||||
@ -82,7 +110,7 @@ module.exports = class VoerkaI18nScope {
|
||||
* 在全局注册作用域当前作用域
|
||||
* @param {*} callback 注册成功后的回调
|
||||
*/
|
||||
register(callback) {
|
||||
register(callback:Function) {
|
||||
if (!isFunction(callback)) callback = () => {};
|
||||
this.global.register(this).then(callback).catch(callback);
|
||||
}
|
||||
@ -103,7 +131,7 @@ module.exports = class VoerkaI18nScope {
|
||||
语言名称,语言名称数组,或者使用,分割的语言名称字符串
|
||||
asGlobal : 注册到全局
|
||||
*/
|
||||
registerFormatter(name, formatter, { language = "*", global : asGlobal } = {}) {
|
||||
registerFormatter(name:string, formatter:VoerkaI18nFormatter, { language = "*", global : asGlobal } = {}) {
|
||||
if (!isFunction(formatter) || typeof name !== "string") {
|
||||
throw new TypeError("Formatter must be a function");
|
||||
}
|
||||
@ -128,7 +156,7 @@ module.exports = class VoerkaI18nScope {
|
||||
* @param {*} formatters ={"*",zh:{...},en:{...}}
|
||||
* @returns
|
||||
*/
|
||||
registerFormatters(formatters,asGlobal=false) {
|
||||
registerFormatters(formatters:VoerkaI18nFormatters,asGlobal=false) {
|
||||
Object.entries(formatters).forEach(([language,fns])=>{
|
||||
Object.entries(fns).forEach(([name,formatter])=>{
|
||||
this.registerFormatter(name,formatter,{language,global:asGlobal})
|
||||
@ -138,27 +166,27 @@ module.exports = class VoerkaI18nScope {
|
||||
/**
|
||||
* 初始化格式化器
|
||||
* 激活和默认语言的格式化器采用静态导入的形式,而没有采用异步块的形式,这是为了确保首次加载时的能马上读取,而不能采用延迟加载方式
|
||||
* _activeFormatters={$config:{...},$types:{...},[格式化器名称]:()=>{...},[格式化器名称]:()=>{...},...}}
|
||||
* #activeFormatters={$config:{...},$types:{...},[格式化器名称]:()=>{...},[格式化器名称]:()=>{...},...}}
|
||||
*/
|
||||
_initFormatters(newLanguage){
|
||||
private _initFormatters(newLanguage:string){
|
||||
// 全局格式化器,用来注册到全局
|
||||
Object.entries(this._formatters).forEach(([langName,formatters])=>{
|
||||
Object.entries(this.formatters).forEach(([langName,formatters])=>{
|
||||
if(formatters.global===true){
|
||||
this.registerFormatters({[langName]:formatters},true)
|
||||
}else if(isPlainObject(formatters.global)){
|
||||
this.registerFormatters({[langName]:formatters.global},true)
|
||||
}
|
||||
})
|
||||
this._activeFormatters = {}
|
||||
this.activeFormatters = {}
|
||||
try {
|
||||
if (newLanguage in this._formatters) {
|
||||
this._activeFormatters = this._formatters[newLanguage];
|
||||
this.#activeFormatters = this._formatters[newLanguage];
|
||||
} else {
|
||||
if (this._debug) console.warn(`Not initialize <${newLanguage}> formatters.`);
|
||||
if (this.debug) console.warn(`Not initialize <${newLanguage}> formatters.`);
|
||||
}
|
||||
this._generateFormatterConfig(newLanguage)
|
||||
} catch (e) {
|
||||
if (this._debug) console.error(`Error while initialize ${newLanguage} formatters: ${e.message}`);
|
||||
if (this.debug) console.error(`Error while initialize ${newLanguage} formatters: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,22 +202,22 @@ module.exports = class VoerkaI18nScope {
|
||||
*
|
||||
* @param {*} language
|
||||
*/
|
||||
async _changeFormatters(newLanguage) {
|
||||
async _changeFormatters(newLanguage:string) {
|
||||
try {
|
||||
if (newLanguage in this._formatters) {
|
||||
let loader = this._formatters[newLanguage];
|
||||
if (newLanguage in this.formatters) {
|
||||
let loader = this.formatters[newLanguage];
|
||||
if (isPlainObject(loader)) {
|
||||
this._activeFormatters = loader;
|
||||
this.#activeFormatters = loader as unknown as VoerkaI18nFormatters;
|
||||
} else if (isFunction(loader)) {
|
||||
this._activeFormatters = (await loader()).default;
|
||||
this.#activeFormatters = (await (loader as Function).call(this)).default;
|
||||
}
|
||||
// 合并生成格式化器的配置参数,当执行格式化器时该参数将被传递给格式化器
|
||||
this._generateFormatterConfig(newLanguage)
|
||||
} else {
|
||||
if (this._debug) console.warn(`Not configured <${newLanguage}> formatters.`);
|
||||
if (this.debug) console.warn(`Not configured <${newLanguage}> formatters.`);
|
||||
}
|
||||
} catch (e) {
|
||||
if (this._debug) console.error(`Error loading ${newLanguage} formatters: ${e.message}`);
|
||||
} catch (e:any) {
|
||||
if (this.debug) console.error(`Error loading ${newLanguage} formatters: ${e.message}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
@ -198,25 +226,25 @@ module.exports = class VoerkaI18nScope {
|
||||
* - global.formatters[language].$config
|
||||
* - scope.activeFormatters.$config 当前优先
|
||||
*/
|
||||
_generateFormatterConfig(language){
|
||||
_generateFormatterConfig(language:string){
|
||||
try{
|
||||
const fallbackLanguage = this.getLanguage(language).fallback;
|
||||
let configSources = [
|
||||
getByPath(this._global.formatters,`${fallbackLanguage}.$config`,{}),
|
||||
getByPath(this._global.formatters,"*.$config",{}),
|
||||
getByPath(this._formatters,`${fallbackLanguage}.$config`,{}),
|
||||
getByPath(this.#global.formatters,`${fallbackLanguage}.$config`,{}),
|
||||
getByPath(this.#global.formatters,"*.$config",{}),
|
||||
getByPath(this.formatters,`${fallbackLanguage}.$config`,{}),
|
||||
getByPath(this._formatter,"*.$config",{}),
|
||||
getByPath(this._global.formatters,`${language}.$config`,{}),
|
||||
getByPath(this._activeFormatters,"$config",{})
|
||||
getByPath(this.#global.formatters,`${language}.$config`,{}),
|
||||
getByPath(this.#activeFormatters,"$config",{})
|
||||
]
|
||||
return this._activeFormatterConfig = configSources.reduce((finalConfig, config)=>{
|
||||
if(isPlainObject(config)) deepMixin(finalConfig,config)
|
||||
return this.#activeFormatterConfig = configSources.reduce((finalConfig, config)=>{
|
||||
if(isPlainObject(config)) deepMerge(finalConfig,config,{newObject:false})
|
||||
return finalConfig
|
||||
},deepClone(getByPath(this._global.formatters,`*.$config`,{})))
|
||||
},deepClone(getByPath(this.#global.formatters,`*.$config`,{})))
|
||||
|
||||
}catch(e){
|
||||
if(this.debug) console.error(`Error while generate <${language}> formatter options: `,e)
|
||||
return this._activeFormatters.$config || {}
|
||||
return this.#activeFormatters.$config || {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -224,7 +252,7 @@ module.exports = class VoerkaI18nScope {
|
||||
* 注册默认文本信息加载器
|
||||
* @param {Function} 必须是异步函数或者是返回Promise
|
||||
*/
|
||||
registerDefaultLoader(fn) {
|
||||
registerDefaultLoader(fn:Function) {
|
||||
this.global.registerDefaultLoader(fn);
|
||||
}
|
||||
/**
|
||||
@ -232,37 +260,37 @@ module.exports = class VoerkaI18nScope {
|
||||
* @param {*} language
|
||||
* @returns
|
||||
*/
|
||||
getLanguage(language) {
|
||||
let index = this._languages.findIndex((lng) => lng.name == language);
|
||||
if (index !== -1) return this._languages[index];
|
||||
getLanguage(language:string) {
|
||||
let index = this.languages.findIndex((lng) => lng.name == language);
|
||||
if (index !== -1) return this.languages[index];
|
||||
}
|
||||
/**
|
||||
* 返回是否存在指定的语言
|
||||
* @param {*} language 语言名称
|
||||
* @returns
|
||||
*/
|
||||
hasLanguage(language) {
|
||||
return this._languages.indexOf((lang) => lang.name == language) !== -1;
|
||||
hasLanguage(language:string) {
|
||||
return this.languages.indexOf((lang:VoerkaI18nLanguage) => lang.name == language) !== -1;
|
||||
}
|
||||
/**
|
||||
* 回退到默认语言
|
||||
*/
|
||||
_fallback() {
|
||||
this._messages = this._default;
|
||||
this._activeLanguage = this.defaultLanguage;
|
||||
this.#options.messages = this.default;
|
||||
this.#options.activeLanguage = this.defaultLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新当前语言包
|
||||
* @param {*} newLanguage
|
||||
*/
|
||||
async refresh(newLanguage) {
|
||||
this._refreshing = true;
|
||||
async refresh(newLanguage?:string) {
|
||||
this.#refreshing = true;
|
||||
if (!newLanguage) newLanguage = this.activeLanguage;
|
||||
// 默认语言:由于默认语言采用静态加载方式而不是异步块,因此只需要简单的替换即可
|
||||
if (newLanguage === this.defaultLanguage) {
|
||||
this._messages = this._default;
|
||||
await this._patch(this._messages, newLanguage); // 异步补丁
|
||||
this.#options.messages = this.default;
|
||||
await this._patch(this.#options.messages, newLanguage); // 异步补丁
|
||||
await this._changeFormatters(newLanguage);
|
||||
return;
|
||||
}
|
||||
@ -303,10 +331,10 @@ module.exports = class VoerkaI18nScope {
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
if (this._debug) console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`);
|
||||
if (this.debug) console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`);
|
||||
this._fallback();
|
||||
} finally {
|
||||
this._refreshing = false;
|
||||
this.#refreshing = false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
@ -327,7 +355,7 @@ module.exports = class VoerkaI18nScope {
|
||||
this._savePatchedMessages(pachedMessages, newLanguage);
|
||||
}
|
||||
} catch (e) {
|
||||
if (this._debug) console.error(`Error while loading <${newLanguage}> patch messages from remote:`,e);
|
||||
if (this.debug) console.error(`Error while loading <${newLanguage}> patch messages from remote:`,e);
|
||||
}
|
||||
}
|
||||
/**
|
||||
@ -363,7 +391,7 @@ module.exports = class VoerkaI18nScope {
|
||||
globalThis.localStorage.setItem(`voerkai18n_${this.id}_${language}_patched_messages`,JSON.stringify(messages));
|
||||
}
|
||||
} catch (e) {
|
||||
if (this.$cache._debug) console.error("Error while save voerkai18n patched messages:",e);
|
||||
if (this.cache._debug) console.error("Error while save voerkai18n patched messages:",e);
|
||||
}
|
||||
}
|
||||
/**
|
||||
@ -379,10 +407,10 @@ module.exports = class VoerkaI18nScope {
|
||||
}
|
||||
}
|
||||
// 以下方法引用全局VoerkaI18n实例的方法
|
||||
on() {return this._global.on(...arguments); }
|
||||
off() {return this._global.off(...arguments); }
|
||||
offAll() {return this._global.offAll(...arguments);}
|
||||
on() {return this.#global.on(...arguments); }
|
||||
off() {return this.#global.off(...arguments); }
|
||||
offAll() {return this.#global.offAll(...arguments);}
|
||||
async change(language) {
|
||||
await this._global.change(language);
|
||||
await this.#global.change(language);
|
||||
}
|
||||
};
|
@ -1,5 +1,10 @@
|
||||
const {isNumber,isPlainObject,isFunction} = require("./utils")
|
||||
const { replaceInterpolatedVars } = require("./interpolate")
|
||||
import { isNumber } from "flex-tools/typecheck/isNumber"
|
||||
import { isPlainObject } from "flex-tools/typecheck/isPlainObject"
|
||||
import { isFunction } from "flex-tools/typecheck/isFunction"
|
||||
import { replaceInterpolatedVars } from "./interpolate"
|
||||
import type { VoerkaI18nScope } from "./scope"
|
||||
|
||||
|
||||
/**
|
||||
* 当传入的翻译内容不是一个字符串时,进行默认的转换
|
||||
*
|
||||
@ -10,7 +15,7 @@ const { replaceInterpolatedVars } = require("./interpolate")
|
||||
* @param {*} value
|
||||
* @returns
|
||||
*/
|
||||
function transformToString(value){
|
||||
function transformToString(value:any){
|
||||
let result = value
|
||||
try{
|
||||
if(isFunction(result)) result = value()
|
||||
@ -34,7 +39,7 @@ const { replaceInterpolatedVars } = require("./interpolate")
|
||||
* @param {*} content
|
||||
* @returns
|
||||
*/
|
||||
function isMessageId(content){
|
||||
function isMessageId(content:string){
|
||||
return isNumber(content)
|
||||
}
|
||||
/**
|
||||
@ -43,7 +48,7 @@ const { replaceInterpolatedVars } = require("./interpolate")
|
||||
* @param {*} messages 复数形式的文本内容 = [<=0时的内容>,<=1时的内容>,<=2时的内容>,...]
|
||||
* @param {*} value
|
||||
*/
|
||||
function getPluraMessage(messages,value){
|
||||
function getPluraMessage(messages:any,value:number){
|
||||
try{
|
||||
if(Array.isArray(messages)){
|
||||
return messages.length > value ? messages[value] : messages[messages.length-1]
|
||||
@ -67,27 +72,28 @@ function getPluraMessage(messages,value){
|
||||
* this===scope 当前绑定的scope
|
||||
*
|
||||
*/
|
||||
function translate(message) {
|
||||
export function translate(this:VoerkaI18nScope,message:string,...args:any[]) {
|
||||
const scope = this
|
||||
const activeLanguage = scope.global.activeLanguage
|
||||
let content = message
|
||||
let content:string = message
|
||||
let vars=[] // 插值变量列表
|
||||
let pluralVars= [] // 复数变量
|
||||
let pluraValue = null // 复数值
|
||||
if(!typeof(message)==="string") return message
|
||||
if(!(typeof(message)==="string")) return message
|
||||
try{
|
||||
// 1. 预处理变量: 复数变量保存至pluralVars中 , 变量如果是Function则调用
|
||||
if(arguments.length === 2 && isPlainObject(arguments[1])){
|
||||
const dictVars:Record<string,any>={}
|
||||
Object.entries(arguments[1]).forEach(([name,value])=>{
|
||||
if(isFunction(value)){
|
||||
if(typeof(value)=="function"){
|
||||
try{
|
||||
vars[name] = value()
|
||||
dictVars[name] = value()
|
||||
}catch(e){
|
||||
vars[name] = value
|
||||
dictVars[name] = value
|
||||
}
|
||||
}
|
||||
// 以$开头的视为复数变量
|
||||
if(name.startsWith("$") && typeof(vars[name])==="number") pluralVars.push(name)
|
||||
if(name.startsWith("$") && typeof(dictVars[name])==="number") pluralVars.push(name)
|
||||
})
|
||||
vars = [arguments[1]]
|
||||
}else if(arguments.length >= 2){
|
||||
@ -141,9 +147,4 @@ function translate(message) {
|
||||
return content // 出错则返回原始文本
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports = {
|
||||
translate
|
||||
}
|
||||
|
137
packages/runtime/src/types.ts
Normal file
137
packages/runtime/src/types.ts
Normal file
@ -0,0 +1,137 @@
|
||||
import type { VoerkaI18nManager } from "./manager"
|
||||
import type { VoerkaI18nScope } from "./scope"
|
||||
|
||||
declare global {
|
||||
export var VoerkaI18n: VoerkaI18nManager
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export type VoerkaI18nLanguageMessages = Record<string, string | string[]>
|
||||
export interface VoerkaI18nLanguagePack {
|
||||
[language: string]: VoerkaI18nLanguageMessages
|
||||
}
|
||||
|
||||
export type Voerkai18nIdMap = Record<string, number>
|
||||
|
||||
export interface VoerkaI18nLanguage {
|
||||
name: string
|
||||
title?: string
|
||||
default?: boolean
|
||||
fallback?: string
|
||||
}
|
||||
|
||||
|
||||
export type VoerkaI18nFormatter = (value: string, ...args: any[]) => string
|
||||
export type VoerkaI18nFormatterConfigs = Record<string, any>
|
||||
|
||||
export type VoerkaI18nFormatters = Record<string, ({
|
||||
$types?: Record<string, VoerkaI18nFormatter>
|
||||
$config?: Record<string, string>
|
||||
} & {
|
||||
[key: string]: VoerkaI18nFormatter
|
||||
})> //| (() => Promise<any>)>
|
||||
|
||||
export type VoerkI18nLoader = () => Awaited<Promise<any>>
|
||||
export interface VoerkI18nLoaders {
|
||||
[key: string]: VoerkI18nLoader
|
||||
}
|
||||
export type VoerkI18nMessageLoader = (this:VoerkaI18nScope,newLanguage:string,scope:VoerkaI18nScope)=>Promise<VoerkaI18nLanguageMessages>
|
||||
|
||||
export interface VoerkaI18nScopeCache{
|
||||
activeLanguage? : null,
|
||||
typedFormatters?: {},
|
||||
formatters? : {},
|
||||
}
|
||||
export type TranslateMessageVars = number | boolean | string | Function | Date
|
||||
export interface VoerkaI18nTranslate {
|
||||
(message: string, ...args: TranslateMessageVars[]): string
|
||||
(message: string, vars?: Record<string, TranslateMessageVars>): string
|
||||
}
|
||||
export interface VoerkaI18nSupportedLanguages {
|
||||
[key: string]: VoerkaI18nLanguage
|
||||
}
|
||||
|
||||
|
||||
// export class VoerkaI18nScope {
|
||||
// constructor(options: VoerkaI18nScopeOptions, callback?: Function)
|
||||
// get id(): string // 作用域唯一id
|
||||
// get debug(): boolean // 调试开关
|
||||
// get defaultLanguage(): string // 默认语言名称
|
||||
// get activeLanguage(): string // 默认语言名称
|
||||
// get default(): VoerkaI18nLanguageMessages // 默认语言包
|
||||
// get messages(): VoerkaI18nLanguageMessages // 当前语言包
|
||||
// get idMap(): Voerkai18nIdMap // 消息id映射列表
|
||||
// get languages(): VoerkaI18nSupportedLanguages // 当前作用域支持的语言列表[{name,title,fallback}]
|
||||
// get loaders(): VoerkI18nLoaders // 异步加载语言文件的函数列表
|
||||
// get global(): VoerkaI18nManager // 引用全局VoerkaI18n配置,注册后自动引用
|
||||
// get formatters(): VoerkI18nFormatters // 当前作用域的所有格式化器定义 {<语言名称>: {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () => {}}}
|
||||
// get activeFormatters(): VoerkI18nFormatters // 当前作用域激活的格式化器定义 {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () = >{}}
|
||||
// get activeFormatterConfig(): VoerkI18nFormatterConfigs // 当前格式化器合并后的配置参数,参数已经合并了全局格式化器中的参数
|
||||
// get t(): VoerkaI18nTranslate
|
||||
// get translate(): VoerkaI18nTranslate
|
||||
// /**
|
||||
// * 在全局注册作用域当前作用域
|
||||
// * @param {*} callback 注册成功后的回调
|
||||
// */
|
||||
// register(callback: Function): void
|
||||
// /**
|
||||
// * 注册格式化器
|
||||
// */
|
||||
// registerFormatter(name: string, formatter: VoerkI18nFormatter, { language, global }: { language: string | string[], global: boolean }): void
|
||||
// /**
|
||||
// * 注册多种格式化器
|
||||
// */
|
||||
// registerFormatters(formatters: VoerkI18nFormatters, asGlobal?: boolean): void
|
||||
// /**
|
||||
// * 注册默认文本信息加载器
|
||||
// */
|
||||
// registerDefaultLoader(fn: Function): void
|
||||
// /**
|
||||
// * 获取指定语言信息
|
||||
// * @param {*} language
|
||||
// * @returns
|
||||
// */
|
||||
// getLanguage(language: string): VoerkaI18nLanguageMessages
|
||||
// hasLanguage(language: string): boolean
|
||||
// refresh(newLanguage: string): Promise<void>
|
||||
// on(): void
|
||||
// off(): void
|
||||
// offAll(): void
|
||||
// change(language: string): Promise<void>
|
||||
// }
|
||||
export type VoerkI18nFormatterConfigs = Record<string, any>
|
||||
// 翻译函数
|
||||
export var translate: {
|
||||
(message: string, ...args: (string | Function)[]): string
|
||||
(message: string, vars?: Record<string, any>): string
|
||||
}
|
||||
export interface CreateFormatterOptions {
|
||||
normalize?: (value: any) => any // 对输入值进行规范化处理,如进行时间格式化时,为了提高更好的兼容性,支持数字时间戳/字符串/Date等,需要对输入值进行处理,如强制类型转换等
|
||||
params?: string[] | null // 可选的,声明参数顺序,如果是变参的,则需要传入null
|
||||
configKey?: string // 声明该格式化器在$config中的路径,支持简单的使用.的路径语法
|
||||
}
|
||||
|
||||
export type Primitive = string | number | boolean | null | undefined
|
||||
|
||||
export interface FormatterDefine {
|
||||
(this: VoerkI18nFormatterConfigs, value: any, ...args: Primitive[]): string
|
||||
(this: VoerkI18nFormatterConfigs, value: any, arg1: Primitive, $config: VoerkI18nFormatterConfigs): string
|
||||
(this: VoerkI18nFormatterConfigs, value: any, arg1: Primitive, arg2: Primitive, $config: VoerkI18nFormatterConfigs): string
|
||||
(this: VoerkI18nFormatterConfigs, value: any, arg1: Primitive, arg2: Primitive, arg3: Primitive, $config: VoerkI18nFormatterConfigs): string
|
||||
configurable?: boolean
|
||||
}
|
||||
// 创建格式化器
|
||||
export type CreateFormatterType = (fn: Function, options: CreateFormatterOptions, defaultParams: Record<string, any>) => FormatterDefine
|
||||
export var createFormatter: CreateFormatterType
|
||||
export var Formatter: CreateFormatterType
|
||||
|
||||
export type CreateFlexFormatterType = (fn: Function, options: CreateFormatterOptions, defaultParams: Record<string, any>) => FormatterDefine
|
||||
export var createFlexFormatter: CreateFlexFormatterType
|
||||
export var FlexFormatter: CreateFlexFormatterType
|
||||
export var getDataTypeName: (value: any) => string
|
||||
export var toDate: (value: any) => Date | number
|
||||
export var toNumber: (value: any, defaultValue: number) => number
|
||||
export var toBoolean: (value: any) => boolean
|
||||
|
72
packages/runtime/src/utils.ts
Normal file
72
packages/runtime/src/utils.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { isNumber } from "flex-tools/typecheck/isNumber"
|
||||
|
||||
/**
|
||||
* 获取指定变量类型名称
|
||||
* getDataTypeName(1) == Number
|
||||
* getDataTypeName("") == String
|
||||
* getDataTypeName(null) == Null
|
||||
* getDataTypeName(undefined) == Undefined
|
||||
* getDataTypeName(new Date()) == Date
|
||||
* getDataTypeName(new Error()) == Error
|
||||
*
|
||||
* @param {*} v
|
||||
* @returns
|
||||
*/
|
||||
export function getDataTypeName(v:any):string{
|
||||
if (v === null) return 'Null'
|
||||
if (v === undefined) return 'Undefined'
|
||||
if(typeof(v)==="function") return "Function"
|
||||
return v.constructor && v.constructor.name;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 当需要采用正则表达式进行字符串替换时,需要对字符串进行转义
|
||||
*
|
||||
* 比如 str = "I am {username}"
|
||||
* replace(new RegExp(str),"Tom") !=== I am Tom
|
||||
*
|
||||
* 因为{}是正则表达式元字符,需要转义成 "\{username\}"
|
||||
*
|
||||
* replace(new RegExp(escapeRegexpStr(str)),"Tom")
|
||||
*
|
||||
*
|
||||
* @param {*} str
|
||||
* @returns
|
||||
*/
|
||||
export function escapeRegexpStr(str:string):string{
|
||||
return str.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1")
|
||||
}
|
||||
|
||||
export const DataTypes = ["String","Number","Boolean","Object","Array","Function","Error","Symbol","RegExp","Date","Null","Undefined","Set","Map","WeakSet","WeakMap"]
|
||||
|
||||
/**
|
||||
* 转换为数字类型
|
||||
*/
|
||||
export function toNumber(value:any,defualt=0):number {
|
||||
try {
|
||||
if (isNumber(value)) {
|
||||
return parseFloat(value)
|
||||
} else {
|
||||
return defualt
|
||||
}
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将值转换为Date类型
|
||||
* @param {*} value
|
||||
*/
|
||||
export function toDate(value:any) {
|
||||
try {
|
||||
return value instanceof Date ? value : new Date(value)
|
||||
} catch {
|
||||
return parseInt(value)
|
||||
}
|
||||
}
|
||||
|
||||
export function toBoolean(value:any){
|
||||
return !!value
|
||||
}
|
106
packages/runtime/tsconfig.json
Normal file
106
packages/runtime/tsconfig.json
Normal file
@ -0,0 +1,106 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
//"suppressImplicitAnyIndexErrors":true,
|
||||
/* Language and Environment */
|
||||
"target": "ES2015", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
"experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
"emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
//"moduleResolution": "node16",
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
// "baseUrl": ".",
|
||||
// "rootDir": "./src",
|
||||
// "outDir": "./dist",
|
||||
// "paths": {
|
||||
// },
|
||||
"rootDir": "./src", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
//"baseUrl": "./dist", /* Specify the base directory to resolve non-relative module names. */
|
||||
//"paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
//"rootDirs": ["src"], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
// "outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
|
||||
|
||||
},
|
||||
"include":[
|
||||
"src"
|
||||
]
|
||||
}
|
@ -1,279 +0,0 @@
|
||||
|
||||
/**
|
||||
* 判断是否是JSON对象
|
||||
* @param {*} obj
|
||||
* @returns
|
||||
*/
|
||||
function isPlainObject(obj){
|
||||
if (typeof obj !== 'object' || obj === null) return false;
|
||||
var proto = Object.getPrototypeOf(obj);
|
||||
if (proto === null) return true;
|
||||
var baseProto = proto;
|
||||
|
||||
while (Object.getPrototypeOf(baseProto) !== null) {
|
||||
baseProto = Object.getPrototypeOf(baseProto);
|
||||
}
|
||||
return proto === baseProto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断值是否是一个数字
|
||||
* @param {*} value
|
||||
* @returns
|
||||
*/
|
||||
function isNumber(value){
|
||||
if(value==undefined) return false
|
||||
if(typeof(value)=='number') return true
|
||||
if(typeof(value)!='string') return false
|
||||
try{
|
||||
if(value.includes(".")){
|
||||
let v = parseFloat(value)
|
||||
if(value.endsWith(".")){
|
||||
return !isNaN(v) && String(v).length===value.length-1
|
||||
}else{
|
||||
return !isNaN(v) && String(v).length===value.length
|
||||
}
|
||||
}else{
|
||||
let v = parseInt(value)
|
||||
return !isNaN(v) && String(v).length===value.length
|
||||
}
|
||||
}catch{
|
||||
return false
|
||||
}
|
||||
}
|
||||
function isFunction(fn){
|
||||
return typeof fn === "function"
|
||||
}
|
||||
/**
|
||||
* 当value= null || undefined || "" || [] || {} 时返回true
|
||||
* @param {*} value
|
||||
* @returns
|
||||
*/
|
||||
function isNothing(value){
|
||||
if(["boolean","function"].includes(typeof(value))) return false
|
||||
if(value=="") return true
|
||||
if(value==undefined) return true
|
||||
if(Array.isArray(value) && value.length==0) return true
|
||||
if(typeof(value)=="object" && Object.keys(value).length==0) return true
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度合并对象
|
||||
*
|
||||
* 注意:
|
||||
* - 不会对数组成员进行再次遍历
|
||||
* - 不能处理循环引入
|
||||
*
|
||||
* @param {*} toObj
|
||||
* @param {*} formObj
|
||||
* @param {*} options
|
||||
* array : 数组合并策略,0-替换,1-合并,2-去重合并
|
||||
* mixin : 是否采用混入方式来,=false, 则会创建新对象并返回
|
||||
*/
|
||||
function deepMerge(toObj,formObj,options={}){
|
||||
let results = options.mixin ? toObj : Object.assign({},toObj)
|
||||
Object.entries(formObj).forEach(([key,value])=>{
|
||||
if(key in results){
|
||||
if(typeof value === "object" && value !== null){
|
||||
if(Array.isArray(value)){
|
||||
if(options.array === 1){//合并
|
||||
results[key] = [...results[key],...value]
|
||||
}else if(options.array === 2){//去重合并
|
||||
results[key] = [...new Set([...results[key],...value])]
|
||||
}else{ //默认: 替换
|
||||
results[key] = value
|
||||
}
|
||||
}else{
|
||||
results[key] = deepMerge(results[key],value,options)
|
||||
}
|
||||
}else{
|
||||
results[key] = value
|
||||
}
|
||||
}else{
|
||||
results[key] = value
|
||||
}
|
||||
})
|
||||
return results
|
||||
}
|
||||
|
||||
function deepMixin(toObj,formObj,options={}){
|
||||
return deepMerge(toObj,formObj,{...options,mixin:true})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定变量类型名称
|
||||
* getDataTypeName(1) == Number
|
||||
* getDataTypeName("") == String
|
||||
* getDataTypeName(null) == Null
|
||||
* getDataTypeName(undefined) == Undefined
|
||||
* getDataTypeName(new Date()) == Date
|
||||
* getDataTypeName(new Error()) == Error
|
||||
*
|
||||
* @param {*} v
|
||||
* @returns
|
||||
*/
|
||||
function getDataTypeName(v){
|
||||
if (v === null) return 'Null'
|
||||
if (v === undefined) return 'Undefined'
|
||||
if(typeof(v)==="function") return "Function"
|
||||
return v.constructor && v.constructor.name;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 根据路径获取指定值
|
||||
* 只支持简单的.分割路径
|
||||
* getByPath({a:{b:1}},"a.b") == 1
|
||||
* getByPath({a:{b:1}},"a.c",2) == 2
|
||||
*
|
||||
* @param {*} obj
|
||||
* @param {*} path 使用.分割的路径
|
||||
* @param {*} defaultValue 默认值
|
||||
* @returns
|
||||
*/
|
||||
function getByPath(obj,path,defaultValue){
|
||||
if(typeof(obj)!="object" || typeof(path)!="string") return defaultValue
|
||||
let paths = path.split(".")
|
||||
let cur = obj
|
||||
for(let key of paths){
|
||||
if(typeof(cur)=="object" && (key in cur) ){
|
||||
cur = cur[key]
|
||||
}else{
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
return cur
|
||||
}
|
||||
function deepClone(obj){
|
||||
if(obj==undefined) return obj
|
||||
if (['string',"number","boolean","function","undefined"].includes(typeof(obj))){
|
||||
return obj
|
||||
}else if(Array.isArray(obj)){
|
||||
return obj.map(item => deepClone(item))
|
||||
}else if(typeof(obj)=="object"){
|
||||
let results = {}
|
||||
Object.entries(obj).forEach(([key,value])=>{
|
||||
results[key] = deepClone(value)
|
||||
})
|
||||
return results
|
||||
}else{
|
||||
return obj
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 替换所有字符串
|
||||
* 低版本ES未提供replaceAll,此函数用来替代
|
||||
* @param {*} str
|
||||
* @param {*} findValue
|
||||
* @param {*} replaceValue
|
||||
*/
|
||||
function replaceAll(str,findValue,replaceValue){
|
||||
if(typeof(str)!=="string" || findValue=="" || findValue==replaceValue) return str
|
||||
try{
|
||||
return str.replace(new RegExp(escapeRegexpStr(findValue),"g"),replaceValue)
|
||||
}catch{}
|
||||
return str
|
||||
}
|
||||
/**
|
||||
* 使用正则表达式解析非标JOSN
|
||||
*
|
||||
*/
|
||||
|
||||
const bastardJsonKeyRegex = /(?<value>(?<=:\s*)(\'.*?\')+)|(?<key>(([\w\u4e00-\u9fa5])|(\'.*?\'))+(?=\s*\:))/g
|
||||
|
||||
/**
|
||||
* 当需要采用正则表达式进行字符串替换时,需要对字符串进行转义
|
||||
*
|
||||
* 比如 str = "I am {username}"
|
||||
* replace(new RegExp(str),"Tom") !=== I am Tom
|
||||
*
|
||||
* 因为{}是正则表达式元字符,需要转义成 "\{username\}"
|
||||
*
|
||||
* replace(new RegExp(escapeRegexpStr(str)),"Tom")
|
||||
*
|
||||
*
|
||||
* @param {*} str
|
||||
* @returns
|
||||
*/
|
||||
function escapeRegexpStr(str){
|
||||
return str.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1")
|
||||
}
|
||||
/**
|
||||
* 解析非标的JSON字符串为{}
|
||||
* 非标的JSON字符串指的是:
|
||||
* - key没有使用使用""包裹
|
||||
* - 字符串value没有使用""包裹
|
||||
*
|
||||
* @param {*} str
|
||||
* @returns
|
||||
*/
|
||||
function safeParseJson(str){
|
||||
let matched;
|
||||
while ((matched = bastardJsonKeyRegex.exec(str)) !== null) {
|
||||
if (matched.index === bastardJsonKeyRegex.lastIndex) {
|
||||
bastardJsonKeyRegex.lastIndex++;
|
||||
}
|
||||
let item = matched[0]
|
||||
if(item.startsWith("'") && item.endsWith("'")){
|
||||
item = item.substring(1,item.length-1)
|
||||
}
|
||||
const findValue = matched.groups.key ? new RegExp( escapeRegexpStr(matched[0]) + "\s*:") : new RegExp(":\s*" + escapeRegexpStr(matched[0]))
|
||||
const replaceTo = matched.groups.key ? `"${item}":` : `: "${item}"`
|
||||
str = str.replace(findValue,replaceTo)
|
||||
}
|
||||
return JSON.parse(str)
|
||||
}
|
||||
const DataTypes = ["String","Number","Boolean","Object","Array","Function","Error","Symbol","RegExp","Date","Null","Undefined","Set","Map","WeakSet","WeakMap"]
|
||||
/**
|
||||
* 转换为数字类型
|
||||
*/
|
||||
function toNumber(value,defualt=0) {
|
||||
try {
|
||||
if (isNumber(value)) {
|
||||
return parseFloat(value)
|
||||
} else {
|
||||
return defualt
|
||||
}
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将值转换为Date类型
|
||||
* @param {*} value
|
||||
*/
|
||||
function toDate(value) {
|
||||
try {
|
||||
return value instanceof Date ? value : new Date(value)
|
||||
} catch {
|
||||
return parseInt(value)
|
||||
}
|
||||
}
|
||||
|
||||
function toBoolean(value){
|
||||
return !!value
|
||||
}
|
||||
|
||||
module.exports ={
|
||||
DataTypes,
|
||||
isPlainObject,
|
||||
isFunction,
|
||||
isNumber,
|
||||
isNothing,
|
||||
toNumber,
|
||||
toDate,
|
||||
toBoolean,
|
||||
deepClone,
|
||||
deepMerge,
|
||||
deepMixin,
|
||||
replaceAll,
|
||||
getByPath,
|
||||
getDataTypeName,
|
||||
escapeRegexpStr,
|
||||
safeParseJson
|
||||
}
|
581
pnpm-lock.yaml
generated
581
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user