更新格式化器机制

This commit is contained in:
wxzhang 2022-08-11 18:33:32 +08:00
parent baa7bc569c
commit ab9e150470
13 changed files with 477 additions and 279 deletions

View File

@ -1,9 +1,9 @@
import messageIds from "./idMap.js"
import runtime from "./runtime.js"
const { translate,i18nScope } = runtime
import messageIds from "./idMap.js" // 语言ID映射文件
import { translate,i18nScope } from "./runtime.js" // 运行时
import defaultFormatters from "./formatters/zh.js" // 默认语言格式化器
const activeFormatters = defaultFormatters // 激活语言格式化器
import formatters from "./formatters.js"
import defaultMessages from "./zh.js"
const activeMessages = defaultMessages
@ -18,24 +18,37 @@ const scopeSettings = {
{
"name": "en",
"title": "英文"
},
{
"name": "de",
"title": "德语"
}
],
"defaultLanguage": "zh",
"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: "vueapp", // 当前作用域的id自动取当前工程的package.json的name
default: defaultMessages, // 默认语言包
id : "vueapp", // 当前作用域的id自动取当前工程的package.json的name
default : defaultMessages, // 默认语言包
messages : activeMessages, // 当前语言包
idMap:messageIds, // 消息id映射列表
formatters, // 当前作用域的格式化函数列表
loaders:{
"en" : ()=>import("./en.js")
}
idMap : messageIds, // 消息id映射列表
formatters, // 扩展自定义格式化器
loaders // 语言包加载器
})
// 翻译函数
const scopedTtranslate = translate.bind(scope)

View File

@ -7,6 +7,10 @@
{
"name": "en",
"title": "英文"
},
{
"name": "de",
"title": "德语"
}
],
"defaultLanguage": "zh",

View File

@ -3,180 +3,210 @@
"en": "Hello world!",
"$file": [
"App.vue"
]
],
"de": "Hello world!"
},
"中华人民共和国": {
"en": "The People's Republic of China",
"$file": [
"App.vue"
]
],
"de": "中华人民共和国"
},
"迎接中华民族的伟大复兴": {
"en": "Welcome the great rejuvenation of the Chinese nation",
"$file": [
"App.vue"
]
],
"de": "迎接中华民族的伟大复兴"
},
"成立于{}年": {
"en": "Founded in {}",
"$file": [
"components\\china.vue"
]
],
"de": "成立于{}年"
},
"首都:北京": {
"en": "Capital: Beijing",
"$file": [
"components\\china.vue"
]
],
"de": "首都:北京"
},
"VoerkaI18n多语言解决方案 ": {
"en": "VoerkaI18n多语言解决方案 ",
"$file": [
"App.vue"
]
],
"de": "VoerkaI18n多语言解决方案 "
},
"现在是{ value | date }": {
"en": "Now is { value | date }",
"$file": [
"components\\formatters.vue"
]
],
"de": "现在是{ value | date }"
},
"现在是{ value | shortdate }": {
"en": "Now is { value | shortdate }",
"$file": [
"components\\formatters.vue"
]
],
"de": "现在是{ value | shortdate }"
},
"现在是{ value | time }": {
"en": "Now is { value | time }",
"$file": [
"components\\formatters.vue"
]
],
"de": "现在是{ value | time }"
},
"现在是{ value | shorttime }": {
"en": "Now is { value | shorttime }",
"$file": [
"components\\formatters.vue"
]
],
"de": "现在是{ value | shorttime }"
},
"现在是{ value | year }": {
"en": "Now is { value | year }",
"$file": [
"components\\formatters.vue"
]
],
"de": "现在是{ value | year }"
},
"现在是{ value | month }": {
"en": "Now is { value | month }",
"$file": [
"components\\formatters.vue"
]
],
"de": "现在是{ value | month }"
},
"现在是{ value | day }": {
"en": "Now is { value | day }",
"$file": [
"components\\formatters.vue"
]
],
"de": "现在是{ value | day }"
},
"现在是{ value | weekdayValue }": {
"en": "Now is { value | weekdayValue }",
"$file": [
"components\\formatters.vue"
]
],
"de": "现在是{ value | weekdayValue }"
},
"现在是{ value | weekday }": {
"en": "Now is { value | weekday }",
"$file": [
"components\\formatters.vue"
]
],
"de": "现在是{ value | weekday }"
},
"现在是{ value | shortWeekday }": {
"en": "Now is { value | shortWeekday }",
"$file": [
"components\\formatters.vue"
]
],
"de": "现在是{ value | shortWeekday }"
},
"现在是{ value | monthName }": {
"en": "Now is { value | monthName }",
"$file": [
"components\\formatters.vue"
]
],
"de": "现在是{ value | monthName }"
},
"现在是{ value | shorMonthName }": {
"en": "Now is { value | shorMonthName }",
"$file": [
"components\\formatters.vue"
]
],
"de": "现在是{ value | shorMonthName }"
},
"现在是{ value | hour }": {
"en": "Now is { value | hour }",
"$file": [
"components\\formatters.vue"
]
],
"de": "现在是{ value | hour }"
},
"现在是{ value | hour12 }": {
"en": "Now is { value | hour12 }",
"$file": [
"components\\formatters.vue"
]
],
"de": "现在是{ value | hour12 }"
},
"现在是{ value | minute }": {
"en": "Now is { value | minute }",
"$file": [
"components\\formatters.vue"
]
],
"de": "现在是{ value | minute }"
},
"现在是{ value | second }": {
"en": "Now is { value | second }",
"$file": [
"components\\formatters.vue"
]
],
"de": "现在是{ value | second }"
},
"现在是{ value | millisecond }": {
"en": "Now is { value | millisecond }",
"$file": [
"components\\formatters.vue"
]
],
"de": "现在是{ value | millisecond }"
},
"现在是{ value | timestamp }": {
"en": "Now is { value | timestamp }",
"$file": [
"components\\formatters.vue"
]
],
"de": "现在是{ value | timestamp }"
},
"商品价格:{ value | currency }": {
"en": "Price: { value | currency }",
"$file": [
"components\\formatters.vue"
]
],
"de": "商品价格:{ value | currency }"
},
"商品价格:{ value | capitalizeCurrency }": {
"en": "Price: { value | capitalizeCurrency }",
"$file": [
"components\\formatters.vue"
]
],
"de": "商品价格:{ value | capitalizeCurrency }"
},
"现在是{ value }": {
"en": "Now is { value }",
"$file": [
"components\\formatters.vue"
]
],
"de": "现在是{ value }"
},
"商品价格:{ value | currency('CNY','元整',3)}": {
"en": "Price: { value | currency('CNY','元整',3)}",
"$file": [
"components\\formatters.vue"
]
],
"de": "商品价格:{ value | currency('CNY','元整',3)}"
},
"商品价格:{ value | capitalizeCurrency(true) }": {
"en": "Price:{ value | capitalizeCurrency(true) }",
"$file": [
"components\\formatters.vue"
]
],
"de": "商品价格:{ value | capitalizeCurrency(true) }"
},
"商品数量:{ value | number }": {
"en": "Count: { value | number }",
"$file": [
"components\\formatters.vue"
]
],
"de": "商品数量:{ value | number }"
}
}

