update tests

This commit is contained in:
wxzhang 2022-08-20 21:24:16 +08:00
parent 9b3b7a9035
commit 4058fd08b8
26 changed files with 3795 additions and 1272 deletions

View File

@ -1,6 +1,6 @@
# 概述
基于`javascript`的国际化方案很多,比较有名的有`fbt``i18next``react-i18next``vue-i18n``react-intl`等等,每一种解决方案均有大量的用户。为什么还要再造一个轮子?好吧,再造轮子的理由不外乎不满足于现有方案,总想着现有方案的种种不足之处,然后就撸起袖子想造一个轮子,也不想想自己什么水平
基于`javascript`的国际化方案很多,比较有名的有`fbt``i18next``react-i18next``vue-i18n``react-intl`等等,每一种解决方案均有大量的用户。为什么还要再造一个轮子?好吧,再造轮子的理由不外乎不满足于现有方案,总想着现有方案的种种不足之处,然后就撸起袖子开始干
那么到底是对现有解决方案有什么不满?最主要有三点:
@ -22,17 +22,17 @@
- 强大的插值变量`格式化器`机制,可以扩展出强大的多语言特性。
- 支持`babel`插件自动导入`t`翻译函数
- 多库多包`monorepo`场景下完美国际化联动切换
- 支持`nodejs`、浏览器(`vue`/`react`)前端环境。
- 支持`babel`/`vite`等插件自动导入`t`翻译函数,简化应用集成。
- 支持`nodejs`、浏览器(`vue`/`react`)等几乎所有`javascript`环境。
- 采用`工具链``运行时`分开设计,发布时只需要集成很小的运行时。
- 高度可扩展的`复数``货币``数字`等常用的多语言处理机制。
- 翻译过程内,提取文本可以自动进行同步,并保留已翻译的内容。
- 支持远程加载语言包,并且可以在线打语言补丁包
- 支持应用发布后在线打语言补丁包,方便修复翻译错误。
- 支持调用在线自动翻译对提取文本进行翻译。

View File

@ -1,11 +1,11 @@
# 版本信息
| 包| 版本号| 最后更新|说明|
| --- | :---:| --- |---|
|**@voerkai18n/utils**|1.0.12|2022/08/05|公共工具库
|**@voerkai18n/runtime**|1.0.29|2022/08/16|核心运行时
|**@voerkai18n/formatters**|1.0.6|2022/04/15|格式化器,提供对要翻译文本的转换功能
|**@voerkai18n/react**|1.0.4|2022/04/16|React支持,提供语言切换等功能
|**@voerkai18n/cli**|1.0.33|2022/08/07|命令行工具,用来初始化/提取/编译/自动翻译等工具链
|**@voerkai18n/babel**|1.0.23|2022/08/05|Babel插件实现自动导入t函数和自动文本映射
|**@voerkai18n/vite**|1.0.12|2022/08/05|Vite插件,提供自动插入翻译函数和文本映射等功能
|**@voerkai18n/vue**|1.0.5|2022/04/15|Vue3插件,提供自动插件翻译函数和语言切换功能
|**@voerkai18n/utils**|1.0.13|2022/08/20|公共工具库|
|**@voerkai18n/runtime**|1.1.2|2022/08/20|核心运行时|
|**@voerkai18n/formatters**|1.0.6|2022/04/15|扩展格式化器|
|**@voerkai18n/react**|1.0.4|2022/04/16|React支持,提供语言切换等功能|
|**@voerkai18n/cli**|1.0.35|2022/08/20|命令行工具,用来初始化/提取/编译/自动翻译等工具链|
|**@voerkai18n/babel**|1.0.24|2022/08/20|Babel插件实现自动导入t函数和自动文本映射|
|**@voerkai18n/vite**|1.0.13|2022/08/20|Vite插件,提供自动插入翻译函数和文本映射等功能|
|**@voerkai18n/vue**|1.0.6|2022/08/20|Vue3插件,提供自动插件翻译函数和语言切换功能|

View File

