diff --git a/packages/apps/reactapp/src/languages/index.js b/packages/apps/reactapp/src/languages/index.js index 5e4b5d3..68ba1c5 100644 --- a/packages/apps/reactapp/src/languages/index.js +++ b/packages/apps/reactapp/src/languages/index.js @@ -36,7 +36,7 @@ const loaders = { // 语言作用域 const scope = new VoerkaI18nScope({ ...scopeSettings, // languages,defaultLanguage,activeLanguage,namespaces,formatters - id : "reactapp", // 当前作用域的id,自动取当前工程的package.json的name + id : "reactapp", // 当前作用域的id,自动取当前工程的package.json的name debug : false, // 是否在控制台输出高度信息 default : defaultMessages, // 默认语言包 messages : activeMessages, // 当前语言包 diff --git a/packages/runtime/src/__tests__/index.test.ts b/packages/runtime/src/__tests__/index.test.ts index e69de29..d56d0c8 100644 --- a/packages/runtime/src/__tests__/index.test.ts +++ b/packages/runtime/src/__tests__/index.test.ts @@ -0,0 +1,64 @@ + + +import {test,vi,describe,expect,afterAll,beforeAll} from 'vitest' +import { VoerkaI18nScope } from '../scope' +import zhFormatters from '../formatters/zh'; +import enFormatters from '../formatters/en'; + + +const zhMessages={ + "1": "你好", + "2": "你好,{name}", + "3": "中国", + "4": ["我有一部车","我有很多部车"] +} +const enMessages={ + "1": "hello", + "2": "hello,{name}", + "3": "china", + "4": "I have {} cars" +} + +const messages = { + zh: zhMessages, + en: enMessages +} + +const idMap={ + "你好":1, + "你好,{name}":2, + "中国":3, + "我有{}部车":4 +} +const languages = [ + { name: "zh",default:true,active:true}, + { name: "en"} +] + +const formatters ={ + zh:zhFormatters, + en:enFormatters +} + +describe("VoerkaI18nScope", () => { + let scope = new VoerkaI18nScope({ + id: "test", + languages, + idMap, + messages, + formatters + }) + test("成功创建实例", () => { + expect(scope).toBeInstanceOf(VoerkaI18nScope) + expect(scope.activeLanguage).toBe("zh") + expect(scope.defaultLanguage).toBe("zh") + expect(scope.messages).toEqual(messages) + expect(scope.default).toEqual(zhMessages) + expect(scope.current).toEqual(zhMessages) + expect(scope.idMap).toEqual(idMap) + }) + +}) + +test('translate', () => {}) + diff --git a/packages/runtime/src/datatypes/chinese.ts b/packages/runtime/src/datatypes/chinese.ts index c614225..d1ff0a2 100644 --- a/packages/runtime/src/datatypes/chinese.ts +++ b/packages/runtime/src/datatypes/chinese.ts @@ -10,14 +10,14 @@ import { toChineseNumber } from "flex-tools/chinese/toChineseNumber" import { toChineseCurrency } from "flex-tools/chinese/toChineseCurrency" -export const chineseNumberFormatter = Formatter((value: number, isBig: boolean, $config: any) => { - return toChineseNumber(value, isBig) +export const chineseNumberFormatter = Formatter((value: any, [isBig]:[isBig: boolean], $config) => { + return toChineseNumber(value, isBig) as string }, { params: ["isBig"] }) -export const rmbFormater = FlexFormatter((value:number | string, params : any, $config:any) => { +export const rmbFormater = FlexFormatter((value:number | string, [params] : any[], $config:any) => { return toChineseCurrency(value, params, $config) }, { params: ["big", "prefix", "unit", "suffix"], diff --git a/packages/runtime/src/datatypes/currency.ts b/packages/runtime/src/datatypes/currency.ts index 55bee97..08a11b3 100644 --- a/packages/runtime/src/datatypes/currency.ts +++ b/packages/runtime/src/datatypes/currency.ts @@ -98,7 +98,7 @@ export const currencyFormatter = FlexFormatter((value:string | number,params:Rec } return toCurrency(value,params,$config) },{ - normalize: toNumber, + normalize: (value:string)=>toNumber(value), params : ["format","unit","precision","prefix","suffix","division","symbol","radix"], configKey: "currency" },{ diff --git a/packages/runtime/src/datatypes/datetime.ts b/packages/runtime/src/datatypes/datetime.ts index cb62300..a92cba6 100644 --- a/packages/runtime/src/datatypes/datetime.ts +++ b/packages/runtime/src/datatypes/datetime.ts @@ -3,18 +3,16 @@ 处理日期时间相关 */ -const { isFunction,replaceAll,toDate } = require('../utils') -const { Formatter } = require('../formatter'); +import { toDate } from '../utils' +import { Formatter } from '../formatter'; import { formatDateTime } from "flex-tools/misc/formatDateTime" import { relativeTime } from "flex-tools/misc/relativeTime" import { assignObject } from "flex-tools/object/assignObject" - +import { isFunction } from "flex-tools/typecheck/isFunction" function formatTime(value:number ,template="HH:mm:ss"){ - return formatDateTime(value,template,{ - - }) + return formatDateTime(value,template,{}) } @@ -29,9 +27,10 @@ function formatTime(value:number ,template="HH:mm:ss"){ * **/ export type FormatterTransformer = (value:any,format:string)=>string + export function createDateTimeFormatter(options={},transformer:FormatterTransformer){ let opts = assignObject({presets:{}},options) - return Formatter(function(value,format,$config){ + return Formatter(function(this:any,value:any,[format]:any[],$config:Record){ if((format in opts.presets) && isFunction(opts.presets[format])){ return opts.presets[format](value) }else if((format in $config)){ @@ -53,17 +52,17 @@ export function createDateTimeFormatter(options={},transformer:FormatterTransfor * - format取值:local,long,short,iso,gmt,utc,<模板字符串> * - 默认值由$config.datetime.date.format指定 */ - const dateFormatter = createDateTimeFormatter({ + export const dateFormatter = createDateTimeFormatter({ normalize: toDate, params : ["format"], configKey: "datetime.date", presets : { - local: value=>value.toLocaleString(), - iso : value=>value.toISOString(), - utc : value=>value.toUTCString(), - gmt : value=>value.toGMTString() + local: (value:any)=>value.toLocaleString(), + iso : (value:any)=>value.toISOString(), + utc : (value:any)=>value.toUTCString(), + gmt : (value:any)=>value.toGMTString() } -},formatDatetime) +},formatDateTime) /** @@ -71,8 +70,8 @@ export function createDateTimeFormatter(options={},transformer:FormatterTransfor * - format: long,short,number * - 默认值是 short */ -const quarterFormatter = createDateTimeFormatter({ - normalize : value=>{ +export const quarterFormatter = createDateTimeFormatter({ + normalize : (value:any)=>{ const month = value.getMonth() + 1 return Math.floor( ( month % 3 == 0 ? ( month / 3 ) : (month / 3 + 1 ) )) }, @@ -85,7 +84,7 @@ const quarterFormatter = createDateTimeFormatter({ * - format: long,short,number * - 默认值是 short */ -const monthFormatter = createDateTimeFormatter({ +export const monthFormatter = createDateTimeFormatter({ normalize: (value:Date)=> value.getMonth() + 1, params : ["format"], configKey: "datetime.month" @@ -96,7 +95,7 @@ const monthFormatter = createDateTimeFormatter({ * - format: long,short,number * - 默认值是 long */ -const weekdayFormatter = createDateTimeFormatter({ +export const weekdayFormatter = createDateTimeFormatter({ normalize: (value:Date)=> value.getDay(), params : ["format"], configKey: "datetime.weekday" @@ -107,7 +106,7 @@ const weekdayFormatter = createDateTimeFormatter({ * - format取值:local,long,short,timestamp,<模板字符串> * - 默认值由$config.datetime.time.format指定 */ - const timeFormatter = createDateTimeFormatter({ +export const timeFormatter = createDateTimeFormatter({ normalize : toDate, params : ["format"], configKey : "datetime.time", @@ -122,7 +121,7 @@ const weekdayFormatter = createDateTimeFormatter({ * @param {*} value * @param {*} baseTime 基准时间,默认是相对现在 */ -const relativeTimeFormatter = Formatter((value:Date,baseTime:Date,$config:any)=>{ +export const relativeTimeFormatter = Formatter((value:any,[baseTime]:[Date],$config:any)=>{ //const { units,now,before,base = Date.now() , after } = $config return relativeTime(value, $config) },{ diff --git a/packages/runtime/src/datatypes/numeric.ts b/packages/runtime/src/datatypes/numeric.ts index 58be0d3..02528f7 100644 --- a/packages/runtime/src/datatypes/numeric.ts +++ b/packages/runtime/src/datatypes/numeric.ts @@ -8,16 +8,17 @@ * { value | number('big') } * */ -const { isNumber,toNumber } = require("../utils") -const { Formatter } = require("../formatter") -const { toCurrency } = require("./currency") +import { toNumber } from "../utils" +import { Formatter } from "../formatter" +import { toCurrency } from "./currency" +import { VoerkaI18nFormatter } from '../types'; -const numberFormartter = Formatter(function(value,precision,division,$config){ +export const numberFormartter = Formatter(function(value:any,[precision,division]:[number,number],$config:any){ return toCurrency(value, { division, precision},$config) },{ normalize: toNumber, params:["precision","division"], configKey: "number" -}) +}) as VoerkaI18nFormatter \ No newline at end of file diff --git a/packages/runtime/src/formatter.ts b/packages/runtime/src/formatter.ts index 4d2474c..3fbec1a 100644 --- a/packages/runtime/src/formatter.ts +++ b/packages/runtime/src/formatter.ts @@ -271,8 +271,8 @@ function parseFormaterParams(strParams: string): any[] { */ export type Formatter = (this: any, value: string, ...args: any[]) => string export interface FormatterOptions { - normalize?: (value: string) => string // 对输入值进行规范化处理,如进行时间格式化时,为了提高更好的兼容性,支持数字时间戳/字符串/Date等,需要对输入值进行处理,如强制类型转换等 - params?: Record, // 可选的,声明参数顺序,如果是变参的,则需要传入null + normalize?: (value: string) => any // 对输入值进行规范化处理,如进行时间格式化时,为了提高更好的兼容性,支持数字时间戳/字符串/Date等,需要对输入值进行处理,如强制类型转换等 + params?: Record | null, // 可选的,声明参数顺序,如果是变参的,则需要传入null configKey?: string // 声明该格式化器在$config中的路径,支持简单的使用.的路径语法 } @@ -285,7 +285,7 @@ export function createFormatter(fn: Formatter, options?: FormatterOptions, defau // 最后一个参数是传入activeFormatterConfig参数 // 并且格式化器的this指向的是activeFormatterConfig - const $formatter = function (this: any, value: string, ...args: any[]) { + const $formatter = function (this: any, value: string, args: any[],$config:Record) { let finalValue = value // 1. 输入值规范处理,主要是进行类型转换,确保输入的数据类型及相关格式的正确性,提高数据容错性 if (isFunction(opts.normalize)) { @@ -312,7 +312,7 @@ export function createFormatter(fn: Formatter, options?: FormatterOptions, defau if (args[i] !== undefined) finalArgs[i] = args[i] } } - return fn.call(this, finalValue, ...finalArgs, formatterConfig) + return fn.call(this, finalValue, finalArgs, formatterConfig) } $formatter.configurable = true return $formatter @@ -339,9 +339,7 @@ export const createFlexFormatter = function (fn: Formatter, options: FormatterOp const opts = assignObject({ params: {} }, options) - const $flexFormatter = Formatter(function (this: any, value: string, ...args: string[]) { - // 1. 最后一个参数总是代表格式化器的参数,不同语言不一样 - let $config = args[args.length - 1] as unknown as Record + const $flexFormatter = Formatter(function (this: any, value: string,args: string[],$config:Record ) { // 2. 从语言配置中读取默认参数 let finalParams = (options.params || {}).reduce((r: Record, name: string) => { r[name] = $config[name] == undefined ? (defaultParams || {})[name] : $config[name] @@ -361,6 +359,7 @@ export const createFlexFormatter = function (fn: Formatter, options: FormatterOp } +export const Formatter = createFormatter export const FlexFormatter = createFlexFormatter diff --git a/packages/runtime/src/formatterRegistry.ts b/packages/runtime/src/formatterRegistry.ts index 1acbd1e..0117415 100644 --- a/packages/runtime/src/formatterRegistry.ts +++ b/packages/runtime/src/formatterRegistry.ts @@ -4,9 +4,10 @@ * * */ -import { VoerkaI18nFormatter, VoerkaI18nFormatters, VoerkaI18nFormattersLoader, VoerkaI18nLanguageFormatters, SupportedDateTypes, VoerkaI18nFormatterConfigs, Formatter } from './types'; +import { VoerkaI18nFormatter, VoerkaI18nFormatters, VoerkaI18nFormattersLoader, VoerkaI18nLanguageFormatters, SupportedDateTypes, VoerkaI18nFormatterConfigs } from './types'; import { DataTypes } from './utils'; import { get as getByPath } from "flex-tools/object/get" +import { isFunction } from 'flex-tools/typecheck/isFunction'; export interface VoerkaI18nScopeCache{ @@ -41,7 +42,7 @@ export class VoerkaI18nFormatterRegistry{ set language(language:string){ this.#language = language if(!(language in this.formatters)){ - (this.formatters[language] as any) = Object.assign({},EmptyFormatters) + (this.#formatters[language] as any) = Object.assign({},EmptyFormatters) } this.#ready = typeof(this.#formatters[language]) != 'function' } @@ -63,7 +64,7 @@ export class VoerkaI18nFormatterRegistry{ *代表适用于所有语言 语言名称,语言名称数组,或者使用,分割的语言名称字符串 */ - register(name:string, formatter:VoerkaI18nFormatter, {language = "*"}:{ language: string | string[] } ) { + register(name:string | SupportedDateTypes, formatter:VoerkaI18nFormatter, {language = "*"}:{ language: string | string[] } ) { if (!isFunction(formatter) || typeof name !== "string") { throw new TypeError("Formatter must be a function"); } @@ -73,7 +74,7 @@ export class VoerkaI18nFormatterRegistry{ this.#formatters[lngName] = { } } if(typeof(this.#formatters[lngName])!="function"){ - let lngFormatters = this.#formatters[lngName] as VoerkaI18nFormatters + let lngFormatters = this.#formatters[lngName] as any if (DataTypes.includes(name)) { if(!lngFormatters.$types){ lngFormatters.$types = {} @@ -168,7 +169,7 @@ export class VoerkaI18nFormatterRegistry{ */ get(name:string,dataType?:SupportedDateTypes):VoerkaI18nFormatter | undefined{ if(!this.#ready) throw new FormattersNotLoadedError(this.#language) - const lngFormatters = this.#formatters[this.#language] as VoerkaI18nFormatters + const lngFormatters = this.#formatters[this.#language] as any if(dataType && (dataType in lngFormatters.$types!)){ return lngFormatters.$types![dataType] }else if(name in lngFormatters){ diff --git a/packages/runtime/src/formatters/default.ts b/packages/runtime/src/formatters/default.ts index cc7a3bb..322cc68 100644 --- a/packages/runtime/src/formatters/default.ts +++ b/packages/runtime/src/formatters/default.ts @@ -1,5 +1,7 @@ -const { toNumber,isFunction } = require("../utils") -const { Formatter } = require("../formatter") +import { assignObject } from 'flex-tools/object/assignObject'; +import { VoerkaI18nFormatterConfigs } from '../types'; +import { toNumber } from "../utils" +import { Formatter } from "../formatter" /** * 字典格式化器 @@ -10,11 +12,11 @@ const { Formatter } = require("../formatter") * @param {...any} args * @returns */ - function dict(key, values) { +function dict(key:string, values:any) { if(key in values){ return values[key] }else{ - return ("default" in values) ? values[key] : value + return ("default" in values) ? values[key] : values } } @@ -37,10 +39,8 @@ const { Formatter } = require("../formatter") * @paran {String} next 下一步行为,取值true/false,break,skip,默认是break * @param {*} config */ -const empty = Formatter(function(value,escapeValue,next,$config){ - if(next===false) next = 'break' - if(next===true) next = 'skip' - let opts = Object.assign({escape:"",next:'break',values:[]},$config) +const empty = Formatter(function(value:any,escapeValue:any,next: 'break' | 'ignore',$config:VoerkaI18nFormatterConfigs){ + let opts = assignObject({escape:"",next:'break',values:[]},$config) if(escapeValue!=undefined) opts.escape = escapeValue let emptyValues = [undefined,null] if(Array.isArray(opts.values)) emptyValues.push(...opts.values) @@ -72,10 +72,10 @@ const empty = Formatter(function(value,escapeValue,next,$config){ * @param {*} config 格式化器的全局配置参数 * @returns */ -const error = Formatter(function(value,escapeValue,next,$config){ +const error = Formatter(function(value:any,escapeValue:any,next:'break' | 'ignore',$config:VoerkaI18nFormatterConfigs){ if(value instanceof Error){ try{ - let opts = Object.assign({escape:null,next:'break'},$config) + let opts = assignObject({escape:null,next:'break'},$config) if(escapeValue!=undefined) opts.escape = escapeValue if(next!=undefined) opts.next = next return { @@ -100,7 +100,7 @@ const error = Formatter(function(value,escapeValue,next,$config){ * @param {*} prefix * @returns */ -function prefix(value,prefix="") { +export function prefix(value:any,prefix:string="") { return prefix ? `${prefix}${value}` : value } /** @@ -109,7 +109,7 @@ function prefix(value,prefix="") { * @param {*} suffix * @returns */ -function suffix(value,suffix="") { +export function suffix(value:any,suffix:string="") { return suffix ? `${value}${suffix}` : value } @@ -141,7 +141,7 @@ const FILE_SIZE_WHOLE_UNITS = ["Bytes", "Kilobytes", "Megabytes", "Gigabytes", " * @param {*} brief 是否采用简称单位 * @param {*} options */ - const filesize= Formatter((value,unit,brief=true,$config)=>{ + export const filesize= Formatter((value:any,unit:string,brief:boolean=true,$config:VoerkaI18nFormatterConfigs)=>{ let v = toNumber(value) let unitIndex if(unit==undefined || unit=="auto"){ @@ -164,13 +164,11 @@ const FILE_SIZE_WHOLE_UNITS = ["Bytes", "Kilobytes", "Megabytes", "Gigabytes", " }) - -module.exports = { +export default { dict, + empty, + error, prefix, suffix, - filesize, - error, - empty -} - + filesize +} \ No newline at end of file diff --git a/packages/runtime/src/formatters/en.ts b/packages/runtime/src/formatters/en.ts index 4dd9010..f75324d 100644 --- a/packages/runtime/src/formatters/en.ts +++ b/packages/runtime/src/formatters/en.ts @@ -4,7 +4,6 @@ */ -import { Formatter } fom "../formatter" import { dateFormatter,quarterFormatter,monthFormatter,weekdayFormatter,timeFormatter,relativeTimeFormatter } from "../datatypes/datetime" import { numberFormartter } from "../datatypes/numeric" import { currencyFormatter } from "../datatypes/currency" @@ -149,11 +148,11 @@ export default { }, // 默认数据类型的格式化器 $types: { - Date : dateFormatter, - Null : value =>"", - Undefined: value =>"", - Error : value => "ERROR", - Boolean : value =>value ? "True":"False", + // Date : dateFormatter, + Null : (value: any) =>"", + Undefined: (value: any) =>"", + Error : (value: any) => "ERROR", + Boolean : (value: any) =>value ? "True":"False", Number : numberFormartter }, // 以下是格式化定义 diff --git a/packages/runtime/src/formatters/zh.ts b/packages/runtime/src/formatters/zh.ts index 20bebf8..6efc6c2 100644 --- a/packages/runtime/src/formatters/zh.ts +++ b/packages/runtime/src/formatters/zh.ts @@ -5,9 +5,10 @@ import { chineseNumberFormatter,rmbFormater } from "../datatypes/chinese" +import {CN_DATETIME_UNITS, CN_MONTH_NAMES, CN_SHORT_MONTH_NAMES, CN_SHORT_WEEK_DAYS, CN_WEEK_DAYS } from "flex-tools/chinese/consts" -module.exports = { +export default { // 配置参数: 格式化器函数的最后一个参数就是该配置参数 $config:{ datetime : { diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 50ee6a8..606ee32 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -1,3 +1,4 @@ +// @ts-ignore import replaceAll from "string.prototype.replaceall" replaceAll.shim() diff --git a/packages/runtime/src/interpolate.ts b/packages/runtime/src/interpolate.ts index 3c71c36..f3b27d5 100644 --- a/packages/runtime/src/interpolate.ts +++ b/packages/runtime/src/interpolate.ts @@ -203,7 +203,9 @@ function getFormatter(scope:VoerkaI18nScope, activeLanguage:string, name:string) } } } -export type FormatterChecker = (value:any,config?:VoerkaI18nFormatterConfigs)=>any; +export type FormatterChecker = ((value:any,config?:VoerkaI18nFormatterConfigs)=>any) & { + $name:string +} /** * Checker是一种特殊的格式化器,会在特定的时间执行 @@ -242,7 +244,7 @@ function executeChecker(checker:FormatterChecker, value:any,scope:VoerkaI18nScop * @param {FormatterDefineChain} formatters 经过解析过的格式化器参数链 ,多个格式化器函数(经过包装过的)顺序执行,前一个输出作为下一个格式化器的输入 * formatters [ [<格式化器名称>,[<参数>,<参数>,...],[<格式化器名称>,[<参数>,<参数>,...]],...] */ -function executeFormatter(value:any, formatters:FormatterDefineChain, scope:VoerkaI18nScope, template:string) { +function executeFormatter(value:any, formatters:VoerkaI18nFormatter[], scope:VoerkaI18nScope, template:string) { if (formatters.length === 0) return value; let result = value; // 1. 空值检查 @@ -258,11 +260,11 @@ function executeFormatter(value:any, formatters:FormatterDefineChain, scope:Voer } // 2. 错误检查 const errorCheckerIndex = formatters.findIndex((func) => (func as any).$name === "error" ); - let errorChecker; + let errorChecker:FormatterChecker; if (errorCheckerIndex != -1) { errorChecker = formatters.splice(errorCheckerIndex, 1)[0] as unknown as FormatterChecker if (result instanceof Error) { - result.formatter = formatter.$name; + (result as any).formatter = errorChecker.$name; const { value, next } = executeChecker(errorChecker, result,scope); if (next == "break") { return value; @@ -275,13 +277,13 @@ function executeFormatter(value:any, formatters:FormatterDefineChain, scope:Voer // 3. 分别执行格式化器函数 for (let formatter of formatters) { try { - result = formatter(result, scope.activeFormatterConfig); + result = formatter(result, [result],scope.activeFormatterConfig); } catch (e:any) { - e.formatter = formatter.$name; + e.formatter = (formatter as any).$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); + console.error(`Error while execute i18n formatter<${(formatter as any).$name}> for ${template}: ${e.message} `); + if (isFunction(errorChecker!)) { + const { value, next } = executeChecker(errorChecker!, result,scope); if (next == "break") { if (value !== undefined) result = value; break; @@ -329,13 +331,13 @@ function addDefaultFormatters(formatters:FormatterDefineChain) { * */ function wrapperFormatters(scope:VoerkaI18nScope, activeLanguage:string, formatters:FormatterDefineChain) { - let wrappedFormatters = []; + let wrappedFormatters:VoerkaI18nFormatter[] = []; addDefaultFormatters(formatters); for (let [name, args] of formatters) { let fn = getFormatter(scope, activeLanguage, name); let formatter; if (isFunction(fn)) { - formatter = (value:any, config:VoerkaI18nFormatterConfigs) =>fn.call(scope.activeFormatterConfig, value, args, config); + formatter = (value:any, args?:any[],config?:VoerkaI18nFormatterConfigs) =>fn.call(scope.activeFormatterConfig, value, args, config); } else { // 格式化器无效或者没有定义时,查看当前值是否具有同名的原型方法,如果有则执行调用 // 比如padStart格式化器是String的原型方法,不需要配置就可以直接作为格式化器调用 @@ -362,7 +364,7 @@ function wrapperFormatters(scope:VoerkaI18nScope, activeLanguage:string, formatt * @param {*} value * @returns */ -function getFormattedValue(scope:VoerkaI18nScope, activeLanguage:string, formatters:FormatterDefineChain, value, template) { +function getFormattedValue(scope:VoerkaI18nScope, activeLanguage:string, formatters:FormatterDefineChain, value:any, template:string) { // 1. 取得格式化器函数列表,然后经过包装以传入当前格式化器的配置参数 const formatterFuncs = wrapperFormatters(scope, activeLanguage, formatters); // 3. 执行格式化器 diff --git a/packages/runtime/src/manager.ts b/packages/runtime/src/manager.ts index e454546..3d41ddd 100644 --- a/packages/runtime/src/manager.ts +++ b/packages/runtime/src/manager.ts @@ -1,11 +1,9 @@ import { isFunction } from "flex-tools/typecheck/isFunction" import { deepMerge } from "flex-tools/object/deepMerge" -import {DataTypes} from "./utils" import { EventEmitter } from "./eventemitter" import inlineFormatters from "./formatters" -import { VoerkaI18nScope } from "./scope" +import type { VoerkaI18nScope } from "./scope" import type { VoerkaI18nLanguageDefine, VoerkaI18nLanguageFormatters, VoerkaI18nDefaultMessageLoader, VoerkaI18nFormatter, VoerkaI18nTypesFormatters } from "./types" -import { SupportedDateTypes } from './types'; import { VoerkaI18nFormatterRegistry } from "./formatterRegistry" // 默认语言配置 @@ -24,7 +22,7 @@ export interface VoerkaI18nManagerOptions { debug?: boolean defaultLanguage: string activeLanguage: string - formatters: VoerkaI18nLanguageFormatters + formatters?: VoerkaI18nLanguageFormatters languages: VoerkaI18nLanguageDefine[] } /** @@ -126,9 +124,9 @@ export class VoerkaI18nManager extends EventEmitter{ * @param {*} scope */ async register(scope:VoerkaI18nScope){ - if(!(scope instanceof VoerkaI18nScope)){ - throw new TypeError("Scope must be an instance of VoerkaI18nScope") - } + // if(!(scope instanceof VoerkaI18nScope)){ + // throw new TypeError("Scope must be an instance of VoerkaI18nScope") + // } this.#scopes.push(scope) return await scope.refresh(this.activeLanguage) } diff --git a/packages/runtime/src/scope.ts b/packages/runtime/src/scope.ts index c4e8953..41f12df 100644 --- a/packages/runtime/src/scope.ts +++ b/packages/runtime/src/scope.ts @@ -1,12 +1,9 @@ -import { DataTypes } from "./utils" import { isPlainObject } from "flex-tools/typecheck/isPlainObject" -import { isFunction } from "flex-tools/typecheck/isFunction" -import { deepClone } from "flex-tools/object/deepClone" -import { get as getByPath } from "flex-tools/object/get" +import { isFunction } from "flex-tools/typecheck/isFunction" import { translate } from "./translate" import { deepMerge } from "flex-tools/object/deepMerge" import { assignObject } from "flex-tools/object/assignObject" -import type {VoerkaI18nManager } from "./manager" +import {VoerkaI18nManager } from "./manager" import type { VoerkaI18nFormatterConfigs, VoerkaI18nDefaultMessageLoader, @@ -18,10 +15,12 @@ import type { VoerkaI18nLanguagePack, VoerkaI18nScopeCache, VoerkaI18nTranslate, - VoerkaI18nLoaders, + VoerkaI18nMessageLoaders, VoerkaI18nTypesFormatters, VoerkaI18nFormatters, VoerkaI18nDynamicLanguageMessages, +VoerkaI18nLanguageMessagePack, +VoerkaI18nMessageLoader, } from "./types" import { VoerkaI18nFormatterRegistry } from './formatterRegistry'; import { InvalidLanguageError } from "./errors" @@ -29,14 +28,12 @@ import { InvalidLanguageError } from "./errors" export interface VoerkaI18nScopeOptions { id?: string debug?: boolean - languages: VoerkaI18nLanguageDefine[] // 当前作用域支持的语言列表 - defaultLanguage: string // 默认语言名称 - activeLanguage: string // 当前语言名称 - default: VoerkaI18nLanguageMessages // 默认语言包 - messages: VoerkaI18nLanguageMessages // 当前语言包 + languages: VoerkaI18nLanguageDefine[] // 当前作用域支持的语言列表 + defaultLanguage?: string // 默认语言名称 + activeLanguage?: string // 当前语言名称 + messages: VoerkaI18nLanguageMessagePack // 当前语言包 idMap: Voerkai18nIdMap // 消息id映射列表 - formatters: VoerkaI18nLanguageFormatters // 当前作用域的格式化函数列表{: {$types,$config,[格式化器名称]: () => {},[格式化器名称]: () => {}}} - loaders: VoerkaI18nLoaders; // 异步加载语言文件的函数列表 + formatters: VoerkaI18nLanguageFormatters // 当前作用域的格式化函数列表{: {$types,$config,[格式化器名称]: () => {},[格式化器名称]: () => {}}} } export class VoerkaI18nScope { @@ -48,83 +45,99 @@ export class VoerkaI18nScope { #activeFormatters:VoerkaI18nFormatters = {} #activeFormatterConfig: VoerkaI18nFormatterConfigs={} #cache:VoerkaI18nScopeCache - #messages:VoerkaI18nLanguageMessages = {} #formatterRegistry:VoerkaI18nFormatterRegistry - constructor(options:VoerkaI18nScopeOptions, callback:(e?:Error)=>void) { + #defaultLanguage:string ='zh' + #activeLanguage:string='zh' + #currentMessages:VoerkaI18nLanguageMessages = {} // 当前语言包 + constructor(options:VoerkaI18nScopeOptions, callback?:(e?:Error)=>void) { this.#options = assignObject({ id : Date.now().toString() + parseInt(String(Math.random() * 1000)), debug : false, languages : {}, // 当前作用域支持的语言列表 - defaultLanguage: "zh", // 默认语言名称 - activeLanguage : "zh", // 当前语言名称 - default : {}, // 默认语言包 - messages : {}, // 当前语言包 - idMap : {}, // 消息id映射列表 - formatters : {}, // 当前作用域的格式化函数列表{: {$types,$config,[格式化器名称]: () => {},[格式化器名称]: () => {}}} - loaders : {} // 异步加载语言文件的函数列表 + messages : {}, // 所有语言包={[language]:VoerkaI18nLanguageMessages} + idMap : {}, // 消息id映射列表 + formatters : {}, // 当前作用域的格式化函数列表{: {$types,$config,[格式化器名称]: () => {},[格式化器名称]: () => {}}} },options) as Required - - this.#patchMessages = {}; // 语言包补丁信息{: {....},:{....}} - this.#refreshing = false; // 正在加载语言包标识 // 用来缓存格式化器的引用,当使用格式化器时可以直接引用,减少检索遍历 this.#cache = { activeLanguage : this.#options.activeLanguage, typedFormatters: {}, formatters : {}, }; + // 初始化 this._initiLanguages() // 如果不存在全局VoerkaI18n实例,说明当前Scope是唯一或第一个加载的作用域,则自动创建全局VoerkaI18n实例 if (!globalThis.VoerkaI18n) { - const { VoerkaI18nManager } = require("./"); globalThis.VoerkaI18n = new VoerkaI18nManager({ debug : this.debug, - defaultLanguage: this.defaultLanguage, - activeLanguage : this.activeLanguage, + defaultLanguage: this.#defaultLanguage, + activeLanguage : this.#activeLanguage, languages : options.languages, }); } this.#t = translate.bind(this) this.#global = globalThis.VoerkaI18n as unknown as VoerkaI18nManager; this.#formatterRegistry = new VoerkaI18nFormatterRegistry() - this.loadFormatters(this.activeLanguage) // 初始化活动的格式化器 this._mergePatchedMessages(); // 从本地缓存中读取并合并补丁语言包 - this._patch(this.messages, this.activeLanguage); // 延后执行补丁命令,该命令会向远程下载补丁包 - this.register(callback); // 在全局注册作用域 + this._patch(this.#currentMessages, this.activeLanguage); // 延后执行补丁命令,该命令会向远程下载补丁包 + if(callback) this.register(callback); // 在全局注册作用域 } get id() {return this.#options.id;} // 作用域唯一id get debug() {return this.#options.debug;} // 调试开关 - get defaultLanguage() {return this.#options.defaultLanguage;} // 默认语言名称 + get defaultLanguage() {return this.#global.defaultLanguage;} // 默认语言名称 get activeLanguage() {return this.#global.activeLanguage;} // 默认语言名称 - get default() {return this.#options.default;} // 默认语言包 - get messages() {return this.#options.messages; } // 当前语言包 + // 默认语言包,只能静态语言包,不能是动态语言包 + get default() {return this.#options.messages[this.#defaultLanguage] as VoerkaI18nLanguageMessages;} + get current() {return this.#currentMessages;} // 当前语言包 + get messages() {return this.#options.messages; } // 所有语言包 get idMap() {return this.#options.idMap;} // 消息id映射列表 get languages() {return this.#options.languages;} // 当前作用域支持的语言列表[{name,title,fallback}] - get loaders() { return this.#options.loaders;} // 异步加载语言文件的函数列表 get global() { return this.#global;} // 引用全局VoerkaI18n配置,注册后自动引用 - get formatters() { return this.#formatterRegistry;} // 当前作用域的所有格式化器定义 {<语言名称>: {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () => {}}} - get activeFormatters() {return this.#formatterRegistry.formatters} // 当前作用域激活的格式化器定义 {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () = >{}} + get formatters() { return this.#formatterRegistry;} // 当前作用域的所有格式化器定义 {<语言名称>: {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () => {}}} + get activeFormatters() {return this.#formatterRegistry.formatters} // 当前作用域激活的格式化器定义 {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () = >{}} get activeFormatterConfig(){return this.#activeFormatterConfig} // 当前格式化器合并后的配置参数,参数已经合并了全局格式化器中的参数 get cache(){return this.#cache } set cache(value:VoerkaI18nScopeCache){ this.#cache=value } get translate(){return this.#t} - get t(){return this.#t} + get t(){return this.#t} /** * 对输入的语言配置进行处理 * - 将en配置为默认回退语言 + * - 确保提供了有效的默认语言和活动语言 */ _initiLanguages(){ if(!Array.isArray(this.languages)){ console.warn("[VoerkaI18n] invalid languages config,use default languages config instead.") this.#options.languages = [ - {name: "zh",title: "中文"}, + {name: "zh",title: "中文",default:true,active:true}, {name: "en",title: "英文"} ] } - // 将en配置为默认回退语言 this.languages.forEach(language=>{ if(!language.fallback) language.fallback = "en" + if(language.default) this.#defaultLanguage = language.name + if(language.active) this.#activeLanguage = language.name }) + // 确保提供了有效的默认语言和活动语言 + const lanMessages = this.#options.messages + if(!(this.#defaultLanguage in lanMessages)) { + this.#defaultLanguage = Object.keys(lanMessages)[0] + } + + if(!(this.#activeLanguage in lanMessages)){ + this.#activeLanguage = this.#defaultLanguage + } + + if(!(this.#defaultLanguage in lanMessages)){ + throw new Error("[VoerkaI18n] invalid config, messages not specified.") + } + // 初始化时,默认和激活的语言包只能是静态语言包,不能是动态语言包 + // 因为初始化时激活语言需要马上显示,如果是异步语言包,会导致显示延迟 + if(isFunction(this.messages[this.#defaultLanguage])){ + throw new Error("[VoerkaI18n] invalid config, messages must be static.") + } + this.#currentMessages = this.messages[this.#activeLanguage] as VoerkaI18nLanguageMessages } /** @@ -132,8 +145,8 @@ export class VoerkaI18nScope { * @param {*} callback 注册成功后的回调 */ register(callback:(e?:Error)=>void) { - if (!isFunction(callback)) callback = () => {}; - this.global.register(this).then(()=>callback()).catch((e)=>callback(e)); + if (!isFunction(callback)) callback = () => {}; + this.global.register(this).then(()=>callback()).catch((e)=>callback(e)); } /** * 注册格式化器 @@ -186,10 +199,10 @@ export class VoerkaI18nScope { * ... * } */ - private loadFormatters(newLanguage:string){ - this.#formatterRegistry.language = newLanguage + private loadFormatters(newLanguage:string){ // 初始化格式化器 this.formatters.loadInitials(this.#options.formatters) + this.#formatterRegistry.language = newLanguage if(this.#options.formatters) // 将配置中的指定为全局的格式化器注册到全局 Object.entries(this.#options.formatters).forEach(([langName,formatters])=>{ @@ -308,29 +321,31 @@ export class VoerkaI18nScope { * 回退到默认语言 */ private _fallback() { - this.#options.messages = this.default; - this.#options.activeLanguage = this.defaultLanguage; + this.#currentMessages = this.default; + this.#activeLanguage = this.defaultLanguage; } /** - * 语言信息包可以是: + * + * 加载指定语言的语言包 + * * - 简单的对象{} * - 或者是一个返回Promise的异步函数 - * + * - 或者是全局的默认加载器 * * * @param language 语言名称 * @returns */ private async loadLanguageMessages(language:string):Promise{ - if(!this.hasLanguage(language)) throw new InvalidLanguageError(`Not found language <${language}>`) + // if(!this.hasLanguage(language)) throw new InvalidLanguageError(`Not found language <${language}>`) // 非默认语言可以是:语言包对象,也可以是一个异步加载语言包文件,加载器是一个异步函数 // 如果没有加载器,则无法加载语言包,因此回退到默认语言 - let loader = this.loaders[language]; + let loader = this.messages[language]; let messages:VoerkaI18nLanguageMessages = {} if (isPlainObject(loader)) { // 静态语言包 messages = loader as unknown as VoerkaI18nLanguageMessages; } else if (isFunction(loader)) { // 语言包异步chunk - messages = (await loader()).default; + messages = (await (loader as VoerkaI18nMessageLoader))().default; } else if (isFunction(this.global.defaultMessageLoader)) { // 从远程加载语言包:如果该语言没有指定加载器,则使用全局配置的默认加载器 const loadedMessages = (await this.global.loadMessagesFromDefaultLoader(language,this)) as unknown as VoerkaI18nDynamicLanguageMessages; @@ -344,7 +359,9 @@ export class VoerkaI18nScope { $remote : true // 添加一个标识,表示该语言包是从远程加载的 },this.default,loadedMessages); // 合并默认语言包和动态语言包,这样就可以局部覆盖默认语言包 } - } + }else{ + throw new Error(`Not found loader for language <${language}>`) + } return messages } /** @@ -356,8 +373,8 @@ export class VoerkaI18nScope { if (!newLanguage) newLanguage = this.activeLanguage; // 默认语言:由于默认语言采用静态加载方式而不是异步块,因此只需要简单的替换即可 if (newLanguage === this.defaultLanguage) { - this.#options.messages = this.default; - await this._patch(this.#options.messages, newLanguage); // 异步补丁 + this.#currentMessages = this.default; + await this._patch(this.#currentMessages, newLanguage); // 异步补丁 await this._changeFormatters(newLanguage); this.#refreshing = false return; @@ -365,12 +382,12 @@ export class VoerkaI18nScope { try{ let messages = await this.loadLanguageMessages(newLanguage) if(messages){ - this.#options.messages = messages - this.#options.activeLanguage = newLanguage; + this.#currentMessages = messages + this.#activeLanguage = newLanguage; // 打语言包补丁, 如果是从远程加载语言包则不需要再打补丁了 // 因为远程加载的语言包已经是补丁过的了 if(!messages.$remote) { - await this._patch(this.#options.messages, newLanguage); + await this._patch(this.#currentMessages, newLanguage); } // 切换到对应语言的格式化器 await this._changeFormatters(newLanguage); @@ -458,6 +475,7 @@ export class VoerkaI18nScope { on(callback:Function) {return this.#global.on(callback); } off(callback:Function) {return this.#global.off(callback); } offAll() {return this.#global.offAll();} + async change(language:string) { await this.#global.change(language); } diff --git a/packages/runtime/src/translate.ts b/packages/runtime/src/translate.ts index c6d3572..b240b1f 100644 --- a/packages/runtime/src/translate.ts +++ b/packages/runtime/src/translate.ts @@ -125,7 +125,7 @@ export function translate(this:VoerkaI18nScope,message:string,...args:any[]):str // 2.2 从当前语言包中取得翻译文本模板字符串 // 如果没有启用babel插件将源文本转换为msgId,需要先将文本内容转换为msgId let msgId = isMessageId(result) ? result : scope.idMap[result] - result = scope.messages[msgId] || result + result = scope.current[msgId] || result } // 2. 处理复数 // 经过上面的处理,content可能是字符串或者数组 diff --git a/packages/runtime/src/types.ts b/packages/runtime/src/types.ts index a5fec7d..4e43474 100644 --- a/packages/runtime/src/types.ts +++ b/packages/runtime/src/types.ts @@ -8,9 +8,13 @@ declare global { export type SupportedDateTypes = "String" | "Number" | "Boolean" | "Object" | "Array" | "Function" | "Error" | "Symbol" | "RegExp" | "Date" | "Null" | "Undefined" | "Set" | "Map" | "WeakSet" | "WeakMap" +// 语言包 export type VoerkaI18nLanguageMessages = Record & { $config?: VoerkaI18nTypesFormatterConfigs } + +export type VoerkaI18nLanguageMessagePack = Record + export type VoerkaI18nDynamicLanguageMessages = Record & { $config?: VoerkaI18nTypesFormatterConfigs } @@ -23,38 +27,36 @@ export type Voerkai18nIdMap = Record export interface VoerkaI18nLanguageDefine { name: string title?: string - default?: boolean + default?: boolean // 是否默认语言 + active?: boolean fallback?: string } export type VoerkaI18nFormatterConfigs = Record -export type VoerkaI18nFormatter = (value: string,args: any[],$config:VoerkI18nFormatterConfigs) => string -export type VoerkaI18nTypesFormatters=Record -export type VoerkaI18nTypesFormatterConfigs= Record> +export type VoerkaI18nFormatter = ((value: string,args?: any[],$config?:VoerkI18nFormatterConfigs) => string) +export type VoerkaI18nTypesFormatters=Partial> +export type VoerkaI18nTypesFormatterConfig= Partial> +export type VoerkaI18nTypesFormatterConfigs= Partial>> export type VoerkaI18nFormattersLoader = (()=>Promise) // 每一个语言的格式化器定义={$types:{...},$config:{...},[格式化器名称]: () => {},[格式化器名称]: () => {} // 在formatters/xxxx.ts里面进行配置 -export type VoerkaI18nFormatters = ({ +export type VoerkaI18nFormatters = { global?: boolean | Omit // 是否全局格式化器 $types?:VoerkaI18nTypesFormatters $config?: VoerkaI18nTypesFormatterConfigs -} & { - [DataTypeName in SupportedDateTypes]?: VoerkaI18nFormatter -} & { +} | { [key: string]: VoerkaI18nFormatter -}) +} // 包括语言的{"*":{...},zh:{...},en:{...}} // 声明格式化器 -export type VoerkaI18nLanguageFormatters = Record +export type VoerkaI18nLanguageFormatters = Record - -// -export type VoerkaI18nLoader = () => Awaited> -export interface VoerkaI18nLoaders { - [key: string]: VoerkaI18nLoader +export type VoerkaI18nMessageLoader = () => Awaited> +export interface VoerkaI18nMessageLoaders { + [key: string]: VoerkaI18nMessageLoader } export type VoerkaI18nDefaultMessageLoader = (this:VoerkaI18nScope,newLanguage:string,scope:VoerkaI18nScope)=>Promise @@ -145,14 +147,13 @@ export interface FormatterDefine { } // 创建格式化器 export type CreateFormatterType = (fn: Function, options: CreateFormatterOptions, defaultParams: Record) => FormatterDefine -export var createFormatter: CreateFormatterType -export var Formatter: CreateFormatterType +// export var createFormatter: CreateFormatterType +// export var Formatter: CreateFormatterType export type CreateFlexFormatterType = (fn: Function, options: CreateFormatterOptions, defaultParams: Record) => FormatterDefine -export var createFlexFormatter: CreateFlexFormatterType -export var FlexFormatter: CreateFlexFormatterType -export var getDataTypeName: (value: any) => string -export var toDate: (value: any) => Date | number -export var toNumber: (value: any, defaultValue: number) => number -export var toBoolean: (value: any) => boolean +// export var createFlexFormatter: CreateFlexFormatterType +// export var FlexFormatter: CreateFlexFormatterType +// export var getDataTypeName: (value: any) => string +// export var toDate: (value: any) => Date | number +// export var toBoolean: (value: any) => boolean diff --git a/packages/runtime/src/utils.ts b/packages/runtime/src/utils.ts index 67082a3..2e404cd 100644 --- a/packages/runtime/src/utils.ts +++ b/packages/runtime/src/utils.ts @@ -44,12 +44,12 @@ export const DataTypes = ["String","Number","Boolean","Object","Array","Functio /** * 转换为数字类型 */ -export function toNumber(value:any,defualt=0):number { +export function toNumber(value:any):number { try { if (isNumber(value)) { return parseFloat(value) } else { - return defualt + return 0 } } catch { return value