View File

@ -146,7 +146,29 @@ module.exports =async function compile(langFolder,opts={}){
}
// 5 . 生成编译后的格式化函数文件
const formattersFile = path.join(langFolder,"formatters.js")
// const formattersFile = path.join(langFolder,"formatters.js")
// if(!fs.existsSync(formattersFile)){
// const formattersContent = artTemplate(path.join(__dirname,"templates","formatters.js"), templateContext )
// fs.writeFileSync(formattersFile,formattersContent)
// logger.log(t(" - 格式化器:{}"),path.basename(formattersFile))
// }else{ // 格式化器如果存在,则需要更改对应的模块类型
// let formattersContent = fs.readFileSync(formattersFile,"utf8").toString()
// if(moduleType == "esm"){
// formattersContent = formattersContent.replaceAll(/^[^\n\r\w]*module.exports\s*\=/gm,"export default ")
// formattersContent = formattersContent.replaceAll(/^[^\n\r\w]*module.exports\./gm,"export ")
// }else{
// formattersContent = formattersContent.replaceAll(/^[^\n\r\w]*export\s*default\s*/gm,"module.exports = ")
// formattersContent = formattersContent.replaceAll(/^[^\n\r\w]*export\s*/gm,"module.exports.")
// }
// fs.writeFileSync(formattersFile,formattersContent)
// logger.log(t(" - 更新格式化器:{}"),path.basename(formattersFile))
// }
const formattersFolder = path.join(langFolder,"formatters")
if(!fs.existsSync(formattersFolder)) fs.mkdirSync(formattersFolder)
// 为每一个语言生成一个对应的式化器
languages.forEach(lang=>{
const formattersFile = path.join(formattersFolder,`${lang.name}.js`)
if(!fs.existsSync(formattersFile)){
const formattersContent = artTemplate(path.join(__dirname,"templates","formatters.js"), templateContext )
fs.writeFileSync(formattersFile,formattersContent)
@ -163,6 +185,8 @@ module.exports =async function compile(langFolder,opts={}){
fs.writeFileSync(formattersFile,formattersContent)
logger.log(t(" - 更新格式化器:{}"),path.basename(formattersFile))
}
})
// 6. 生成编译后的访问入口文件
const entryFile = path.join(langFolder,"index.js")

View File

@ -1,35 +1,48 @@
{{if moduleType === "esm"}}
import messageIds from "./idMap.js"
{{if inlineRuntime }}import runtime from "./runtime.js"
const { translate,i18nScope } = runtime
{{else}}import { translate,i18nScope } from "@voerkai18n/runtime"{{/if}}
import formatters from "./formatters.js"
import messageIds from "./idMap.js" // 语言ID映射文件
{{if inlineRuntime }}import { translate,i18nScope } from "./runtime.js" // 运行时
import defaultFormatters from "./formatters/{{defaultLanguage}}.js" // 默认语言格式化器
{{if defaultLanguage === activeLanguage}}const activeFormatters = defaultFormatters{{else}}import activeFormatters from "./formatters/{{activeLanguage}}.js"{{/if}} // 激活语言格式化器
{{else}}import { translate,i18nScope } from "@voerkai18n/runtime"
{{if defaultLanguage === activeLanguage}}const activeFormatters = defaultFormatters{{else}}import activeFormatters from "@voerkai18n/runtime/formatters/{{activeLanguage}}.js"{{/if}}
{{/if}}
import defaultMessages from "./{{defaultLanguage}}.js"
{{if defaultLanguage === activeLanguage}}const activeMessages = defaultMessages
{{else}}import activeMessages from "./{{activeLanguage}}.js"{{/if}}
{{if defaultLanguage === activeLanguage}}const activeMessages = defaultMessages{{else}}import activeMessages from "./{{activeLanguage}}.js"{{/if}}
{{else}}
const messageIds = require("./idMap")
{{if inlineRuntime }}const { translate,i18nScope } = require("./runtime.js")
{{else}}const { translate,i18nScope } = require("@voerkai18n/runtime"){{/if}}
const formatters = require("./formatters.js")
const defaultMessages = require("./{{defaultLanguage}}.js")
{{if defaultLanguage === activeLanguage}}const activeMessages = defaultMessages
{{else}}const activeMessages = require("./{{activeLanguage}}.js"){{/if}}
const defaultFormatters = require("./formatters/{{defaultLanguage}}.js")
{{if defaultLanguage === activeLanguage}}const activeFormatters = defaultFormatters{{else}}const activeFormatters = require("./formatters/{{activeLanguage}}.js"){{/if}}
{{else}}const { translate,i18nScope } = require("@voerkai18n/runtime")
const defaultFormatters = require("@voerkai18n/runtime/formatters/{{defaultLanguage}}.js")
{{if defaultLanguage === activeLanguage}}const activeFormatters = defaultFormatters{{else}}const activeFormatters = require("@voerkai18n/runtime/formatters/{{activeLanguage}}.js"){{/if}}
{{/if}}
const defaultMessages = require("./{{defaultLanguage}}.js") // 默认语言包
{{if defaultLanguage === activeLanguage}}const activeMessages = defaultMessages{{else}}const activeMessages = require("./{{activeLanguage}}.js"){{/if}} {{/if}}
// 语言配置文件
const scopeSettings = {{@ settings}}
// 格式化器
const formatters = {
{{each languages}}{{if $value.name == defaultLanguage}}'{{defaultLanguage}}' : defaultFormatters{{if $index !== languages.length - 1}},{{/if}}
{{else if $value.name == activeLanguage}}{{if defaultLanguage !== activeLanguage}}'{{activeLanguage}}':activeFormatters{{/if}}{{if $index !== languages.length - 1}},{{/if}}
{{else}}'{{$value.name}}' : ()=>import("./formatters/{{$value.name}}.js"){{if $index !== languages.length - 1}},{{'\n\t'}}{{/if}}{{/if}}{{/each}}
}
// 语言包加载器
const loaders = { {{each languages}}{{if $value.name !== defaultLanguage}}
{{if $value.name == activeLanguage}}"{{$value.name}}" : activeMessages{{else}}"{{$value.name}}" : ()=>import("./{{$value.name}}.js"){{/if}}{{if $index !== languages.length - 1}},{{/if}}{{/if}}{{/each}}
}
// 语言作用域
const scope = new i18nScope({
...scopeSettings, // languages,defaultLanguage,activeLanguage,namespaces,formatters
id: "{{scopeId}}", // 当前作用域的id自动取当前工程的package.json的name
default: defaultMessages, // 默认语言包
id : "{{scopeId}}", // 当前作用域的id自动取当前工程的package.json的name
debug : false, // 是否在控制台输出高度信息
default : defaultMessages, // 默认语言包
messages : activeMessages, // 当前语言包
idMap:messageIds, // 消息id映射列表
formatters, // 当前作用域的格式化函数列表
loaders:{ {{each languages}}{{if $value.name !== defaultLanguage}}
{{if $value.name == activeLanguage}}"{{$value.name}}" : ()=>activeMessages{{else}}"{{$value.name}}" : ()=>import("./{{$value.name}}.js"){{/if}}{{if $index !== languages.length - 1}},{{/if}}{{/if}}{{/each}}
}
idMap : messageIds, // 消息id映射列表
formatters, // 扩展自定义格式化器
loaders // 语言包加载器
})
// 翻译函数
const scopedTtranslate = translate.bind(scope)

View File

@ -1,17 +1,13 @@
/**
格式化器用来对翻译文本内容中的插值变量进行格式化
格式化器用来对翻译文本内容中的插值变量进行处理
比如将一个数字格式化为货币格式或者将一个日期格式化为友好的日期格式
- 以下定义了一些格式化器在中文场景下会启用这些格式化器
import dayjs from "dayjs";
const formatters = {
"*":{ // 在所有语言下生效的格式化器
$types:{...}, // 只作用于特定数据类型的默认格式化器
.... // 全局格式化器
},
zh:{
// 只作用于特定数据类型的格式化器
export default {
$options:{...},
$types:{
Date:(value)=>dayjs(value).format("YYYY年MM月DD日 HH:mm:ss"),
},
@ -20,16 +16,6 @@
[格式化器名称]:(value)=>{...},
[格式化器名称]:(value)=>{...},
[格式化器名称]:(value)=>{...},
},
en:{
$types:{
Date:(value)=>dayjs(value).format("YYYY/MM/DD HH:mm:ss"), // 默认的格式化器
},
date:(value)=>dayjs(value).format("YYYY/MM/DD")
bjTime:(value)=>"BeiJing "+ value,
[格式化器名称]:(value)=>{...},
[格式化器名称]:(value)=>{...},
[格式化器名称]:(value)=>{...},
}
}
- 在翻译函数中使用格式化器的方法,示例如下
@ -38,48 +24,47 @@
其等效于
t(`Now is ${bjTime(date(value))",{value: new Date()})
由于value分别经过两个管道符转换上一个管道符的输出作为下一个管道符的输入,可以多次使用管道符
最终的输出结果
中文: "现在是北京时间2022年3月1日"
英文: "Now is BeiJing 2022/03/01"
*
*/
{{if moduleType === "esm"}}
export default{{else}}module.exports = {{/if}}{
// 在所有语言下生效的格式化器
"*":{
//[格式化名称]:(value)=>{...},
//[格式化名称]:(value,arg)=>{...},
},
// 在所有语言下只作用于特定数据类型的格式化器
$types:{
// 格式化器参数
$options:{
},
{{each languages}} {{$value.name}}:{
// 指定数据类型的默认格式化器
$types:{
// 所有类型的默认格式化器
// "*":{
// },
// Date:{
// },
// Number:{
// },
// String:{
// },
// Array:{
// },
// Object:{
// }
// "*" : { },
// Date : { },
// Number: { },
// String: { },
// Array : { },
// Object: { }
}
}{{if $index !== languages.length - 1}},{{/if}}
{{/each}}}
// 允许重载内置的格式化器
// --- 日期 ------
// 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

@ -5,6 +5,7 @@
*/
const { isNumber } = require('./utils')
const CN_DATETIME_UNITS = ["年","季度","月","周","日","小时","分钟","秒","毫秒","微秒"]
const CN_WEEK_DAYS = ["星期日","星期一","星期二","星期三","星期四","星期五","星期六"]
const CN_SHORT_WEEK_DAYS =["日","一","二","三","四","五","六"]
const CN_MONTH_NAMES= ["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"]
@ -93,6 +94,7 @@ function toChineseCurrency(value,{big=false,prefix="",unit="元",suffix=""}={}){
toChineseCurrency,
toChineseNumber,
toChineseBigNumber,
CN_DATETIME_UNITS,
CN_WEEK_DAYS,
CN_SHORT_WEEK_DAYS,
CN_MONTH_NAMES,

View File

@ -6,12 +6,35 @@
const { toDate,toCurrency } = require("../utils")
module.exports = {
// 配置参数: 格式化器函数的最后一个参数就是该配置参数
$options:{
datetime : {
units : ["Year","Quarter","Month","Week","Day","Hour","Minute","Second","Millisecond","Microsecond"],
weekday : ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
shortWeekdays : ["Sun", "Mon", "Tues", "Wed", "Thur", "Fri", "Sat"],
monthNames : ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
shorMonthNames: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"]
},
currency : {
unit : "$",
prefix : "",
suffix : "",
division : 3,
precision : 2
},
number : {
division : 3,
precision : 2
}
},
// 默认数据类型的格式化器
$types: {
Date : value => { const d = toDate(value); return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()} ${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}` },
Null : value =>"",
Undefined: value =>"",
Error : value => "ERROR"
},
// 以下是格式化定义
// 日期
date : value => { const d = toDate(value); return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}` },
shortdate : value => { const d = toDate(value); return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}` },

View File

@ -1,5 +1,6 @@
/**
* 内置的格式化器
* 被注册到全局语言管理器
*/
const enFormatters = require("./en")

View File

@ -3,11 +3,31 @@
*
*/
const { toChineseCurrency,toChineseNumber,CN_WEEK_DAYS,CN_SHORT_WEEK_DAYS, CN_MONTH_NAMES, CN_SHORT_MONTH_NAMES} = require("../cnutils")
const { toChineseCurrency,toChineseNumber,CN_DATETIME_UNITS,CN_WEEK_DAYS,CN_SHORT_WEEK_DAYS, CN_MONTH_NAMES, CN_SHORT_MONTH_NAMES} = require("../cnutils")
const { toDate, toCurrency } = require("../utils")
module.exports = {// 简体中文
module.exports = {
// 配置参数: 格式化器函数的最后一个参数就是该配置参数
$options:{
datetime : {
units : CN_DATETIME_UNITS,
weekdays : CN_WEEK_DAYS,
shortWeekdays : CN_SHORT_WEEK_DAYS,
monthNames : CN_MONTH_NAMES,
shorMonthNames: CN_SHORT_MONTH_NAMES
},
currency : {
unit : "$",
prefix : "",
suffix : "",
division : 3,
precision : 2
},
number : {
division : 3,
precision : 2
}
},
$types: {
Date: value => {const d = toDate(value);return `${d.getFullYear()}${d.getMonth() + 1}${d.getDate()}${d.getHours()}${d.getMinutes()}${d.getSeconds()}`}
},

View File

@ -1,7 +1,7 @@
const { getDataTypeName,isNumber,isPlainObject,deepMerge } = require("./utils")
const EventEmitter = require("./eventemitter")
const i18nScope = require("./scope.js")
let inlineFormatters = require("./formatters") // 内置格式化器
let inlineFormatters = require("./formatters")
@ -34,28 +34,31 @@ const DataTypes = ["String","Number","Boolean","Object","Array","Function","Err
/**
通过正则表达式对原始文本内容进行解析匹配后得到的
使用正则表达式对原始文本内容进行解析匹配后得到的便以处理的数组
formatters="| aaa(1,1) | bbb "
需要统一解析为
统一解析为
[
[aaa,[1,1]], // [formatter'name,[args,...]]
[bbb,[]],
[<格式化器名称>,[<参数>,<参数>,...]]
]
formatters="| aaa(1,1,"dddd") | bbb "
目前对参数采用简单的split(",")来解析因此如果参数中包括了,则无法正确解析例如aaa(1,1,"dd,,dd")形式的参数
特别注意
- 目前对参数采用简单的split(",")来解析因此如果参数中包括了逗号等会影响解析的字符时可能导致错误
例如aaa(1,1,"dd,,dd")形式的参数
在此场景下基本够用了如果需要支持更复杂的参数解析可以后续考虑使用正则表达式来解析
- 如果参数是{},[]则尝试解决为对象和数组但是里面的内容无法支持复杂和嵌套数据类型
@returns [[<formatterName>,[<arg>,<arg>,...]]]
@returns [ [<格式化器名称>,[<参数>,<参数>,...],[<格式化器名称>,[<参数>,<参数>,...]],...]
*/
function parseFormatters(formatters){
if(!formatters) return []
// 1. 先解析为 ["aaa()","bbb"]形式
let result = formatters.trim().substr(1).trim().split("|").map(r=>r.trim())
// 2. 解析格式化器参数
return result.map(formatter=>{
let firstIndex = formatter.indexOf("(")
@ -64,11 +67,11 @@ function parseFormatters(formatters){
const argsContent = formatter.substr(firstIndex+1,lastIndex-firstIndex-1).trim()
let args = argsContent=="" ? [] : argsContent.split(",").map(arg=>{
arg = arg.trim()
if(!isNaN(parseInt(arg))){
return parseInt(arg) // 数字
if(isNumber(arg)){ // 数字
return parseFloat(arg)
}else if((arg.startsWith('\"') && arg.endsWith('\"')) || (arg.startsWith('\'') && arg.endsWith('\'')) ){
return arg.substr(1,arg.length-2) // 字符串
}else if(arg.toLowerCase()==="true" || arg.toLowerCase()==="false"){
}else if(["true" ,"false"].includes(arg.toLowerCase())){
return arg.toLowerCase()==="true" // 布尔值
}else if((arg.startsWith('{') && arg.endsWith('}')) || (arg.startsWith('[') && arg.endsWith(']'))){
try{
@ -89,7 +92,7 @@ function parseFormatters(formatters){
/**
* 提取字符串中的插值变量
* // [
* [
// {
name:<变量名称>,formatters:[{name:<格式化器名称>,args:[<参数>,<参数>,....]]],<匹配字符串>],
// ....
@ -145,12 +148,12 @@ function forEachInterpolatedVars(str,callback,options={}){
const varname = match.groups.varname || ""
// 解析格式化器和参数 = [<formatterName>,[<formatterName>,[<arg>,<arg>,...]]]
const formatters = parseFormatters(match.groups.formatters)
if(typeof(callback)==="function"){
if(isFunction(callback)){
try{
if(opts.replaceAll){
if(opts.replaceAll){ // 在某此版本上可能没有
result=result.replaceAll(match[0],callback(varname,formatters,match[0]))
}else{
result=result.replace(match[0],callback(varname,formatters,match[0]))
result=result.replace(new RegExp(match[0],"gm"),callback(varname,formatters,match[0]))
}
}catch{// callback函数可能会抛出异常如果抛出异常则中断匹配过程
break
@ -161,7 +164,8 @@ function forEachInterpolatedVars(str,callback,options={}){
return result
}
/**
* 将要翻译内容提供了一个非文本内容时进行默认的转换
* 当传入的翻译内容不是一个字符串时进行默认的转换
*
* - 对函数则执行并取返回结果()
* - 对Array和Object使用JSON.stringify
* - 其他类型使用toString
@ -171,7 +175,8 @@ function forEachInterpolatedVars(str,callback,options={}){
*/
function transformToString(value){
let result = value
if(typeof(result)==="function") result = value()
try{
if(isFunction(result)) result = value()
if(!(typeof(result)==="string")){
if(Array.isArray(result) || isPlainObject(result)){
result = JSON.stringify(result)
@ -179,9 +184,16 @@ function transformToString(value){
result = result.toString()
}
}
}catch{
result = result.toString()
}
return result
}
/**
* 清空指定语言的缓存
* @param {*} scope
* @param {*} activeLanguage
*/
function resetScopeCache(scope,activeLanguage=null){
scope.$cache = {activeLanguage,typedFormatters:{},formatters:{}}
}
@ -190,25 +202,19 @@ function resetScopeCache(scope,activeLanguage=null){
*
* 可以为每一个数据类型指定一个默认的格式化器,当传入插值变量时
* 会自动调用该格式化器来对值进行格式化转换
*
*
*
const formatters = {
"*":{
$types:{...} // 在所有语言下只作用于特定数据类型的格式化器
}, // 在所有语言下生效的格式化器
zh:{
$types:{
[数据类型]:{
default:(value)=>{...} // 默认
},
[数据类型]:(value)=>{...} // 默认
},
[格式化器名称]:(value)=>{...},
[格式化器名称]:(value)=>{...},
[格式化器名称]:(value)=>{...},
},
en:{.....}
}
* @param {*} scope
* @param {*} activeLanguage
@ -216,6 +222,7 @@ function resetScopeCache(scope,activeLanguage=null){
* @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]
@ -229,20 +236,21 @@ function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){
// 1. 在全局$types中查找
if(("*" in target) && isPlainObject(target["*"].$types)){
let formatters = target["*"].$types
if(dataType in formatters && typeof(formatters[dataType])==="function"){
if(dataType in formatters && isFunction(formatters[dataType])){
return scope.$cache.typedFormatters[dataType] = formatters[dataType]
}
}
// 2. 当前语言的$types中查找
if((activeLanguage in target) && isPlainObject(target[activeLanguage].$types)){
let formatters = target[activeLanguage].$types
if(dataType in formatters && typeof(formatters[dataType])==="function"){
if(dataType in formatters && isFunction(formatters[dataType])){
return scope.$cache.typedFormatters[dataType] = formatters[dataType]
}
}
}
}
/**
* 获取指定名称的格式化器函数
*
@ -254,7 +262,7 @@ function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){
* 全局作用域的格式化器优先
*
* @param {*} scope
* @param {*} activeLanguage
* @param {*} activeLanguage 当前激活语言名称
* @param {*} name 格式化器名称
* @returns {Function} 格式化函数
*/
@ -272,11 +280,18 @@ function getFormatter(scope,activeLanguage,name){
// 1. 优先在当前语言查找
if(activeLanguage in target){
let formatters = target[activeLanguage] || {}
if((name in formatters) && typeof(formatters[name])==="function") return scope.$cache.formatters[name] = formatters[name]
if((name in formatters) && isFunction(formatters[name])) {
return scope.$cache.formatters[name] = formatters[name]
}else{ // 如果语言指定了fallback,则在其回退语言中查找
let fallbackLangName = scope.getFallbackLanguage(activeLanguage)
if((fallbackLangName in formatters) && isFunction(formatters[fallbackLangName])) {
return scope.$cache.formatters[name] = formatters[fallbackLangName]
}
}
}
// 2. 全局作用域中查找
let formatters = target["*"] || {}
if((name in formatters) && typeof(formatters[name])==="function") return scope.$cache.formatters[name] = formatters[name]
if((name in formatters) && isFunction(formatters[name])) return scope.$cache.formatters[name] = formatters[name]
}
}
@ -290,7 +305,7 @@ function executeFormatter(value,formatters,scope){
let result = value
try{
for(let formatter of formatters){
if(typeof(formatter) === "function") {
if(isFunction(formatter)) {
result = formatter.call(scope,result)
}else{// 如果碰到无效的格式化器,则跳过过续的格式化器
return result
@ -320,7 +335,7 @@ function buildFormatters(scope,activeLanguage,formatters){
for(let formatter of formatters){
if(formatter[0]){
const func = getFormatter(scope,activeLanguage,formatter[0])
if(typeof(func)==="function"){
if(isFunction(func)){
results.push((v)=>{
return func(v,...formatter[1])
})
@ -328,7 +343,7 @@ function buildFormatters(scope,activeLanguage,formatters){
// 格式化器无效或者没有定义时,查看当前值是否具有同名的原型方法,如果有则执行调用
// 比如padStart格式化器是String的原型方法不需要配置就可以直接作为格式化器调用
results.push((v)=>{
if(typeof(v[formatter[0]])==="function"){
if(isFunction(v[formatter[0]])){
return v[formatter[0]].call(v,...formatter[1])
}else{
return v
@ -417,23 +432,23 @@ function replaceInterpolatedVars(template,...args) {
// 默认语言配置
const defaultLanguageSettings = {
debug : true,
defaultLanguage: "zh",
activeLanguage: "zh",
languages:[
activeLanguage : "zh",
formatters : inlineFormatters,
languages : [
{name:"zh",title:"中文",default:true},
{name:"en",title:"英文"}
],
formatters:inlineFormatters,
datetime:{
},
currency:{
}
]
}
/**
* 文本id必须是一个数字
* @param {*} content
* @returns
*/
function isMessageId(content){
return parseInt(content)>0
return isNumber(content)
}
/**
* 根据值的单数和复数形式从messages中取得相应的消息
@ -477,7 +492,7 @@ function translate(message) {
// 1. 预处理变量: 复数变量保存至pluralVars中 , 变量如果是Function则调用
if(arguments.length === 2 && isPlainObject(arguments[1])){
Object.entries(arguments[1]).forEach(([name,value])=>{
if(typeof(value)==="function"){
if(isFunction(value)){
try{
vars[name] = value()
}catch(e){
@ -491,7 +506,7 @@ function translate(message) {
}else if(arguments.length >= 2){
vars = [...arguments].splice(1).map((arg,index)=>{
try{
arg = typeof(arg)==="function" ? arg() : arg
arg = isFunction(arg) ? arg() : arg
// 位置参数中以第一个数值变量为复数变量
if(isNumber(arg)) pluraValue = parseInt(arg)
}catch(e){ }
@ -580,7 +595,7 @@ function translate(message) {
get defaultMessageLoader(){ return this._defaultMessageLoader}
// 通过默认加载器加载文件
async loadMessagesFromDefaultLoader(newLanguage,scope){
if(typeof(this._defaultMessageLoader) != "function") return //throw new Error("No default message loader specified")
if(!isFunction(this._defaultMessageLoader)) return //throw new Error("No default message loader specified")
return await this._defaultMessageLoader.call(scope,newLanguage,scope)
}
/**
@ -588,7 +603,7 @@ function translate(message) {
*/
async change(value){
value=value.trim()
if(this.languages.findIndex(lang=>lang.name === value)!==-1 || typeof(this._defaultMessageLoader)==="function"){
if(this.languages.findIndex(lang=>lang.name === value)!==-1 || isFunction(this._defaultMessageLoader)){
// 通知所有作用域刷新到对应的语言包
await this._refreshScopes(value)
this._settings.activeLanguage = value
@ -636,29 +651,36 @@ function translate(message) {
* 注册全局格式化器
* 格式化器是一个简单的同步函数value=>{...}用来对输入进行格式化后返回结果
*
* registerFormatters(name,value=>{...}) // 适用于所有语言
* registerFormatters(name,value=>{...},{langauge:"zh"}) // 适用于cn语言
* registerFormatters(name,value=>{...},{langauge:"en"}) // 适用于en语言
* registerFormatter(name,value=>{...}) // 注册到所有语言
* registerFormatter(name,value=>{...},{langauge:"zh"}) // 注册到zh语言
* registerFormatter(name,value=>{...},{langauge:"en"}) // 注册到en语言
registerFormatter("Date",value=>{...},{langauge:"en"}) // 注册到en语言的默认数据类型格式化器
registerFormatter(name,value=>{...},{langauge:["zh","cht"]}) // 注册到zh和cht语言
registerFormatter(name,value=>{...},{langauge:"zh,cht"})
* @param {*} formatters
* @param {*} formatter
language : 声明该格式化器适用语言
isGlobal : 注册到全局
*/
registerFormatter(name,formatter,{language="*",isGlobal}={}){
if(!typeof(formatter)==="function" || typeof(name)!=="string"){
registerFormatter(name,formatter,{language="*"}={}){
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[language].$types[name] = formatter
this.formatters[lng].$types[name] = formatter
}else{
this.formatters[language][name] = formatter
this.formatters[lng][name] = formatter
}
})
}
/**
* 注册默认文本信息加载器
*/
registerDefaultLoader(fn){
if(typeof(fn) !== 'function') throw new Error("The default loader must be a async function or promise returned")
if(!isFunction(fn)) throw new Error("The default loader must be a async function or promise returned")
this._defaultMessageLoader = fn
this.refresh()
}
@ -673,27 +695,6 @@ function translate(message) {
}catch{}
}
}
/**
* 创建格式化器
* @param {*} fn
* @param {*} options = {
* erorr:(e)=>{...} //执行出错时返回值
* empty:()=>{...} // 当空值时的返回值 *
* }
*
*/
function createFormatter(fn,options){
if(isPlainObject(options)) fn._options = options
return fn.bind(fn._options)
}
/**
* 扩展格式化器
* @param {*} fn
*/
function extendFormatter(fn){
}
module.exports ={

View File

@ -1,38 +1,40 @@
const { isPlainObject } = require("./utils")
const { isPlainObject,isFunction } = require("./utils")
const DataTypes = ["String","Number","Boolean","Object","Array","Function","Null","Undefined","Symbol","Date","RegExp","Error"];
module.exports = class i18nScope {
constructor(options={},callback){
// 每个作用域都有一个唯一的id
this._id = options.id || (new Date().getTime().toString()+parseInt(Math.random()*1000))
this._debug = 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 // 当前作用域的格式化函数列表
this._formatters = options.formatters // 当前作用域的格式化函数列表{<lang>:{$types,$options,[格式化器名称]:()=>{},[格式化器名称]:()=>{}}}
this._loaders = options.loaders // 异步加载语言文件的函数列表
this._global = null // 引用全局VoerkaI18n配置注册后自动引用
this._activeFormatters= options.formatters[options.activeLanguage] // 激活使用的格式化器,查找格式化器时在此查找
this._patchMessages = {} // 语言包补丁信息 {<language>:{....},<language>:{....}}
// 主要用来缓存格式化器的引用,当使用格式化器时可以直接引用,避免检索
// 用来缓存格式化器的引用,当使用格式化器时可以直接引用,减少检索遍历
this.$cache={
activeLanguage : null,
typedFormatters: {},
formatters : {},
}
// 如果不存在全局VoerkaI18n实例说明当前Scope是唯一或第一个加载的作用域
// 则使用当前作用域来初始化全局VoerkaI18n实例
// 如果不存在全局VoerkaI18n实例说明当前Scope是唯一或第一个加载的作用域则自动创建全局VoerkaI18n实例
if(!globalThis.VoerkaI18n){
const { I18nManager } = require("./")
globalThis.VoerkaI18n = new I18nManager({
debug : this._debug,
defaultLanguage: this.defaultLanguage,
activeLanguage : this.activeLanguage,
languages: options.languages,
languages : options.languages,
})
}
this._global = globalThis.VoerkaI18n
// 合并补丁语言包
this._mergePatchedMessages()
this._patch(this._messages,this.activeLanguage)
// 正在加载语言包标识
@ -42,6 +44,8 @@ module.exports = class i18nScope {
}
// 作用域
get id(){return this._id}
// 调试开关
get debug(){return this._debug}
// 默认语言名称
get defaultLanguage(){return this._defaultLanguage}
// 默认语言名称
@ -52,7 +56,7 @@ module.exports = class i18nScope {
get messages(){return this._messages}
// 消息id映射列表
get idMap(){return this._idMap}
// 当前作用域的格式化函数列表
// 当前作用域的格式化器 {<lang>:{$types,$options,[格式化器名称]:()=>{},[格式化器名称]:()=>{}}}
get formatters(){return this._formatters}
// 当前作用域支持的语言列表[{name,title,fallback}]
get languages(){return this._languages}
@ -65,33 +69,39 @@ module.exports = class i18nScope {
* @param {*} callback 注册成功后的回调
*/
register(callback){
if(!typeof(callback)==="function") callback = ()=>{}
if(!isFunction(callback)) callback = ()=>{}
this.global.register(this).then(callback).catch(callback)
}
/**
* 注册格式化器
*
* 格式化器是一个简单的同步函数value=>{...}用来对输入进行格式化后返回结果
*
* registerFormatters(name,value=>{...}) // 适用于所有语言
* registerFormatters(name,value=>{...},{langauge:"zh"}) // 适用于cn语言
* registerFormatters(name,value=>{...},{langauge:"en"}) // 适用于en语言
* @param {*} formatters
* registerFormatter(name,value=>{...}) // 注册到所有语言
* registerFormatter(name,value=>{...},{langauge:"zh"}) // 注册到zh语言
* registerFormatter(name,value=>{...},{langauge:"en"}) // 注册到en语言
registerFormatter("Date",value=>{...},{langauge:"en"}) // 注册到en语言的默认数据类型格式化器
registerFormatter(name,value=>{...},{langauge:["zh","cht"]}) // 注册到zh和cht语言
registerFormatter(name,value=>{...},{langauge:"zh,cht"})
* @param {*} formatter 格式化器
language : 声明该格式化器适用语言
isGlobal : 注册到全局
asGlobal : 注册到全局
*/
registerFormatter(name,formatter,{language="*",isGlobal}={}){
if(!typeof(formatter)==="function" || typeof(name)!=="string"){
registerFormatter(name,formatter,{language="*",asGlobal}={}){
if(!isFunction(formatter) || typeof(name)!=="string"){
throw new TypeError("Formatter must be a function")
}
if(isGlobal){
language = Array.isArray(language) ? language : (language ? language.split(",") : [])
if(asGlobal){
this.global.registerFormatter(name,formatter,{language})
}else{
language.forEach(lng=>{
if(DataTypes.includes(name)){
this.formatters[language].$types[name] = formatter
this._formatters[lng].$types[name] = formatter
}else{
this.formatters[language][name] = formatter
this._formatters[lng][name] = formatter
}
})
}
}
/**
@ -101,17 +111,76 @@ module.exports = class i18nScope {
registerDefaultLoader(fn){
this.global.registerDefaultLoader(fn)
}
_getLanguage(lang){
let index = this._languages.findIndex(lng=>lng.name==lang)
/**
* 获取指定语言信息
* @param {*} language
* @returns
*/
_getLanguage(language){
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
}
/**
* 获取指定语言的回退语言名称
*
* 如果没有指定则总是回退到到默认语言
*
* 但是不支持回退链
*
* 可以在配置中指定回退语言
* // settings.json
* {
* languages:[
* {name:"zh"},
* {name:"cht",fallback:"zh"}, //繁体中文可以回退到简体中文,
* {name:"en"},
* ]
* }
*
* @param {*} language
* @returns {Object} 语言信息数据 {name,title,fallback,...}
*/
getFallbackLanguage(language){
let lang = this._getLanguage(language)
if(lang){
return this.hasLanguage(lang.fallback) ? lang.fallback : this.defaultLanguage
}
}
/**
* 回退到默认语言
*/
_fallback(newLanguage){
_fallback(){
this._messages = this._default
this._activeLanguage = this.defaultLanguage
}
/**
* 当切换语言时格式化器应该切换到对应语言的格式化器
* @param {*} language
*/
async _loadFormatters(newLanguage){
try{
if(newLanguage in this._formatters){
let loader = this._formatters[newLanguage]
if(isPlainObject(loader)){
this._activeFormatters = loader
}else if(isFunction(loader)){
this._activeFormatters = (await loader()).default
}
}else{
if(this._debug) console.warn(`Not configured <${newLanguage}> formatters.`)
}
}catch(e){
if(this._debug) console.error(`Error loading ${newLanguage} formatters: ${e.message}`)
}
}
/**
* 刷新当前语言包
* @param {*} newLanguage
@ -119,29 +188,35 @@ module.exports = class i18nScope {
async refresh(newLanguage){
this._refreshing = true
if(!newLanguage) newLanguage = this.activeLanguage
// 默认语言,默认语言采用静态加载方式,只需要简单的替换即可
// 默认语言:由于默认语言采用静态加载方式而不是异步块,因此只需要简单的替换即可
if(newLanguage === this.defaultLanguage){
this._messages = this._default
await this._patch(this._messages,newLanguage) // 异步补丁
this._loadFormatters(newLanguage)
return
}
// 非默认语言需要异步加载语言包文件,加载器是一个异步函数
// 如果没有加载器,则无法加载语言包,因此回退到默认语言
let loader = this.loaders[newLanguage]
try{
if(typeof(loader) === "function"){
if(isPlainObject(loader)){
this._messages = loader
await this._patch(this._messages,newLanguage)
}else if(isFunction(loader)){
this._messages = (await loader()).default
this._activeLanguage = newLanguage
await this._patch(this._messages,newLanguage)
}else if(typeof(this.global.defaultMessageLoader) === "function"){// 如果该语言没有指定加载器,则使用全局配置的默认加载器
}else if(isFunction(this.global.defaultMessageLoader)){// 如果该语言没有指定加载器,则使用全局配置的默认加载器
const loadedMessages = await this.global.loadMessagesFromDefaultLoader(newLanguage,this)
this._messages = Object.assign({},this._default,loadedMessages)
this._activeLanguage = newLanguage
}else{
this._fallback()
}
// 应该切换到对应语言的格式化器
this._loadFormatters(newLanguage)
}catch(e){
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
@ -157,14 +232,16 @@ module.exports = class i18nScope {
* @returns
*/
async _patch(messages,newLanguage){
if(typeof(this.global.loadMessagesFromDefaultLoader) !== 'function') return
if(!isFunction(this.global.loadMessagesFromDefaultLoader)) return
try{
let pachedMessages = await this.global.loadMessagesFromDefaultLoader(newLanguage,this)
if(isPlainObject(pachedMessages)){
Object.assign(messages,pachedMessages)
this._savePatchedMessages(pachedMessages,newLanguage)
}
}catch{}
}catch(e){
if(this._debug) console.error(`Error while loading <${newLanguage}> messages from remote:${error.message}`)
}
}
/**
* 从本地存储中读取语言包补丁合并到当前语言包中
@ -190,7 +267,6 @@ module.exports = class i18nScope {
* 因此采用的方式是
* - 加载语言包补丁后将之保存到到本地的LocalStorage中
* - 当应用加载时会查询是否存在补丁如果存在就会合并渲染
* -
*
* @param {*} messages
*/
@ -200,9 +276,14 @@ module.exports = class i18nScope {
globalThis.localStorage.setItem(`voerkai18n_${this.id}_${language}_patched_messages`, JSON.stringify(messages));
}
}catch(e){
console.error("Error while save voerkai18n patched messages:",e.message)
if(this.$cache._debug) console.error("Error while save voerkai18n patched messages:",e.message)
}
}
/**
* 从本地缓存中读取补丁语言包
* @param {*} language
* @returns
*/
_getPatchedMessages(language){
try{
return JSON.parse(localStorage.getItem(`voerkai18n_${this.id}_${language}_patched_messages`))
@ -211,10 +292,8 @@ module.exports = class i18nScope {
}
}
// 以下方法引用全局VoerkaI18n实例的方法
get on(){return this.global.on.bind(this.global)}
get off(){return this.global.off.bind(this.global)}
get offAll(){return this.global.offAll.bind(this.global)}
get change(){
return this.global.change.bind(this.global)
}
get on(){return this._global.on.bind(this._global)}
get off(){return this._global.off.bind(this._global)}
get offAll(){return this._global.offAll.bind(this._global)}
get change(){return this._global.change.bind(this._global)}
}

View File

@ -41,6 +41,9 @@
return false
}
}
function isFunction(fn){
return typeof fn === "function"
}
/**
* 当value= null || undefined || "" || [] || {} 时返回true
* @param {*} value
@ -143,7 +146,6 @@ function toNumber(value,defualt=0) {
return value
}
}
/**
* 转换为货币格式
*
@ -184,6 +186,7 @@ function relativeTime(value, rel){
module.exports ={
isPlainObject,
isFunction,
isNumber,
isNothing,
deepMerge,