@ -364,7 +364,7 @@ function generatePackageVersionDoc(){
results.push("| --- | :---:| --- |---|")
getPackages().forEach(package => {
const lastPublish = package.lastPublish ? dayjs(package.lastPublish).format("YYYY/MM/DD") : "None"
results.push(`|**${package.name}**|${package.version}|${lastPublish}|${package.description}`)
results.push(`|**${package.name}**|${package.version}|${lastPublish}|${package.description}|`)
})
fs.writeFileSync(path.join(workspaceRoot,"docs/src/guide/intro/versions.md"), results.join("\n"))

View File

@ -1,6 +1,6 @@
{
"name": "@voerkai18n/babel",
"version": "1.0.23",
"version": "1.0.24",
"description": "Babel插件实现自动导入t函数和自动文本映射",
"main": "index.js",
"homepage": "https://gitee.com/zhangfisher/voerka-i18n",
@ -20,5 +20,5 @@
"devDependencies": {
"@voerkai18n/autopublish": "workspace:^1.0.2"
},
"lastPublish": "2022-08-05T16:45:56+08:00"
"lastPublish": "2022-08-20T20:54:29+08:00"
}

View File

@ -1,4 +1,4 @@
export default {
module.exports = {
"1": "支持的语言\t: {}",
"2": "默认语言\t: {}",
"3": "激活语言\t: {}",

View File

@ -1,4 +1,4 @@
export default {
module.exports = {
"1": "Supported languages\t: {}",
"2": "Default language\t: {}",
"3": "Active language\t\t: {}",

View File

@ -0,0 +1,69 @@
/**
格式化器用来对翻译文本内容中的插值变量进行处理
比如将一个数字格式化为货币格式或者将一个日期格式化为友好的日期格式
- 以下定义了一些格式化器在中文场景下会启用这些格式化器
import dayjs from "dayjs";
module.exports = {
$config:{...},
$types:{
Date:(value)=>dayjs(value).format("YYYY年MM月DD日 HH:mm:ss"),
},
date:(value)=>dayjs(value).format("YYYY年MM月DD日")
bjTime:(value)=>"北京时间"+ value,
[格式化器名称]:(value)=>{...},
[格式化器名称]:(value)=>{...},
[格式化器名称]:(value)=>{...},
}
}
- 在翻译函数中使用格式化器的方法,示例如下
t("Now is { value | date | bjTime }",{value: new Date()})
其等效于
t(`Now is ${bjTime(date(value))",{value: new Date()})
由于value分别经过两个管道符转换上一个管道符的输出作为下一个管道符的输入,可以多次使用管道符
最终的输出结果
中文: "现在是北京时间2022年3月1日"
英文: "Now is BeiJing 2022/03/01"
*/
module.exports = {
// 格式化器参数
$config:{
},
// 指定数据类型的默认格式化器
$types:{
// "*" : { },
// Date : { },
// Number: { },
// String: { },
// Array : { },
// Object: { }
}
// 允许重载内置的格式化器
// --- 日期 ------
// date : value => { ... },
// shortdate : value => { ... },
// time : value => { ... },
// shorttime : value => { ... },
// year : value => { ... },
// month : value => { ... },
// day : value => { ... },
// weekdayValue : value => { ... },
// weekday : value => { ... },
// shortWeekday : value => { ... },
// monthName : value => { ... },
// shorMonthName : value => { ... },
// --- 时间 ------
// hour : value => { ... },
// hour12 : value => { ... },
// minute : value => { ... },
// second : value => { ... },
// millisecond : value => { ... },
// timestamp : value => { ... },
// currency : value => { ... },
// number : value => { ... },
}

View File

@ -0,0 +1,69 @@
/**
格式化器用来对翻译文本内容中的插值变量进行处理
比如将一个数字格式化为货币格式或者将一个日期格式化为友好的日期格式
- 以下定义了一些格式化器在中文场景下会启用这些格式化器
import dayjs from "dayjs";
module.exports = {
$config:{...},
$types:{
Date:(value)=>dayjs(value).format("YYYY年MM月DD日 HH:mm:ss"),
},
date:(value)=>dayjs(value).format("YYYY年MM月DD日")
bjTime:(value)=>"北京时间"+ value,
[格式化器名称]:(value)=>{...},
[格式化器名称]:(value)=>{...},
[格式化器名称]:(value)=>{...},
}
}
- 在翻译函数中使用格式化器的方法,示例如下
t("Now is { value | date | bjTime }",{value: new Date()})
其等效于
t(`Now is ${bjTime(date(value))",{value: new Date()})
由于value分别经过两个管道符转换上一个管道符的输出作为下一个管道符的输入,可以多次使用管道符
最终的输出结果
中文: "现在是北京时间2022年3月1日"
英文: "Now is BeiJing 2022/03/01"
*/
module.exports = {
// 格式化器参数
$config:{
},
// 指定数据类型的默认格式化器
$types:{
// "*" : { },
// Date : { },
// Number: { },
// String: { },
// Array : { },
// Object: { }
}
// 允许重载内置的格式化器
// --- 日期 ------
// date : value => { ... },
// shortdate : value => { ... },
// time : value => { ... },
// shorttime : value => { ... },
// year : value => { ... },
// month : value => { ... },
// day : value => { ... },
// weekdayValue : value => { ... },
// weekday : value => { ... },
// shortWeekday : value => { ... },
// monthName : value => { ... },
// shorMonthName : value => { ... },
// --- 时间 ------
// hour : value => { ... },
// hour12 : value => { ... },
// minute : value => { ... },
// second : value => { ... },
// millisecond : value => { ... },
// timestamp : value => { ... },
// currency : value => { ... },
// number : value => { ... },
}

View File

@ -0,0 +1,69 @@
/**
格式化器用来对翻译文本内容中的插值变量进行处理
比如将一个数字格式化为货币格式或者将一个日期格式化为友好的日期格式
- 以下定义了一些格式化器在中文场景下会启用这些格式化器
import dayjs from "dayjs";
module.exports = {
$config:{...},
$types:{
Date:(value)=>dayjs(value).format("YYYY年MM月DD日 HH:mm:ss"),
},
date:(value)=>dayjs(value).format("YYYY年MM月DD日")
bjTime:(value)=>"北京时间"+ value,
[格式化器名称]:(value)=>{...},
[格式化器名称]:(value)=>{...},
[格式化器名称]:(value)=>{...},
}
}
- 在翻译函数中使用格式化器的方法,示例如下
t("Now is { value | date | bjTime }",{value: new Date()})
其等效于
t(`Now is ${bjTime(date(value))",{value: new Date()})
由于value分别经过两个管道符转换上一个管道符的输出作为下一个管道符的输入,可以多次使用管道符
最终的输出结果
中文: "现在是北京时间2022年3月1日"
英文: "Now is BeiJing 2022/03/01"
*/
module.exports = {
// 格式化器参数
$config:{
},
// 指定数据类型的默认格式化器
$types:{
// "*" : { },
// Date : { },
// Number: { },
// String: { },
// Array : { },
// Object: { }
}
// 允许重载内置的格式化器
// --- 日期 ------
// date : value => { ... },
// shortdate : value => { ... },
// time : value => { ... },
// shorttime : value => { ... },
// year : value => { ... },
// month : value => { ... },
// day : value => { ... },
// weekdayValue : value => { ... },
// weekday : value => { ... },
// shortWeekday : value => { ... },
// monthName : value => { ... },
// shorMonthName : value => { ... },
// --- 时间 ------
// hour : value => { ... },
// hour12 : value => { ... },
// minute : value => { ... },
// second : value => { ... },
// millisecond : value => { ... },
// timestamp : value => { ... },
// currency : value => { ... },
// number : value => { ... },
}

View File

@ -1,4 +1,4 @@
export default {
module.exports = {
"支持的语言\t: {}": 1,
"默认语言\t: {}": 2,
"激活语言\t: {}": 3,

View File

@ -1,12 +1,11 @@
import messageIds from "./idMap.js"
import runtime from "./runtime.js"
const { translate,i18nScope } = runtime
const messageIds = require("./idMap")
const { translate,i18nScope } = require("./runtime.js")
const defaultFormatters = require("./formatters/zh.js")
const activeFormatters = defaultFormatters
import formatters from "./formatters.js"
import defaultMessages from "./zh.js"
const activeMessages = defaultMessages
const defaultMessages = require("./zh.js") // 默认语言包
const activeMessages = defaultMessages
// 语言配置文件
const scopeSettings = {
@ -28,25 +27,32 @@ const scopeSettings = {
"activeLanguage": "zh",
"namespaces": {}
}
// 格式化器
const formatters = {
'zh' : defaultFormatters,
'en' : ()=>import("./formatters/en.js"),
'de' : ()=>import("./formatters/de.js")
}
// 语言包加载器
const loaders = {
"en" : ()=>import("./en.js"),
"de" : ()=>import("./de.js")
}
// 语言作用域
const scope = new i18nScope({
...scopeSettings, // languages,defaultLanguage,activeLanguage,namespaces,formatters
id: "@voerkai18n/cli", // 当前作用域的id自动取当前工程的package.json的name
default: defaultMessages, // 默认语言包
messages : activeMessages, // 当前语言包
idMap:messageIds, // 消息id映射列表
formatters, // 当前作用域的格式化函数列表
loaders:{
"en" : ()=>import("./en.js"),
"de" : ()=>import("./de.js")
}
...scopeSettings, // languages,defaultLanguage,activeLanguage,namespaces,formatters
id : "@voerkai18n/cli", // 当前作用域的id自动取当前工程的package.json的name
debug : false, // 是否在控制台输出高度信息
default : defaultMessages, // 默认语言包
messages : activeMessages, // 当前语言包
idMap : messageIds, // 消息id映射列表
formatters, // 扩展自定义格式化器
loaders // 语言包加载器
})
// 翻译函数
const scopedTtranslate = translate.bind(scope)
export {
scopedTtranslate as t,
scope as i18nScope
}
module.exports.t = scopedTtranslate
module.exports.i18nScope = scope

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
export default {
module.exports = {
"1": "支持的语言\t: {}",
"2": "默认语言\t: {}",
"3": "激活语言\t: {}",

View File

@ -1,6 +1,6 @@
{
"name": "@voerkai18n/cli",
"version": "1.0.33",
"version": "1.0.35",
"description": "命令行工具,用来初始化/提取/编译/自动翻译等工具链",
"main": "index.js",
"homepage": "https://gitee.com/zhangfisher/voerka-i18n",
@ -50,5 +50,5 @@
"devDependencies": {
"@voerkai18n/autopublish": "workspace:^1.0.2"
},
"lastPublish": "2022-08-07T19:16:38+08:00"
"lastPublish": "2022-08-20T20:56:11+08:00"
}

View File

@ -0,0 +1,547 @@
const {i18nScope, translate, getInterpolatedVars } = require('./dist/runtime.cjs.js')
const dayjs = require('dayjs');
function toLanguageDict(values,startIndex=0){
return values.reduce((result,curValue,i)=>{
result[i+startIndex] = curValue;
return result
},{})
}
function toLanguageIdMap(values,startIndex=0){
return values.reduce((result,curValue,i)=>{
result[curValue] = i+startIndex
return result
},{})
}
// 显示两个数组哪一行不同
function diffArray(arr1,arr2){
let diffs = []
arr1.forEach((v,i)=>{
if(v!=arr2[i]) diffs.push([i,[v,arr2[i]]])
})
return diffs
}
const NOW = new Date("2022/08/12 10:12:36")
const zhDatetimes =[
"现在是{ value }",
"现在是{ value | date }",
"现在是{ value | date('local') }",
"现在是{ value | date('long') }",
"现在是{ value | date('short') }",
"现在是{ value | date('iso') }",
"现在是{ value | date('gmt') }",
"现在是{ value | date('utc') }",
"现在是{ value | date(0) }", // local
"现在是{ value | date(1) }", // long
"现在是{ value | date(2) }", // short
"现在是{ value | date(3) }", // iso
"现在是{ value | date(4) }", // gmt
"现在是{ value | date(5) }", // utc
"现在是{ value | date('YYYY-MM-DD HH:mm:ss')}",
"现在是{ value | date('YYYY-MM-DD')}",
"现在是{ value | date('HH:mm:ss')}",
"现在是{ value | month }",
"现在是{ value | month('long')}",
"现在是{ value | month('short')}",
"现在是{ value | month('number')}",
"现在是{ value | month(0)}",
"现在是{ value | month(1)}",
"现在是{ value | month(2)}",
"现在是{ value | weekday }",
"现在是{ value | weekday('long')}",
"现在是{ value | weekday('short')}",
"现在是{ value | weekday('number')}",
"现在是{ value | weekday(0)}",
"现在是{ value | weekday(1)}",
"现在是{ value | weekday(2)}",
// 时间
"现在时间 - { value | time }",
"现在时间 - { value | time('local') }",
"现在时间 - { value | time('long') }",
"现在时间 - { value | time('short') }",
"现在时间 - { value | time('timestamp') }",
"现在时间 - { value | time(0) }",
"现在时间 - { value | time(1) }",
"现在时间 - { value | time(2) }",
"现在时间 - { value | time(3) }",
"现在时间 - { value | time('HH:mm:ss') }",
"现在时间 - { value | time('mm:ss') }",
"现在时间 - { value | time('ss') }"
]
//
const expectZhDatetimes =[
"现在是2022/8/12 10:12:36", // { value }
"现在是2022/8/12 10:12:36", // { value | date }
`现在是${NOW.toLocaleString()}`, // { value | date('local') }
"现在是2022年08月12日 10点12分36秒", // { value | date('long') }
"现在是2022/08/12", // { value | date('short') }
`现在是${NOW.toISOString()}`, // { value | date('iso') }
`现在是${NOW.toGMTString()}`, // { value | date('gmt') }
`现在是${NOW.toUTCString()}`, // { value | date('utc') }
`现在是${NOW.toLocaleString()}`, // { value | date(0) } // local
"现在是2022年08月12日 10点12分36秒", // { value | date(1) } // long
"现在是2022/08/12", // { value | date(2) } // short
`现在是${NOW.toISOString()}`, // { value | date(3) } // iso
`现在是${NOW.toGMTString()}`, // { value | date(4) } // gmt
`现在是${NOW.toUTCString()}`, // { value | date(5) } // utc
"现在是2022-08-12 10:12:36", // { value | date('YYYY-MM-DD HH:mm:ss')}
"现在是2022-08-12", // { value | date('YYYY-MM-DD')}
"现在是10:12:36", // { value | date('HH:mm:ss')}
"现在是八月", // { value | month }
"现在是八月", // { value | month('long')}
"现在是八", // { value | month('short')}
"现在是8", // { value | month('number')}
"现在是八月", // { value | month(0)}
"现在是八", // { value | month(1)}
"现在是8", // { value | month(2)}
"现在是星期五", // { value | weekday }
"现在是星期五", // { value | weekday('long')}
"现在是五", // { value | weekday('short')}
"现在是5", // { value | weekday('number')}
"现在是星期五", // { value | weekday(0)}
"现在是五", // { value | weekday(1)}
"现在是5", // { value | weekday(2)}
// 时间
`现在时间 - ${NOW.toLocaleTimeString()}`, // { value | time }
`现在时间 - ${NOW.toLocaleTimeString()}`, // { value | time('local') }
"现在时间 - 10点12分36秒", // { value | time('long') }
"现在时间 - 10:12:36", // { value | time('short') }
"现在时间 - 1660270356000", // { value | time('timestamp') }
`现在时间 - ${NOW.toLocaleTimeString()}`, // { value | time(0) }
"现在时间 - 10点12分36秒", // { value | time(1) }
"现在时间 - 10:12:36", // { value | time(2) }
"现在时间 - 1660270356000", // { value | time(3) }
"现在时间 - 10:12:36", // { value | time('HH:mm:ss') }
"现在时间 - 12:36", // { value | time('mm:ss') }
"现在时间 - 36", // { value | time('ss') }"
]
const enDatetimes =[
"Now is { value }",
"Now is { value | date }",
"Now is { value | date('local') }",
"Now is { value | date('long') }",
"Now is { value | date('short') }",
"Now is { value | date('iso') }",
"Now is { value | date('gmt') }",
"Now is { value | date('utc') }",
"Now is { value | date(0) }", // local
"Now is { value | date(1) }", // long
"Now is { value | date(2) }", // short
"Now is { value | date(3) }", // iso
"Now is { value | date(4) }", // gmt
"Now is { value | date(5) }", // utc
"Now is { value | date('YYYY-MM-DD HH:mm:ss')}",
"Now is { value | date('YYYY-MM-DD')}",
"Now is { value | date('HH:mm:ss')}",
"Now is { value | month }",
"Now is { value | month('long')}",
"Now is { value | month('short')}",
"Now is { value | month('number')}",
"Now is { value | month(0)}",
"Now is { value | month(1)}",
"Now is { value | month(2)}",
"Now is { value | weekday }",
"Now is { value | weekday('long')}",
"Now is { value | weekday('short')}",
"Now is { value | weekday('number')}",
"Now is { value | weekday(0)}",
"Now is { value | weekday(1)}",
"Now is { value | weekday(2)}",
// 时间
"Now time: { value | time }",
"Now time: { value | time('local') }",
"Now time: { value | time('long') }",
"Now time: { value | time('short') }",
"Now time: { value | time('timestamp') }",
"Now time: { value | time(0) }",
"Now time: { value | time(1) }",
"Now time: { value | time(2) }",
"Now time: { value | time(3) }",
"Now time: { value | time('HH:mm:ss') }",
"Now time: { value | time('mm:ss') }",
"Now time: { value | time('ss') }"
]
const expectEnDatetimes =[
"Now is 2022/8/12 10:12:36", // { value }
"Now is 2022/8/12 10:12:36", // { value | date }
`Now is ${NOW.toLocaleString()}`, // { value | date('local') }
"Now is 2022/08/12 10:12:36", // { value | date('long') }
"Now is 2022/08/12", // { value | date('short') }
`Now is ${NOW.toISOString()}`, // { value | date('iso') }
`Now is ${NOW.toGMTString()}`, // { value | date('gmt') }
`Now is ${NOW.toUTCString()}`, // { value | date('utc') }
`Now is ${NOW.toLocaleString()}`, // { value | date(0) } // local
"Now is 2022/08/12 10:12:36", // { value | date(1) } // long
"Now is 2022/08/12", // { value | date(2) } // short
`Now is ${NOW.toISOString()}`, // { value | date(3) } // iso
`Now is ${NOW.toGMTString()}`, // { value | date(4) } // gmt
`Now is ${NOW.toUTCString()}`, // { value | date(5) } // utc
"Now is 2022-08-12 10:12:36", // { value | date('YYYY-MM-DD HH:mm:ss')}
"Now is 2022-08-12", // { value | date('YYYY-MM-DD')}
"Now is 10:12:36", // { value | date('HH:mm:ss')}
"Now is August", // { value | month }
"Now is August", // { value | month('long')}
"Now is Aug", // { value | month('short')}
"Now is 8", // { value | month('number')}
"Now is August", // { value | month(0)}
"Now is Aug", // { value | month(1)}
"Now is 8", // { value | month(2)}
"Now is Friday", // { value | weekday }
"Now is Friday", // { value | weekday('long')}
"Now is Fri", // { value | weekday('short')}
"Now is 5", // { value | weekday('number')}
"Now is Friday", // { value | weekday(0)}
"Now is Fri", // { value | weekday(1)}
"Now is 5", // { value | weekday(2)}
// 时间
`Now time: ${NOW.toLocaleTimeString()}`, // { value | time }
`Now time: ${NOW.toLocaleTimeString()}`, // { value | time('local') }
"Now time: 10:12:36", // { value | time('long') }
"Now time: 10:12:36", // { value | time('short') }
"Now time: 1660270356000", // { value | time('timestamp') }
`Now time: ${NOW.toLocaleTimeString()}`, // { value | time(0) }
"Now time: 10:12:36", // { value | time(1) }
"Now time: 10:12:36", // { value | time(2) }
"Now time: 1660270356000", // { value | time(3) }
"Now time: 10:12:36", // { value | time('HH:mm:ss') }
"Now time: 12:36", // { value | time('mm:ss') }
"Now time: 36", // { value | time('ss') }"
]
const MONEY = 123456789.88
const zhMoneys = [
"商品价格: { value | currency}", // 默认格式
// long
"商品价格: { value | currency('long')}", // 长格式
"商品价格: { value | currency('long',1)}", // 长格式: 万元
"商品价格: { value | currency('long',2)}", // 长格式: 亿
"商品价格: { value | currency('long',3)}", // 长格式: 万亿
"商品价格: { value | currency('long',4)}", // 长格式: 万万亿
// short
"商品价格: { value | currency('short')}", // 短格式
"商品价格: { value | currency('short',1)}", // 短格式 Thousands
"商品价格: { value | currency('short',2)}", // 短格式 Millions
"商品价格: { value | currency('short',3)}", // 短格式 Billions
"商品价格: { value | currency('short',4)}", // 短格式 Trillions
// 自定义货币格式
"商品价格: { value | currency({symbol:'¥¥'})}",
"商品价格: { value | currency({symbol:'¥¥',prefix:'人民币:'})}",
"商品价格: { value | currency({symbol:'¥¥',prefix:'人民币:',suffix:'元整'})}",
"商品价格: { value | currency({symbol:'¥¥',prefix:'人民币:',suffix:'元整',unit:2})}",
"商品价格: { value | currency({symbol:'¥¥',prefix:'人民币:',suffix:'元整',unit:2,precision:4})}",
"商品价格: { value | currency({symbol:'¥¥',prefix:'人民币:',suffix:'元整',unit:2,precision:4,format:'{prefix}*{symbol}*{value}*{unit}*{suffix}'})}"
]
const expectZhMoneys =[
"商品价格: ¥1,2345,6789.88", // { value | currency }
// long
"商品价格: RMB ¥1,2345,6789.88元", // { value | currency('long')}
"商品价格: RMB ¥1,2345.678988万元", // { value | currency('long',1)}
"商品价格: RMB ¥1.2345678988亿元", // { value | currency('long',2)}
"商品价格: RMB ¥0.00012345678988万亿元", // { value | currency('long',3)}
"商品价格: RMB ¥0.000000012345678988万万亿元", // { value | currency('long',4)}
// short
"商品价格: ¥1,2345,6789.88", // { value | currency('short')}
"商品价格: ¥1,2345.678988万", // { value | currency('short',1)}
"商品价格: ¥1.2345678988亿", // { value | currency('short',2)}
"商品价格: ¥0.00012345678988万亿", // { value | currency('short',3)}
"商品价格: ¥0.000000012345678988万万亿", // { value | currency('short',4)}
// 自定义货币格式
"商品价格: RMB ¥¥1,2345,6789.88元",
"商品价格: 人民币: ¥¥1,2345,6789.88元",
"商品价格: 人民币: ¥¥1,2345,6789.88元整",
"商品价格: 人民币: ¥¥1.2345678988亿元整",
"商品价格: 人民币: ¥¥1.2346+亿元整",
"商品价格: 人民币:*¥¥*1.2346+*亿*元整"
]
const enMoneys = [
"Price: { value | currency }", // 默认格式
// long
"Price: { value | currency('long') }", // 长格式
"Price: { value | currency('long',1) }", // 长格式: 万元
"Price: { value | currency('long',2) }", // 长格式: 亿
"Price: { value | currency('long',3) }", // 长格式: 万亿
"Price: { value | currency('long',4) }", // 长格式: 万万亿
// short
"Price: { value | currency('short') }", // 短格式
"Price: { value | currency('short',1) }", // 短格式 Thousands
"Price: { value | currency('short',2) }", // 短格式 Millions
"Price: { value | currency('short',3) }", // 短格式 Billions
"Price: { value | currency('short',4) }", // 短格式 Trillions
]
const expectEnMoneys =[
"Price: $123,456,789.88", // { value | currency }
// long
"Price: USD $123,456,789.88", // { value | currency('long')}
"Price: USD $123,456.78988 thousands", // { value | currency('long',1)}
"Price: USD $123.45678988 millions", // { value | currency('long',2)}
"Price: USD $0.12345678988 billions", // { value | currency('long',3)}
"Price: USD $0.00012345678988 trillions", // { value | currency('long',4)}
// short
"Price: $123,456,789.88", // { value | currency('short')}
"Price: $123,456.78988 thousands", // { value | currency('short',1)}
"Price: $123.45678988 millions", // { value | currency('short',2)}
"Price: $0.12345678988 billions", // { value | currency('short',3)}
"Price: $0.00012345678988 trillions", // { value | currency('short',4)}
]
const loaders = {
zh:{
1:"你好",
2:"现在是{ value | }",
3:"我出生于{year}年,今年{age}岁",
4:"我有{}个朋友",
...toLanguageDict(zhDatetimes,5),
},
en :{
1:"hello",
2:"Now is {}",
3:"I was born in {year}, now is {age} years old",
4:["I have no friends","I have one friends","I have two friends","I have {} friends"],
...toLanguageDict(enDatetimes,5),
}
}
const formatters = {
zh:{
$config:{},
$types:{},
book:(v)=>`${v}`,
},
en:{
$config:{},
$types:{ },
book:(v)=>`<${v}>`,
},
}
const idMap = {
"你好":1,
"现在是{ value | }":2,
"我出生于{year}年,今年{age}岁":3,
"我有{}个朋友":4,
...toLanguageIdMap(zhDatetimes,5)
}
const languages = [
{ name: "zh", title: "中文" },
{ name: "en", title: "英文" },
{ name: "de", title: "德语" },
{ name: "jp", title: "日语" }
]
const scope = new i18nScope({
id : "test",
defaultLanguage: "zh",
activeLanguage : "zh",
namespaces : {},
default : loaders.zh, // 默认语言包
messages : loaders.zh, // 当前语言包
languages, // 语言配置
idMap, // 消息id映射列表
formatters, // 扩展自定义格式化器
loaders // 语言包加载器
})
const t = translate.bind(scope)
// 适用于所有语言的格式化器,并且注册到全局
scope.registerFormatters({
"*":{
sum : (v,n=1)=>v+n,
double: (v)=>v*2,
upper : (v)=>v.toUpperCase()
}
},true)
describe("翻译函数",()=>{
beforeEach(() => {
scope.change("zh")
});
test("获取翻译内容中的插值变量",done=>{
const results = getInterpolatedVars("中华人民共和国成立于{date | year(1,2) | time('a','b') | rel }年,首都是{city}市");
expect(results.length).toEqual(2);
//
expect(results[0].name).toEqual("date");
expect(results[0].formatters.length).toEqual(3);
// year(1,2)
expect(results[0].formatters[0].name).toEqual("year");
expect(results[0].formatters[0].args).toEqual([1,2]);
// time('a','b')
expect(results[0].formatters[1].name).toEqual("time");
expect(results[0].formatters[1].args).toEqual(["a","b"]);
// rel
expect(results[0].formatters[2].name).toEqual("rel");
expect(results[0].formatters[2].args).toEqual([]);
expect(results[1].name).toEqual("city");
expect(results[1].formatters.length).toEqual(0);
done()
})
test("获取翻译内容中定义了重复的插值变量",done=>{
const results = getInterpolatedVars("{a}{a}{a|x}{a|x}{a|x|y}{a|x|y}");
expect(results.length).toEqual(3);
expect(results[0].name).toEqual("a");
expect(results[0].formatters.length).toEqual(0);
expect(results[1].name).toEqual("a");
expect(results[1].formatters.length).toEqual(1);
expect(results[1].formatters[0].name).toEqual("x");
expect(results[1].formatters[0].args).toEqual([]);
expect(results[2].name).toEqual("a");
expect(results[2].formatters.length).toEqual(2);
expect(results[2].formatters[0].name).toEqual("x");
expect(results[2].formatters[0].args).toEqual([]);
expect(results[2].formatters[1].name).toEqual("y");
expect(results[2].formatters[1].args).toEqual([]);
done()
})
test("替换翻译内容的位置插值变量",done=>{
expect(t("{}{}{}",1,2,3)).toBe("123");
expect(t("{a}{b}{c}",1,2,3)).toBe("123");
// 定义了一些无效的格式化器,直接忽略
expect(t("{a|xxx}{b|dd}{c|}",1,2,3)).toBe("123");
expect(t("{a|xxx}{b|dd}{c|}",1,2,3,4,5,6)).toBe("123");
expect(t("{ a|}{b|dd}{c|}{}",1,2,3)).toBe("123{}");
// 中文状态下true和false被转换成中文的"是"和"否"
expect(t("{}{}{}",1,"2",true)).toBe("12是");
expect(t("{|double}{}{}",1,"2",true)).toBe("22是");
done()
})
test("替换翻译内容的命名插值变量",done=>{
expect(t("{a}{b}{c}",{a:11,b:22,c:33})).toBe("112233");
expect(t("{a}{b}{c}{a}{b}{c}",{a:1,b:"2",c:3})).toBe("123123");
done()
})
test("命名插值变量使用格式化器",done=>{
// 提供无效的格式化器,直接忽略
expect(t("{a|x}{b|x|y}{c|}",{a:1,b:2,c:3})).toBe("123");
expect(t("{a|x}{b|x|y}{c|double}",{a:1,b:2,c:3})).toBe("126");
// padStart和trim格式化器只是字符串的原型方法不需要额外定义可以直接使用
expect(t("{a|padStart(10)}",{a:"123"})).toBe(" 123");
expect(t("{a|padStart(10)|trim}",{a:"123"})).toBe("123");
done()
})
test("命名插值变量使用格式化器",done=>{
// 提供无效的格式化器,直接忽略
expect(t("{a|x}{b|x|y}{c|}",{a:1,b:2,c:3})).toBe("123");
expect(t("{a|x}{b|x|y}{c|double}",{a:1,b:2,c:3})).toBe("126");
// 默认的字符串格式化器,不需要定义使用字符串方法
expect(t("{a|x}{b|x|y}{c|double}",{a:1,b:2,c:3})).toBe("126");
expect(t("{a|padStart(10)}",{a:"123"})).toBe(" 123");
expect(t("{a|padStart(10)|trim}",{a:"123"})).toBe("123");
done()
})
test("切换到其他语言时的自动匹配同名格式化器",async ()=>{
expect(t("{a}",{a:true})).toBe("是");
expect(t("{name|book}是毛泽东思想的重要载体","毛泽东选集")).toBe("《毛泽东选集》是毛泽东思想的重要载体");
await scope.change("en")
expect(t("{a}",{a:false})).toBe("False");
expect(t("{name|book}是毛泽东思想的重要载体","毛泽东选集")).toBe("<毛泽东选集>是毛泽东思想的重要载体");
})
test("位置插值翻译文本内容",async ()=>{
const now = new Date()
expect(t("你好")).toBe("你好");
expect(t("现在是{ value | }",now)).toBe(`现在是${dayjs(now).format('YYYY/M/D HH:mm:ss')}`);
// 经babel自动码换后文本内容会根据idMap自动转为id
expect(t("1")).toBe("你好");
expect(t("2",now)).toBe(`现在是${dayjs(now).format('YYYY/M/D HH:mm:ss')}`);
await scope.change("en")
expect(t("你好")).toBe("hello");
expect(t("现在是{ value | }",now)).toBe(`Now is ${dayjs(now).format('YYYY/M/D HH:mm:ss')}`);
expect(t("1")).toBe("hello");
expect(t("2",now)).toBe(`Now is ${dayjs(now).format('YYYY/M/D HH:mm:ss')}`);
})
test("命名插值翻译文本内容",async ()=>{
const now = new Date()
expect(t("你好")).toBe("你好");
expect(t("现在是{ value | }",now)).toBe(`现在是${dayjs(now).format('YYYY/M/D HH:mm:ss')}`);
await scope.change("en")
expect(t("你好")).toBe("hello");
expect(t("现在是{ value | }",now)).toBe(`Now is ${dayjs(now).format('YYYY/M/D HH:mm:ss')}`);
// 使用idMap
expect(t("1")).toBe("hello");
expect(t("2",now)).toBe(`Now is ${dayjs(now).format('YYYY/M/D HH:mm:ss')}`);
})
test("当没有对应的语言翻译时,保持原始输出",async ()=>{
expect(t("我是中国人")).toBe("我是中国人");
await scope.change("en")
expect(t("我是中国人")).toBe("我是中国人");
})
test("切换到未知语言时回退到默认语言",async ()=>{
expect(t("我是中国人")).toBe("我是中国人");
expect(async ()=>await scope.change("xn")).rejects.toThrow(Error);
expect(t("我是中国人")).toBe("我是中国人");
})
test("翻译复数支持",async ()=>{
await scope.change("en")
expect(t("我有{}个朋友",0)).toBe("I have no friends");
expect(t("我有{}个朋友",1)).toBe("I have one friends");
expect(t("我有{}个朋友",2)).toBe("I have two friends");
expect(t("我有{}个朋友",3)).toBe("I have 3 friends");
expect(t("我有{}个朋友",4)).toBe("I have 4 friends");
})
test("日期时间格式化器",async ()=>{
let zhTranslatedResults = zhDatetimes.map(v=>t(v,NOW))
expect(zhTranslatedResults).toStrictEqual(expectZhDatetimes)
await scope.change("en")
let enTranslatedResults = zhDatetimes.map(v=>t(v,NOW))
expect(enTranslatedResults).toStrictEqual(expectEnDatetimes)
})
test("货币格式化器",async ()=>{
let zhMoneysResults = zhMoneys.map(v=>t(v,MONEY))
expect(zhMoneysResults).toStrictEqual(expectZhMoneys)
await scope.change("en")
let enMoneysResults = enMoneys.map(v=>t(v,MONEY))
expect(enMoneysResults).toStrictEqual(expectEnMoneys)
})
})

View File

@ -0,0 +1,308 @@
/**
*
* 解析格式化器
*
* 解析{ 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参数
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=>getByPath(formatterConfig,param,undefined))
// 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(finalValue,...finalArgs,formatterConfig)
}
$formatter.configurable = true // 当函数是可配置时才在最后一个参数中传入$config
return $formatter
}
const Formatter = createFormatter
module.exports = {
createFormatter,
Formatter,
parseFormatters
}

View File

@ -3,7 +3,8 @@
*
*/
const { toDate,toCurrency,toNumber,isPlainObject,formatDatetime,formatTime,Formatter } = require("../utils")
const { toDate,toCurrency,toNumber,isPlainObject,formatDatetime,formatTime } = require("../utils")
const { Formatter } = require("../formatter")
/**
* 日期格式化器
@ -138,7 +139,7 @@ const currencyFormatter = Formatter((value,...args) =>{
if(args.length==1) { // 无参调用
Object.assign(params,{format:'default'})
}else if(args.length==2 && isPlainObject(args[0])){ // 一个参数且是{}
Object.assign(params,args[0])
Object.assign(params,{format:$config.custom},args[0])
}else if(args.length==2){
// 一个字符串参数只能是default,long,short, 或者是一个模板字符串,如"{symbol}{value}{unit}"
Object.assign(params,{format:args[0]})
@ -206,6 +207,7 @@ module.exports = {
default : "{symbol}{value}{unit}",
long : "{prefix} {symbol}{value}{unit}{suffix}",
short : "{symbol}{value}{unit}",
custom : "{prefix} {symbol}{value}{unit}{suffix}",
//--
units : [""," thousands"," millions"," billions"," trillions"], //千,百万,十亿,万亿
radix : 3, // 进制即三位一进制中文是是4位一进

View File

@ -1,5 +1,6 @@
const {createFormatter,Formatter,getDataTypeName,isNumber,isPlainObject,isFunction,isNothing,deepMerge,deepMixin} = require("./utils")
const {getDataTypeName,isNumber,isPlainObject,isFunction,isNothing,deepMerge,deepMixin} = require("./utils")
const {getInterpolatedVars,replaceInterpolatedVars} = require("./interpolate")
const {createFormatter,Formatter} = require("./formatter")
const EventEmitter = require("./eventemitter")
const inlineFormatters = require("./formatters")
const i18nScope = require("./scope")
@ -155,7 +156,7 @@ const defaultLanguageSettings = {
}
}
}
}
module.exports ={
getInterpolatedVars,

View File

@ -1,11 +1,11 @@
/**
*
*
* 处理翻译文本中的插件变量
*
*
* 处理逻辑如下
*
*
* "Now is { value | date | prefix('a') | suffix('b')}"为例
*
*
* 1. 先判断一下输入是否包括{}字符如果是则说明可能存在插值变量如果没有则说明一定不存在插值变量
* 这样做的目的是如果确认不存在插值变量时就不需要后续的正则表表达式匹配提取过程
* 这对大部份没有采用插件变量的文本能提高性能
@ -24,122 +24,34 @@
* [(value,config)=>{....},(value,config)=>{....},(value,config)=>{....}]
* 为优化性能在从格式化器名称转换为函数过程中会进行缓存
* 6. 最后只需要依次执行这些格式化化器函数即可
*
*
*
*
*/
const { getDataTypeName,isPlainObject,isFunction,replaceAll } = require("./utils");
const { parseFormatters } = require("./formatter")
const {createFormatter,Formatter,getDataTypeName,isNumber,isPlainObject,isFunction,safeParseJson} = require("./utils")
const EventEmitter = require("./eventemitter")
const inlineFormatters = require("./formatters")
const i18nScope = require("./scope")
// 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter }
// 支持参数: { var | formatter(x,x,..) | formatter }
let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*(\(.*\)){0,1}\s*)*)\s*\}/g
// 插值变量字符串替换正则
let varReplaceRegexp =String.raw`\{\s*{varname}\s*\}`
// 提取匹配("a",1,2,'b',{..},[...])
const formatterParamsRegex = /((([\'\"])(.*?)\3)|(\{.*?\})|(\[.*?\])|([\d]+\.?[\d]?)|((true|false|null)(?=[,\b\s]))|([\w\.]+)|((?<=,)\s*(?=,)))(?<=\s*[,\)]?\s*)/g;
// 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter }
// 支持参数: { var | formatter(x,x,..) | formatter }
let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*(\(.*\)){0,1}\s*)*)\s*\}/g;
/**
* 考虑到通过正则表达式进行插值的替换可能较慢
* 因此提供一个简单方法来过滤掉那些不需要进行插值处理的字符串
* 原理很简单就是判断一下是否同时具有{}字符如果有则认为可能有插值变量如果没有则一定没有插件变量则就不需要进行正则匹配
* 从而可以减少不要的正则匹配
* 注意该方法只能快速判断一个字符串不包括插值变量
* @param {*} str
* @returns {boolean} true=可能包含插值变量
*/
function hasInterpolation(str){
return str.includes("{") && str.includes("}")
}
/**
使用正则表达式对原始文本内容进行解析匹配后得到的便以处理的数组
formatters="| aaa(1,1) | bbb "
统一解析为
[
[aaa,[1,1]], // [formatter'name,[args,...]]
[<格式化器名称>,[<参数>,<参数>,...]]
]
formatters="| aaa(1,1,"dddd") | bbb "
特别注意
- 目前对参数采用简单的split(",")来解析因此如果参数中包括了逗号等会影响解析的字符时可能导致错误
例如aaa(1,1,"dd,,dd")形式的参数
在此场景下基本够用了如果需要支持更复杂的参数解析可以后续考虑使用正则表达式来解析
- 如果参数是{},[]则尝试解决为对象和数组但是里面的内容无法支持复杂和嵌套数据类型
@returns [ [<格式化器名称>,[<参数>,<参数>,...],[<格式化器名称>,[<参数>,<参数>,...]],...]
*/
function parseFormatters(formatters){
if(!formatters) return []
// 1. 先解析为 ["aaa()","bbb"]形式
let result = formatters.trim().substr(1).trim().split("|").map(r=>r.trim())
// 2. 解析格式化器参数
return result.map(formatter=>{
if(formatter=="") return null
let firstIndex = formatter.indexOf("(")
let lastIndex = formatter.lastIndexOf(")")
if(firstIndex!==-1 && lastIndex!==-1){ // 带参数的格式化器
// 取括号中的参数字符串内容
const strParams = formatter.substr(firstIndex+1,lastIndex-firstIndex-1).trim()
// 解析出格式化的参数数组
let params = parseFormaterParams(strParams)
// 返回[<格式化器名称>,[<参数>,<参数>,...]
return [formatter.substr(0,firstIndex),params]
}else{// 不带参数的格式化器
return [formatter,[]]
}
}).filter(formatter=> Array.isArray(formatter))
}
/**
* 解析格式化器的参数
*
* 采用正则表达式解析缺点是无法解析嵌套的{}[]因此不能在格式化器参数中使用复杂嵌套格式的{}[]
*
* @param {*} strParams
* @returns {Array} 返回参数值数组 []
/**
* 考虑到通过正则表达式进行插值的替换可能较慢
* 因此提供一个简单方法来过滤掉那些不需要进行插值处理的字符串
* 原理很简单就是判断一下是否同时具有{}字符如果有则认为可能有插值变量如果没有则一定没有插件变量则就不需要进行正则匹配
* 从而可以减少不要的正则匹配
* 注意该方法只能快速判断一个字符串不包括插值变量
* @param {*} str
* @returns {boolean} true=可能包含插值变量
*/
function parseFormaterParams(strParams) {
let params = [];
let matched;
try{
while ((matched = formatterParamsRegex.exec(strParams)) !== null) {
// 这对于避免零宽度匹配的无限循环是必要的
if (matched.index === formatterParamsRegex.lastIndex) {
formatterParamsRegex.lastIndex++;
}
let value = matched[0]
if(value.trim()==''){
value = null
}else if((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"'))){
value = value.substring(1,value.length-1)
}else if((value.startsWith("{") && value.endsWith("}")) || (value.startsWith('[') && value.endsWith(']'))){
try{
value = safeParseJson(value)
}catch{}
}else if(["true","false","null"].includes(value)){
value = JSON.parse(value)
}else if(isNumber(value)){
value = parseFloat(value)
}else{
value =String(value)
}
params.push(value)
}
}catch{
}
return params
function hasInterpolation(str) {
return str.includes("{") && str.includes("}");
}
/**
/**
* 解析格式化器的参数
*
/**
* 提取字符串中的插值变量
* [
// {
@ -161,70 +73,65 @@ function parseFormaterParams(strParams) {
* ...
* ]
*/
function getInterpolatedVars(str){
let vars = []
forEachInterpolatedVars(str,(varName,formatters,match)=>{
let varItem = {
name:varName,
formatters:formatters.map(([formatter,args])=>{
return {
name:formatter,
args:args
}
}),
match:match
}
if(vars.findIndex(varDef=>((varDef.name===varItem.name) && (varItem.formatters.toString() == varDef.formatters.toString())))===-1){
vars.push(varItem)
}
return ""
})
return vars
}
/**
* 遍历str中的所有插值变量传递给callback将callback返回的结果替换到str中对应的位置
* @param {*} str
* @param {Function(<变量名称>,[formatters],match[0])} callback
* @returns 返回替换后的字符串
*/
function forEachInterpolatedVars(str,replacer,options={}){
let result=str, match
let opts = Object.assign({
replaceAll:true, // 是否替换所有插值变量当使用命名插值时应置为true当使用位置插值时应置为false
},options)
varWithPipeRegexp.lastIndex=0
while ((match = varWithPipeRegexp.exec(result)) !== null) {
const varname = match.groups.varname || ""
// 解析格式化器和参数 = [<formatterName>,[<formatterName>,[<arg>,<arg>,...]]]
const formatters = parseFormatters(match.groups.formatters)
if(isFunction(replacer)){
try{
const finalValue = replacer(varname,formatters,match[0])
if(opts.replaceAll){ // 在某此版本上可能没有
result=result.replaceAll(match[0],finalValue)
}else{
result=result.replace(match[0],finalValue)
}
}catch{// callback函数可能会抛出异常如果抛出异常则中断匹配过程
break
}
}
varWithPipeRegexp.lastIndex=0
}
return result
}
/**
* 清空指定语言的缓存
* @param {*} scope
* @param {*} activeLanguage
function getInterpolatedVars(str) {
let vars = [];
forEachInterpolatedVars(str, (varName, formatters, match) => {
let varItem = {
name: varName,
formatters: formatters.map(([formatter, args]) => {
return {name: formatter,args: args };
}),
match: match,
};
if (vars.findIndex((varDef) =>varDef.name === varItem.name && varItem.formatters.toString() ==varDef.formatters.toString()) === -1){
vars.push(varItem);
}
return "";
});
return vars;
}
/**
* 遍历str中的所有插值变量传递给callback将callback返回的结果替换到str中对应的位置
* @param {*} str
* @param {Function(<变量名称>,[formatters],match[0])} callback
* @param {Boolean} replaceAll 是否替换所有插值变量当使用命名插值时应置为true当使用位置插值时应置为false
* @returns 返回替换后的字符串
*/
function resetScopeCache(scope,activeLanguage=null){
scope.$cache = {activeLanguage,typedFormatters:{},formatters:{}}
function forEachInterpolatedVars(str, replacer, options = {}) {
let result = str, matched;
let opts = Object.assign({replaceAll: true },options);
varWithPipeRegexp.lastIndex = 0;
while ((matched = varWithPipeRegexp.exec(result)) !== null) {
const varname = matched.groups.varname || "";
// 解析格式化器和参数 = [<formatterName>,[<formatterName>,[<arg>,<arg>,...]]]
const formatters = parseFormatters(matched.groups.formatters);
if (isFunction(replacer)) {
try {
const finalValue = replacer(varname, formatters, matched[0]);
if (opts.replaceAll) {
result = replaceAll(result,matched[0], finalValue);
} else {
result = result.replace(matched[0], finalValue);
}
} catch {
break;// callback函数可能会抛出异常如果抛出异常则中断匹配过程
}
}
varWithPipeRegexp.lastIndex = 0;
}
return result;
}
/**
* 清空指定语言的缓存
* @param {*} scope
* @param {*} activeLanguage
*/
function resetScopeCache(scope, activeLanguage = null) {
scope.$cache = { activeLanguage, typedFormatters: {}, formatters: {} };
}
/**
/**
* 取得指定数据类型的默认格式化器
*
* 可以为每一个数据类型指定一个默认的格式化器,当传入插值变量时
@ -248,239 +155,255 @@ function resetScopeCache(scope,activeLanguage=null){
* @param {*} dataType 数字类型
* @returns {Function} 格式化函数
*/
function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){
// 当指定数据类型的的默认格式化器的缓存处理
if(!scope.$cache) resetScopeCache(scope)
if(scope.$cache.activeLanguage === activeLanguage) {
if(dataType in scope.$cache.typedFormatters) return scope.$cache.typedFormatters[dataType]
}else{// 当语言切换时清空缓存
resetScopeCache(scope,activeLanguage)
}
const fallbackLanguage = scope.getLanguage(activeLanguage).fallback;
// 先在当前作用域中查找,再在全局查找
const targets = [
scope.activeFormatters,
scope.formatters[fallbackLanguage], // 如果指定了回退语言时,也在该回退语言中查找
scope.global.formatters[activeLanguage],
scope.global.formatters["*"]
]
for(const target of targets){
if(!target) continue
if(isPlainObject(target.$types) && isFunction(target.$types[dataType])){
return scope.$cache.typedFormatters[dataType] = target.$types[dataType]
}
}
}
/**
* 获取指定名称的格式化器函数
*
* 查找逻辑
* - 在当前作用域中查找
* - 在全局作用域中查找
*
* @param {*} scope
* @param {*} activeLanguage 当前激活语言名称
* @param {*} name 格式化器名称
* @returns {Function} 格式化函数
*/
function getFormatter(scope,activeLanguage,name){
// 1. 从缓存中直接读取: 缓存格式化器引用,避免重复检索
if(!scope.$cache) resetScopeCache(scope)
if(scope.$cache.activeLanguage === activeLanguage) {
if(name in scope.$cache.formatters) return scope.$cache.formatters[name]
}else{// 当语言切换时清空缓存
resetScopeCache(scope,activeLanguage)
}
const fallbackLanguage = scope.getLanguage(activeLanguage).fallback
// 2. 先在当前作用域中查找,再在全局查找 formatters={$types,$config,[格式化器名称]:()=>{},[格式化器名称]:()=>{}}
const range = [
scope.activeFormatters,
scope.formatters[fallbackLanguage], // 如果指定了回退语言时,也在该回退语言中查找
scope.global.formatters[activeLanguage], // 适用于activeLanguage全局格式化器
scope.global.formatters["*"], // 适用于所有语言的格式化器
]
for(const formatters of range){
if(!formatters) continue
if(isFunction(formatters[name])) {
return scope.$cache.formatters[name] = formatters[name]
}
}
}
/**
* Checker是一种特殊的格式化器会在特定的时间执行
*
* Checker应该返回{value,next}用来决定如何执行下一个格式化器函数
*
*
* @param {*} checker
* @param {*} value
* @returns
*/
function executeChecker(checker,value){
let result ={ value, next:"skip"}
if(!isFunction(checker)) return result
try{
const r = checker(value)
if(isPlainObject(r)) {
Object.assign(result,r)
}else{
result.value = r
}
if(!["break","skip"].includes(result.next)) result.next="break"
}catch(e){
}
return result
}
/**
* 执行格式化器并返回结果
*
* 格式化器this指向当前scope并且最后一个参数是当前scope格式化器的$config
*
* 这样格式化器可以读取$config
*
* @param {*} value
* @param {Array[Function]} formatters 多个格式化器函数(经过包装过的)顺序执行前一个输出作为下一个格式化器的输入
*/
function executeFormatter(value,formatters,scope,template){
if(formatters.length===0) return value
let result = value
// 1. 空值检查
const emptyCheckerIndex = formatters.findIndex(func=>func.$name==='empty')
if(emptyCheckerIndex!=-1){
const emptyChecker = formatters.splice(emptyCheckerIndex,1)[0]
const { value,next } = executeChecker(emptyChecker,result)
if(next == 'break') {
return value
}else{
result = value
}
}
// 2. 错误检查
const errorCheckerIndex = formatters.findIndex(func=>func.$name==='error')
let errorChecker
if(errorCheckerIndex!=-1){
errorChecker = formatters.splice(errorCheckerIndex,1)[0]
if(result instanceof Error){
result.formatter = formatter.$name
const { value,next } = executeChecker(errorChecker,result)
if(next == 'break') {
return value
}else{
result = value
}
}
}
// 3. 分别执行格式化器函数
for(let formatter of formatters){
try{
result = formatter(result,scope.activeFormatterConfig)
}catch(e){
e.formatter = formatter.$name
if(scope.debug) console.error(`Error while execute i18n formatter<${formatter.$name}> for ${template}: ${e.message} ` )
if(isFunction(errorChecker)){
const { value,next } = executeChecker(errorChecker,result)
if(next=="break"){
if(value!==undefined) result = value
break
}else if(next=="skip"){
continue
}
}
}
}
return result
}
/**
* 添加默认的empty和error格式化器用来提供默认的空值和错误处理逻辑
*
* empty和error格式化器有且只能有一个其他无效
*
* @param {*} formatters
*/
function addDefaultFormatters(formatters){
// 默认的空值处理逻辑: 转换为"",然后继续执行接下来的逻辑
if(formatters.findIndex(([name])=>name=="empty")===-1){
formatters.push(["empty",[]])
}
// 默认的错误处理逻辑: 开启DEBUG时会显示ERROR:message关闭DEBUG时会保持最近值不变然后中止后续执行
if(formatters.findIndex(([name])=>name=="error")===-1){
formatters.push(["error",[]])
}
}
/**
*
* 经parseFormatters解析t('{}')中的插值表达式中的格式化器后会得到
* [[<格式化器名称>,[参数,参数,...]][<格式化器名称>,[参数,参数,...]]]数组
*
* 本函数将之传换为转化为调用函数链形式如下
* [(v)=>{...},(v)=>{...},(v)=>{...}]
*
* 并且会自动将当前激活语言的格式化器配置作为最后一个参数配置传入,这样格式化器函数就可以读取
*
* @param {*} scope
* @param {*} activeLanguage
* @param {*} formatters
* @returns {Array} [(v)=>{...},(v)=>{...},(v)=>{...}]
*
*/
function wrapperFormatters(scope,activeLanguage,formatters){
let wrappedFormatters = []
addDefaultFormatters(formatters)
for(let [name,args] of formatters){
let fn = getFormatter(scope,activeLanguage,name)
let formatter
// 格式化器无效或者没有定义时,查看当前值是否具有同名的原型方法,如果有则执行调用
// 比如padStart格式化器是String的原型方法不需要配置就可以直接作为格式化器调用
if(isFunction(fn)){
formatter = (value,config) => fn.call(scope,value,...args,config)
}else{
formatter = (value) =>{
if(isFunction(value[name])){
return value[name](...args)
}else{
return value
}
}
}
formatter.$name = name
wrappedFormatters.push(formatter)
}
return wrappedFormatters
}
/**
* 将value经过格式化器处理后返回的结果
* @param {*} scope
* @param {*} activeLanguage
* @param {*} formatters
* @param {*} value
* @returns
*/
function getFormattedValue(scope,activeLanguage,formatters,value,template){
// 1. 取得格式化器函数列表,然后经过包装以传入当前格式化器的配置参数
const formatterFuncs = wrapperFormatters(scope,activeLanguage,formatters)
// 3. 执行格式化器
// EMPTY和ERROR是默认两个格式化器如果只有两个则说明在t(...)中没有指定格式化器
if(formatterFuncs.length==2){
// 当没有格式化器时,查询是否指定了默认数据类型的格式化器,如果有则执行
const defaultFormatter = getDataTypeDefaultFormatter(scope,activeLanguage,getDataTypeName(value))
if(defaultFormatter){
return executeFormatter(value,[defaultFormatter],scope,template)
}
}else{
value = executeFormatter(value,formatterFuncs,scope,template)
}
return value
}
/**
function getDataTypeDefaultFormatter(scope, activeLanguage, dataType) {
// 当指定数据类型的的默认格式化器的缓存处理
if (!scope.$cache) resetScopeCache(scope);
if (scope.$cache.activeLanguage === activeLanguage) {
if (dataType in scope.$cache.typedFormatters)
return scope.$cache.typedFormatters[dataType];
} else {
// 当语言切换时清空缓存
resetScopeCache(scope, activeLanguage);
}
const fallbackLanguage = scope.getLanguage(activeLanguage).fallback;
// 先在当前作用域中查找,再在全局查找
const targets = [
scope.activeFormatters,
scope.formatters[fallbackLanguage], // 如果指定了回退语言时,也在该回退语言中查找
scope.global.formatters[activeLanguage],
scope.global.formatters["*"],
];
for (const target of targets) {
if (!target) continue;
if (
isPlainObject(target.$types) &&
isFunction(target.$types[dataType])
) {
return (scope.$cache.typedFormatters[dataType] =
target.$types[dataType]);
}
}
}
/**
* 获取指定名称的格式化器函数
*
* 查找逻辑
* - 在当前作用域中查找
* - 在全局作用域中查找
*
* @param {*} scope
* @param {*} activeLanguage 当前激活语言名称
* @param {*} name 格式化器名称
* @returns {Function} 格式化函数
*/
function getFormatter(scope, activeLanguage, name) {
// 1. 从缓存中直接读取: 缓存格式化器引用,避免重复检索
if (!scope.$cache) resetScopeCache(scope);
if (scope.$cache.activeLanguage === activeLanguage) {
if (name in scope.$cache.formatters)
return scope.$cache.formatters[name];
} else {
// 当语言切换时清空缓存
resetScopeCache(scope, activeLanguage);
}
const fallbackLanguage = scope.getLanguage(activeLanguage).fallback;
// 2. 先在当前作用域中查找,再在全局查找 formatters={$types,$config,[格式化器名称]:()=>{},[格式化器名称]:()=>{}}
const range = [
scope.activeFormatters,
scope.formatters[fallbackLanguage], // 如果指定了回退语言时,也在该回退语言中查找
scope.global.formatters[activeLanguage], // 适用于activeLanguage全局格式化器
scope.global.formatters["*"], // 适用于所有语言的格式化器
];
for (const formatters of range) {
if (!formatters) continue;
if (isFunction(formatters[name])) {
return (scope.$cache.formatters[name] = formatters[name]);
}
}
}
/**
* Checker是一种特殊的格式化器会在特定的时间执行
*
* Checker应该返回{value,next}用来决定如何执行下一个格式化器函数
*
*
* @param {*} checker
* @param {*} value
* @returns
*/
function executeChecker(checker, value) {
let result = { value, next: "skip" };
if (!isFunction(checker)) return result;
try {
const r = checker(value);
if (isPlainObject(r)) {
Object.assign(result, r);
} else {
result.value = r;
}
if (!["break", "skip"].includes(result.next)) result.next = "break";
} catch (e) {}
return result;
}
/**
* 执行格式化器并返回结果
*
* 格式化器this指向当前scope并且最后一个参数是当前scope格式化器的$config
*
* 这样格式化器可以读取$config
*
* @param {*} value
* @param {Array[Function]} formatters 多个格式化器函数(经过包装过的)顺序执行前一个输出作为下一个格式化器的输入
*/
function executeFormatter(value, formatters, scope, template) {
if (formatters.length === 0) return value;
let result = value;
// 1. 空值检查
const emptyCheckerIndex = formatters.findIndex(
(func) => func.$name === "empty"
);
if (emptyCheckerIndex != -1) {
const emptyChecker = formatters.splice(emptyCheckerIndex, 1)[0];
const { value, next } = executeChecker(emptyChecker, result);
if (next == "break") {
return value;
} else {
result = value;
}
}
// 2. 错误检查
const errorCheckerIndex = formatters.findIndex(
(func) => func.$name === "error"
);
let errorChecker;
if (errorCheckerIndex != -1) {
errorChecker = formatters.splice(errorCheckerIndex, 1)[0];
if (result instanceof Error) {
result.formatter = formatter.$name;
const { value, next } = executeChecker(errorChecker, result);
if (next == "break") {
return value;
} else {
result = value;
}
}
}
// 3. 分别执行格式化器函数
for (let formatter of formatters) {
try {
result = formatter(result, scope.activeFormatterConfig);
} catch (e) {
e.formatter = formatter.$name;
if (scope.debug)
console.error(
`Error while execute i18n formatter<${formatter.$name}> for ${template}: ${e.message} `
);
if (isFunction(errorChecker)) {
const { value, next } = executeChecker(errorChecker, result);
if (next == "break") {
if (value !== undefined) result = value;
break;
} else if (next == "skip") {
continue;
}
}
}
}
return result;
}
/**
* 添加默认的empty和error格式化器用来提供默认的空值和错误处理逻辑
*
* empty和error格式化器有且只能有一个其他无效
*
* @param {*} formatters
*/
function addDefaultFormatters(formatters) {
// 默认的空值处理逻辑: 转换为"",然后继续执行接下来的逻辑
if (formatters.findIndex(([name]) => name == "empty") === -1) {
formatters.push(["empty", []]);
}
// 默认的错误处理逻辑: 开启DEBUG时会显示ERROR:message关闭DEBUG时会保持最近值不变然后中止后续执行
if (formatters.findIndex(([name]) => name == "error") === -1) {
formatters.push(["error", []]);
}
}
/**
*
* 经parseFormatters解析t('{}')中的插值表达式中的格式化器后会得到
* [[<格式化器名称>,[参数,参数,...]][<格式化器名称>,[参数,参数,...]]]数组
*
* 本函数将之传换为转化为调用函数链形式如下
* [(v)=>{...},(v)=>{...},(v)=>{...}]
*
* 并且会自动将当前激活语言的格式化器配置作为最后一个参数配置传入,这样格式化器函数就可以读取
*
* @param {*} scope
* @param {*} activeLanguage
* @param {*} formatters
* @returns {Array} [(v)=>{...},(v)=>{...},(v)=>{...}]
*
*/
function wrapperFormatters(scope, activeLanguage, formatters) {
let wrappedFormatters = [];
addDefaultFormatters(formatters);
for (let [name, args] of formatters) {
let fn = getFormatter(scope, activeLanguage, name);
let formatter;
// 格式化器无效或者没有定义时,查看当前值是否具有同名的原型方法,如果有则执行调用
// 比如padStart格式化器是String的原型方法不需要配置就可以直接作为格式化器调用
if (isFunction(fn)) {
formatter = (value, config) =>
fn.call(scope, value, ...args, config);
} else {
formatter = (value) => {
if (isFunction(value[name])) {
return value[name](...args);
} else {
return value;
}
};
}
formatter.$name = name;
wrappedFormatters.push(formatter);
}
return wrappedFormatters;
}
/**
* 将value经过格式化器处理后返回的结果
* @param {*} scope
* @param {*} activeLanguage
* @param {*} formatters
* @param {*} value
* @returns
*/
function getFormattedValue(scope, activeLanguage, formatters, value, template) {
// 1. 取得格式化器函数列表,然后经过包装以传入当前格式化器的配置参数
const formatterFuncs = wrapperFormatters(scope, activeLanguage, formatters);
// 3. 执行格式化器
// EMPTY和ERROR是默认两个格式化器如果只有两个则说明在t(...)中没有指定格式化器
if (formatterFuncs.length == 2) {
// 当没有格式化器时,查询是否指定了默认数据类型的格式化器,如果有则执行
const defaultFormatter = getDataTypeDefaultFormatter(
scope,
activeLanguage,
getDataTypeName(value)
);
if (defaultFormatter) {
return executeFormatter(value, [defaultFormatter], scope, template);
}
} else {
value = executeFormatter(value, formatterFuncs, scope, template);
}
return value;
}
/**
* 字符串可以进行变量插值替换
* replaceInterpolatedVars("<模板字符串>",{变量名称:变量值,变量名称:变量值,...})
* replaceInterpolatedVars("<模板字符串>",[变量值,变量值,...])
@ -497,48 +420,43 @@ function resetScopeCache(scope,activeLanguage=null){
* @param {*} template
* @returns
*/
function replaceInterpolatedVars(template,...args) {
const scope = this
// 当前激活语言
const activeLanguage = scope.global.activeLanguage
let result=template
// 没有变量插值则的返回原字符串
if(args.length===0 || !hasInterpolation(template)) return template
// ****************************变量插值****************************
if(args.length===1 && isPlainObject(args[0])){
// 读取模板字符串中的插值变量列表
// [[var1,[formatter,formatter,...],match],[var2,[formatter,formatter,...],match],...}
let varValues = args[0]
return forEachInterpolatedVars(template,(varname,formatters,match)=>{
let value = (varname in varValues) ? varValues[varname] : ''
return getFormattedValue(scope,activeLanguage,formatters,value,template)
})
}else{
// ****************************位置插值****************************
// 如果只有一个Array参数则认为是位置变量列表进行展开
const params=(args.length===1 && Array.isArray(args[0])) ? [...args[0]] : args
if(params.length===0) return template // 没有变量则不需要进行插值处理,返回原字符串
let i = 0
return forEachInterpolatedVars(template,(varname,formatters,match)=>{
if(params.length>i){
return getFormattedValue(scope,activeLanguage,formatters,params[i++],template)
}else{
throw new Error() // 抛出异常,停止插值处理
}
},{replaceAll:false})
}
return result
}
function replaceInterpolatedVars(template, ...args) {
const scope = this;
// 当前激活语言
const activeLanguage = scope.global.activeLanguage;
// 没有变量插值则的返回原字符串
if (args.length === 0 || !hasInterpolation(template)) return template;
module.exports ={
forEachInterpolatedVars,
getInterpolatedVars, // 获取指定字符串中的插件值变量列表
replaceInterpolatedVars, //
createFormatter,
Formatter,
}
// ****************************变量插值****************************
if (args.length === 1 && isPlainObject(args[0])) {
// 读取模板字符串中的插值变量列表
// [[var1,[formatter,formatter,...],match],[var2,[formatter,formatter,...],match],...}
let varValues = args[0];
return forEachInterpolatedVars(template,(varname, formatters, match) => {
let value = varname in varValues ? varValues[varname] : "";
return getFormattedValue(scope,activeLanguage,formatters,value,template);
}
);
} else {
// ****************************位置插值****************************
// 如果只有一个Array参数则认为是位置变量列表进行展开
const params =args.length === 1 && Array.isArray(args[0]) ? [...args[0]] : args;
if (params.length === 0) return template; // 没有变量则不需要进行插值处理,返回原字符串
let i = 0;
return forEachInterpolatedVars(template,(varname, formatters, match) => {
if (params.length > i) {
return getFormattedValue(scope,activeLanguage,formatters,params[i++],template);
} else {
throw new Error(); // 抛出异常,停止插值处理
}
},
{ replaceAll: false }
);
}
}
module.exports = {
forEachInterpolatedVars, // 遍历插值变量并替换
getInterpolatedVars, // 获取指定字符串中的插件值变量列表
replaceInterpolatedVars // 替换插值变量
};

View File

@ -1,6 +1,6 @@
{
"name": "@voerkai18n/runtime",
"version": "1.0.29",
"version": "1.1.2",
"description": "核心运行时",
"main": "./dist/index.cjs",
"module": "dist/index.esm.js",
@ -29,11 +29,12 @@
"@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-commonjs": "^21.0.2",
"@rollup/plugin-node-resolve": "^13.1.3",
"@voerkai18n/autopublish": "workspace:^1.0.2",
"deepmerge": "^4.2.2",
"jest": "^27.5.1",
"rollup": "^2.69.0",
"rollup-plugin-clear": "^2.0.7",
"rollup-plugin-terser": "^7.0.2",
"@voerkai18n/autopublish": "workspace:^1.0.2"
"rollup-plugin-terser": "^7.0.2"
},
"lastPublish": "2022-08-16T09:47:13+08:00"
"lastPublish": "2022-08-20T20:55:57+08:00"
}

View File

@ -58,31 +58,6 @@ function isNothing(value){
return false
}
// 区配JSON字符串里面的非标的key即key没有使用"字符包起来的键
const bastardJsonKeyRegex = /((?<=:\s*)(\'.*?\')+)|((([\w\u4e00-\u9fa5])|(\'.*?\'))+(?=\s*\:))/g
/**
* 格式化器中的{a:1,b:2}形式的参数由于是非标准的JSON格式采用JSON.parse会出错
* 如果使用eval转换则存在安全隐患
* 因此本函数采用正则表达式来匹配KEY然后为KEY自动添加""转换成标准JSON后再转换
* @param {*} s
*/
function safeParseJson(str){
let params = [];
let matched;
while ((matched = bastardJsonKeyRegex.exec(str)) !== null) {
if (matched.index === bastardJsonKeyRegex.lastIndex) {
bastardJsonKeyRegex.lastIndex++;
}
str = str.replace(new RegExp(matched[0]),key=>{
if(key.startsWith("'") && key.endsWith("'")){
key = key.substring(1,key.length-1)
}
return `"${key}"`
})
}
return JSON.parse(str)
}
/**
* 深度合并对象
*
@ -195,8 +170,8 @@ function toNumber(value,defualt=0) {
// 不足位数时补零
if(wholeDigits.length<radix*unit) wholeDigits = new Array(radix*unit-wholeDigits.length+1).fill(0).join("")+ wholeDigits
// 将整数的最后radix*unit字符移到小数部分前面
wholeDigits = wholeDigits.substring(0,wholeDigits.length-radix*unit)
decimalDigits=wholeDigits.substring(wholeDigits,wholeDigits.length-radix*unit)+decimalDigits
wholeDigits = wholeDigits.substring(0,wholeDigits.length-radix*unit)
if(wholeDigits=="") wholeDigits = "0"
}
@ -357,101 +332,62 @@ function replaceAll(str,findValue,replaceValue){
if(typeof(str)!=="string" || findValue=="" || findValue==replaceValue) return str
let result = str
try{
while(result.search(findValue)!=-1){
while(result.includes(findValue)){
result = result.replace(findValue,replaceValue)
}
}catch{}
return result
}
/**
* 创建格式化器
* 使用正则表达式解析非标JOSN
*
* 格式化器是一个普通的函数具有以下特点
*
* - 函数第一个参数是上一上格式化器的输出
* - 支持0-N个简单类型的入参
* - 可以是定参也可以变参
* - 格式化器可以在格式化器的$config参数指定一个键值来配置不同语言时的参数
*/
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没有使用""包裹
*
* "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 可选默认值
* @param {*} str
* @returns
*/
function createFormatter(fn,options={},defaultParams={}){
let opts = Object.assign({
normalize : null, // 对输入值进行规范化处理,如进行时间格式化时,为了提高更好的兼容性,支持数字时间戳/字符串/Date等需要对输入值进行处理如强制类型转换等
params : null, // 可选的声明参数顺序如果是变参的则需要传入null
configKey : null // 声明该格式化器在$config中的路径支持简单的使用.的路径语法
},options)
// 最后一个参数是传入activeFormatterConfig参数
const $formatter = function(value,...args){
let finalValue = value
// 1. 输入值规范处理,主要是进行类型转换,确保输入的数据类型及相关格式的正确性,提高数据容错性
if(isFunction(opts.normalize)){
try{
finalValue = opts.normalize(finalValue)
}catch{}
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)
}
// 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
}else{ // 具有固定的参数个数
finalArgs = opts.params.map(param=>getByPath(formatterConfig,param,undefined))
// 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(finalValue,...finalArgs,formatterConfig)
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)
}
$formatter.configurable = true // 当函数是可配置时才在最后一个参数中传入$config
return $formatter
return JSON.parse(str)
}
const Formatter = createFormatter
module.exports ={
isPlainObject,
isFunction,
@ -460,8 +396,6 @@ module.exports ={
deepClone,
deepMerge,
deepMixin,
Formatter,
createFormatter,
replaceAll,
getByPath,
getDataTypeName,
@ -470,6 +404,6 @@ module.exports ={
toDate,
toNumber,
toCurrency,
safeParseJson,
createFormatter
escapeRegexpStr,
safeParseJson
}

View File

@ -1,6 +1,6 @@
{
"name": "@voerkai18n/utils",
"version": "1.0.12",
"version": "1.0.13",
"description": "公共工具库",
"main": "index.js",
"scripts": {
@ -16,5 +16,5 @@
"devDependencies": {
"@voerkai18n/autopublish": "workspace:^1.0.2"
},
"lastPublish": "2022-08-05T16:45:29+08:00"
"lastPublish": "2022-08-20T20:54:07+08:00"
}

View File

@ -1,6 +1,6 @@
{
"name": "@voerkai18n/vite",
"version": "1.0.12",
"version": "1.0.13",
"description": "Vite插件,提供自动插入翻译函数和文本映射等功能",
"main": "index.js",
"scripts": {
@ -15,5 +15,5 @@
"devDependencies": {
"@voerkai18n/autopublish": "workspace:^1.0.2"
},
"lastPublish": "2022-08-05T16:46:10+08:00"
"lastPublish": "2022-08-20T20:54:41+08:00"
}

View File

@ -1,6 +1,6 @@
{
"name": "@voerkai18n/vue",
"version": "1.0.5",
"version": "1.0.6",
"description": "Vue3插件,提供自动插件翻译函数和语言切换功能",
"main": "index.js",
"scripts": {
@ -12,5 +12,5 @@
"devDependencies": {
"@voerkai18n/autopublish": "workspace:^1.0.2"
},
"lastPublish": "2022-04-15T17:45:10+08:00"
"lastPublish": "2022-08-20T20:54:52+08:00"
}

2
pnpm-lock.yaml generated
View File

@ -209,6 +209,7 @@ importers:
'@rollup/plugin-node-resolve': ^13.1.3
'@voerkai18n/autopublish': workspace:^1.0.2
deepmerge: ^4.2.2
jest: ^27.5.1
rollup: ^2.69.0
rollup-plugin-clear: ^2.0.7
rollup-plugin-terser: ^7.0.2
@ -223,6 +224,7 @@ importers:
'@rollup/plugin-node-resolve': 13.3.0_rollup@2.77.2
'@voerkai18n/autopublish': link:../autopublish
deepmerge: 4.2.2
jest: 27.5.1
rollup: 2.77.2
rollup-plugin-clear: 2.0.7
rollup-plugin-terser: 7.0.2_rollup@2.77.2

View File

@ -236,7 +236,9 @@ const zhMoneys = [
"商品价格: { value | currency({symbol:'¥¥',prefix:'人民币:'})}",
"商品价格: { value | currency({symbol:'¥¥',prefix:'人民币:',suffix:'元整'})}",
"商品价格: { value | currency({symbol:'¥¥',prefix:'人民币:',suffix:'元整',unit:2})}",
"商品价格: { value | currency({symbol:'¥¥',prefix:'人民币:',suffix:'元整',unit:2,precision:4})}"
"商品价格: { value | currency({symbol:'¥¥',prefix:'人民币:',suffix:'元整',unit:2,precision:4})}",
"商品价格: { value | currency({symbol:'¥¥',prefix:'人民币:',suffix:'元整',unit:2,precision:4,format:'{prefix}*{symbol}*{value}*{unit}*{suffix}'})}"
]
const expectZhMoneys =[
@ -254,11 +256,12 @@ const expectZhMoneys =[
"商品价格: ¥0.00012345678988万亿", // { value | currency('short',3)}
"商品价格: ¥0.000000012345678988万万亿", // { value | currency('short',4)}
// 自定义货币格式
"商品价格: ¥¥1,2345,6789.88",
"商品价格: 人民币:¥¥1,2345,6789.88",
"商品价格: 人民币:¥¥1,2345,6789.88元整",
"商品价格: 人民币:¥¥1,2345.678988万元",
"商品价格: 人民币:¥¥1.2346亿元整"
"商品价格: RMB ¥¥1,2345,6789.88元",
"商品价格: 人民币: ¥¥1,2345,6789.88元",
"商品价格: 人民币: ¥¥1,2345,6789.88元整",
"商品价格: 人民币: ¥¥1.2345678988亿元整",
"商品价格: 人民币: ¥¥1.2346+亿元整",
"商品价格: 人民币:*¥¥*1.2346+*亿*元整"
]
@ -367,11 +370,13 @@ scope.registerFormatters({
}
},true)
beforeEach(() => {
scope.change("zh")
});
describe("翻译函数",()=>{
beforeEach(() => {
scope.change("zh")
});
test("获取翻译内容中的插值变量",done=>{
const results = getInterpolatedVars("中华人民共和国成立于{date | year(1,2) | time('a','b') | rel }年,首都是{city}市");
expect(results.length).toEqual(2);
@ -531,10 +536,12 @@ test("日期时间格式化器",async ()=>{
})
test("货币格式化器",async ()=>{
test("货币格式化器",async ()=>{
let zhMoneysResults = zhMoneys.map(v=>t(v,MONEY))
expect(zhMoneysResults).toStrictEqual(expectZhMoneys)
await scope.change("en")
let enMoneysResults = enMoneys.map(v=>t(v,MONEY))
expect(enMoneysResults).toStrictEqual(expectEnMoneys)
})
})