diff --git a/packages/runtime/src/__tests__/index.test.ts b/packages/runtime/src/__tests__/index.test.ts index 2024f62..5fea37c 100644 --- a/packages/runtime/src/__tests__/index.test.ts +++ b/packages/runtime/src/__tests__/index.test.ts @@ -18,21 +18,24 @@ function mergeFormattersConfigs(configSources:any[]){ return finalConfig },{}) } + const zhMessages:VoerkaI18nLanguageMessages = { $config:{ add:{a:1}, dec:{b:1} }, - "1": "你好", - "2": "你好,{name}", - "3": "中国", - "4": ["我没有车","我有一部车","我有两部车","我有{}部车"] , + "你好": "你好", + "我叫{name},今年{age}岁": "我叫{name},今年{age}岁", + "中国": "中国", + "我有{}部车": ["我没有车","我有一部车","我有两部车","我有{}部车"] , + "我的工资是每月{}元":"我的工资是每月{}元" } const enMessages={ - "1": "hello", - "2": "hello,{name}", - "3": "china", - "4": ["I don't have car","I have a car","I have two cars","I have {} cars"] + "你好": "hello", + "我叫{name},今年{age}岁": "My name is {name},Now {age} years old year", + "中国": "china", + "我有{}部车": ["I don't have car","I have a car","I have two cars","I have {} cars"], + "我的工资是每月{}元":"My salary is {} yuan per month" } const messages = { @@ -42,7 +45,7 @@ const messages = { const idMap={ "你好":1, - "你好,{name}":2, + "你好,{name}":2, "中国":3, "我有{}部车":4 } @@ -67,16 +70,16 @@ const formatters ={ x:{g:1}, y:{g:1}, g:{g1:1,g2:2} - }, - add:(value:any,args?:any[],$config?:VoerkaI18nFormatterConfigs)=>'*'+ value+1, + } }, zh:{ + $config:{}, + prefix:(value:any,args:any[],config?:VoerkaI18nFormatterConfigs)=>config?.chars+value, first:(value:any)=>'ZH'+value[0], - ...zhFormatters }, en:{ + $config:{}, first:(value:any)=>'EN'+value[0], - ...enFormatters, }, jp:()=>{} } @@ -88,8 +91,7 @@ beforeAll(async ()=>{ return new Promise((resolve)=>{ scope = new VoerkaI18nScope({ id: "test", - languages, - idMap, + languages, messages, formatters, callback:()=>{ @@ -238,23 +240,79 @@ describe('翻译函数', () => { beforeAll(() => { t = scope.t }) - test('基本翻译',async () => { expect(t("你好")).toBe("你好") - expect(t("你好,{name}","张三")).toBe("你好,张三") - expect(t("中国")).toBe("中国") - expect(t("我有{}部车",0)).toBe("我没有车") - expect(t("我有{}部车",1)).toBe("我有一部车") - expect(t("我有{}部车",1)).toBe("我有两部车") - expect(t("我有{}部车",3)).toBe("我有3部车") + expect(t("我叫{name},今年{age}岁","张三",12)).toBe("我叫张三,今年12岁") + expect(t("我叫{name},今年{age}岁",["张三",12])).toBe("我叫张三,今年12岁") + expect(t("我叫{name},今年{age}岁",{name:"张三",age:12})).toBe("我叫张三,今年12岁") await scope.change("en") expect(t("你好")).toBe("hello") - expect(t("你好,{name}","张三")).toBe("hello,张三") + expect(t("我叫{name},今年{age}岁","tom",12)).toBe("My name is tom,Now 12 years old year") + expect(t("我叫{name},今年{age}岁",["tom",12])).toBe("My name is tom,Now 12 years old year") + expect(t("我叫{name},今年{age}岁",{name:"tom",age:12})).toBe("My name is tom,Now 12 years old year") expect(t("中国")).toBe("china") - expect(t("我有{}部车",0)).toBe("I have 0 cars") - expect(t("我有{}部车",1)).toBe("I have 1 cars") - expect(t("我有{}部车",2)).toBe("I have many cars") }) - + test('基本复数翻译',async () => { + expect(t("我有{}部车",0)).toBe("我没有车") + expect(t("我有{}部车",1)).toBe("我有一部车") + expect(t("我有{}部车",2)).toBe("我有两部车") + expect(t("我有{}部车",3)).toBe("我有3部车") + expect(t("我有{}部车",100)).toBe("我有100部车") + await scope.change("en") + expect(t("我有{}部车",0)).toBe("I don't have car") + expect(t("我有{}部车",1)).toBe("I have a car") + expect(t("我有{}部车",2)).toBe("I have two cars") + expect(t("我有{}部车",3)).toBe("I have 3 cars") + expect(t("我有{}部车",100)).toBe("I have 100 cars") + }) }) + +describe('插值变量格式化器', () => { + let t:VoerkaI18nTranslate + + + beforeAll(() => { + t = scope.t + // 注册格式化器,注册为所有语言 + scope.registerFormatter("add", (value,args,config) => { + return String(Number(value) + (Number(args.length==0 ? 1 : args[0]))) + }); + scope.formatters.updateConfig("zh",{ + bookname:{ + beginChar:"《", + endChar:"》" + } + }); + scope.formatters.updateConfig("en",{ + bookname:{ + beginChar:"<", + endChar:">" + } + }); + // 注册格式化器,注册为所有语言 + scope.registerFormatter("bookname", (value,args,config) => { + let { beginChar = "<",endChar=">" } = Object.assign({},(config as any)?.bookname) + if(args.length==1){ + beginChar = endChar = args[0] + }else if(args.length>=2){ + beginChar = args[0] + endChar = args[1] + } + return beginChar + value + endChar + }) + }) + test('格式化器',async () => { + expect(t("我的工资是每月{|add}元",1000)).toBe("我的工资是每月1001元") + expect(t("我的工资是每月{|add()}元",1000)).toBe("我的工资是每月1001元") + expect(t("我的工资是每月{|add(2)}元",1000)).toBe("我的工资是每月1002元") + expect(t("我的工资是每月{|add|add()|add(2)}元",1000)).toBe("我的工资是每月1004元") + }) + test('bookname式化器',async () => { + expect(t("hello {|bookname}","tom")).toBe("hello 《tom》") + expect(t("hello {|bookname('#')}","tom")).toBe("hello #tom#") + expect(t("hello {|bookname('#','!')}","tom")).toBe("hello #tom!") + await scope.change("en") + expect(t("hello {|bookname}","tom")).toBe("hello ") + }) +}) \ No newline at end of file diff --git a/packages/runtime/src/formatterRegistry.ts b/packages/runtime/src/formatterRegistry.ts index 559938e..6840fb8 100644 --- a/packages/runtime/src/formatterRegistry.ts +++ b/packages/runtime/src/formatterRegistry.ts @@ -101,7 +101,7 @@ export class VoerkaI18nFormatterRegistry{ configSources.push(this.getConfig(language)) // 合并当前语言的格式化器配置参数 this.#activeFormattersConfigs = configSources.reduce((finalConfig, curConfig)=>{ - if(isPlainObject(curConfig)) deepMerge(finalConfig,curConfig,{newObject:false,array:'replace'}) + if(isPlainObject(curConfig)) finalConfig = deepMerge(finalConfig,curConfig,{array:'replace'}) return finalConfig },{}) }catch(e){ @@ -113,7 +113,7 @@ export class VoerkaI18nFormatterRegistry{ if(language in this.#formatters ){ let formatters = this.#formatters[language] as VoerkaI18nFormatters if(!("$config" in formatters)) formatters.$config = {} - assignObject(formatters.$config as object ,config) + assignObject(formatters.$config as any ,config) } if(language === this.#language){ this.generateFormattersConfigs(language) @@ -142,7 +142,8 @@ export class VoerkaI18nFormatterRegistry{ * 也可以指定将格式化器注册到多个语言 * */ - register(name:string | SupportedDateTypes, formatter:VoerkaI18nFormatter, {language = "*"}:{ language: string | string[] } ) { + register(name:string | SupportedDateTypes, formatter:VoerkaI18nFormatter,options?:{ language?: string | string[] } ) { + const { language='*' } = options || {}; if (!isFunction(formatter) || typeof name !== "string") { throw new TypeError("Formatter must be a function"); } @@ -166,21 +167,18 @@ export class VoerkaI18nFormatterRegistry{ * @param language */ getConfig(language?:string){ - if(language== this.#language) return this.#activeFormattersConfigs return language ? getByPath(this.#formatters,`${language}.$config`,{defaultValue:{}}) : {} } /** 获取指定语言中为每个数据类型指定的格式化器 */ getTypes(language?:string){ - if(language== this.#language) return this.activeFormatters.$types return language ? getByPath(this.#formatters,`${language}.$types`,{defaultValue:{}}) : {} } /** 获取指定语言中为每个数据类型指定的格式化器 */ getFormatters(language?:string){ - if(language== this.#language) return this.activeFormatters return language ? getByPath(this.#formatters,language,{defaultValue:{}}) : {} } diff --git a/packages/runtime/src/interpolate.ts b/packages/runtime/src/interpolate.ts index 5cc55ce..0a6d903 100644 --- a/packages/runtime/src/interpolate.ts +++ b/packages/runtime/src/interpolate.ts @@ -43,6 +43,9 @@ import { SupportedDateTypes, VoerkaI18nFormatter, VoerkaI18nFormatterConfigs } f // v2: 由于一些js引擎(如react-native Hermes )不支持命名捕获组而导致运行时不能使用,所以此处移除命名捕获组 const varWithPipeRegexp = /\{\s*(\w+)?((\s*\|\s*\w*(\(.*\)){0,1}\s*)*)\s*\}/g; +// +type WrapperedVoerkaI18nFormatter = (value:string,config:VoerkaI18nFormatterConfigs)=>string; + /** * 考虑到通过正则表达式进行插值的替换可能较慢 * 因此提供一个简单方法来过滤掉那些不需要进行插值处理的字符串 @@ -141,7 +144,7 @@ function executeChecker(checker:FormatterChecker, value:any,scope:VoerkaI18nScop * @param {FormatterDefineChain} formatters 经过解析过的格式化器参数链 ,多个格式化器函数(经过包装过的)顺序执行,前一个输出作为下一个格式化器的输入 * formatters [ [<格式化器名称>,[<参数>,<参数>,...],[<格式化器名称>,[<参数>,<参数>,...]],...] */ -function executeFormatter(value:any, formatters:VoerkaI18nFormatter[], scope:VoerkaI18nScope, template:string) { +function executeFormatter(value:any, formatters:WrapperedVoerkaI18nFormatter[], scope:VoerkaI18nScope, template:string) { if (formatters.length === 0) return value; let result = value; // 1. 空值检查 @@ -174,7 +177,7 @@ function executeFormatter(value:any, formatters:VoerkaI18nFormatter[], scope:Voe // 3. 分别执行格式化器函数 for (let formatter of formatters) { try { - result = formatter(result, [result],scope.formatters.config); + result = formatter(result, scope.formatters.config); } catch (e:any) { e.formatter = (formatter as any).$name; if (scope.debug) @@ -228,23 +231,23 @@ function addDefaultFormatters(formatters:FormatterDefineChain) { * */ function wrapperFormatters(scope:VoerkaI18nScope, activeLanguage:string, formatters:FormatterDefineChain) { - let wrappedFormatters:VoerkaI18nFormatter[] = []; + let wrappedFormatters:WrapperedVoerkaI18nFormatter[] = []; addDefaultFormatters(formatters); for (let [name, args] of formatters) { let fn = scope.formatters.get(name,{on:'scope'}) let formatter; if (isFunction(fn)) { - formatter = (value:any, args?:any[],config?:VoerkaI18nFormatterConfigs) =>{ - return (fn as Function).call(scope.formatters.config, value, args, config); + formatter = (value:string,config:VoerkaI18nFormatterConfigs) =>{ + return String((fn as Function).call(scope.formatters.config, value, args, config)) } } else { // 格式化器无效或者没有定义时,查看当前值是否具有同名的原型方法,如果有则执行调用 // 比如padStart格式化器是String的原型方法,不需要配置就可以直接作为格式化器调用 formatter = (value:any) => { if (isFunction(value[name])) { - return value[name](...args); + return String(value[name](...args)); } else { - return value; + return String(value) } }; } @@ -271,8 +274,10 @@ function getFormattedValue(scope:VoerkaI18nScope, activeLanguage:string, formatt if (formatterFuncs.length == 2) { // 当没有格式化器时,查询是否指定了默认数据类型的格式化器,如果有则执行 const defaultFormatter = scope.formatters.get(getDataTypeName(value),{on:'types'}) - if (defaultFormatter) { - return executeFormatter(value, [defaultFormatter], scope, template); + if (defaultFormatter) { + return executeFormatter(value, [ + (value:string,config)=>defaultFormatter.call(config, value, [], config), + ], scope, template); } } else { value = executeFormatter(value, formatterFuncs, scope, template); diff --git a/packages/runtime/src/manager.ts b/packages/runtime/src/manager.ts index 23abd8a..19a7c6d 100644 --- a/packages/runtime/src/manager.ts +++ b/packages/runtime/src/manager.ts @@ -145,7 +145,8 @@ export class VoerkaI18nManager extends EventEmitter{ * @param {*} formatter language : 声明该格式化器适用语言 */ - registerFormatter(name:string,formatter:VoerkaI18nFormatter,{language="*"}:{language:string | string[] | '*'}){ + registerFormatter(name:string,formatter:VoerkaI18nFormatter,options?:{language?:string | string[] | '*'}){ + const {language = "*"} = options || {} if (!isFunction(formatter) || typeof name !== "string") { throw new TypeError("Formatter must be a function"); } diff --git a/packages/runtime/src/scope.ts b/packages/runtime/src/scope.ts index 657104e..c9d3d84 100644 --- a/packages/runtime/src/scope.ts +++ b/packages/runtime/src/scope.ts @@ -28,7 +28,7 @@ export interface VoerkaI18nScopeOptions { defaultLanguage?: string // 默认语言名称 activeLanguage?: string // 当前语言名称 messages: VoerkaI18nLanguageMessagePack // 当前语言包 - idMap: Voerkai18nIdMap // 消息id映射列表 + idMap?: Voerkai18nIdMap // 消息id映射列表 formatters: VoerkaI18nLanguageFormatters // 当前作用域的格式化函数列表{: {$types,$config,[格式化器名称]: () => {},[格式化器名称]: () => {}}} callback?:(e?:Error)=>void // 当注册到全局管理器后的回调函数 } @@ -173,7 +173,8 @@ export class VoerkaI18nScope { 语言名称,语言名称数组,或者使用,分割的语言名称字符串 asGlobal : 注册到全局 */ - registerFormatter(name:string, formatter:VoerkaI18nFormatter, {language = "*", asGlobal= true}:{ language: string | string[] | "*", asGlobal :boolean } ) { + registerFormatter(name:string, formatter:VoerkaI18nFormatter, options?:{ language?: string | string[] | "*", asGlobal?:boolean } ) { + const {language = "*", asGlobal= true} = options || {} if(asGlobal){ this.global.registerFormatter(name, formatter, {language}); }else{ diff --git a/packages/runtime/src/translate.ts b/packages/runtime/src/translate.ts index 0c55f11..8bd34d3 100644 --- a/packages/runtime/src/translate.ts +++ b/packages/runtime/src/translate.ts @@ -5,35 +5,6 @@ import { replaceInterpolatedVars } from "./interpolate" import type { VoerkaI18nScope } from "./scope" -/** - * 当传入的翻译内容不是一个字符串时,进行默认的转换 - * - * - 对函数则执行并取返回结果() - * - 对Array和Object使用JSON.stringify - * - 其他类型使用toString - * - * @param {*} value - * @returns - */ - function transformToString(value:any){ - let result = value - try{ - if(isFunction(result)) result = value() - if(!(typeof(result)==="string")){ - if(Array.isArray(result) || isPlainObject(result)){ - result = JSON.stringify(result) - }else{ - result = result.toString() - } - }else{ - return value - } - }catch{ - result = result.toString() - } - return result -} - /** * 文本id必须是一个数字 * @param {*} content @@ -113,20 +84,28 @@ export function translate(this:VoerkaI18nScope,message:string,...args:any[]):str } // 3. 取得翻译文本模板字符串 - if(activeLanguage === scope.defaultLanguage){ - // 2.1 从默认语言中取得翻译文本模板字符串 - // 如果当前语言就是默认语言,不需要查询加载,只需要做插值变换即可 - // 当源文件运用了babel插件后会将原始文本内容转换为msgId - // 如果是msgId则从scope.default中读取,scope.default=默认语言包={:} - if(isMessageId(result)){ - result = (scope.default as any)[result] || message - } - }else{ - // 2.2 从当前语言包中取得翻译文本模板字符串 - // 如果没有启用babel插件将源文本转换为msgId,需要先将文本内容转换为msgId - let msgId = isMessageId(result) ? result : scope.idMap[result] - result = (scope.current as any)[msgId] || result + // if(activeLanguage === scope.defaultLanguage){ + // // 2.1 从默认语言中取得翻译文本模板字符串 + // // 如果当前语言就是默认语言,不需要查询加载,只需要做插值变换即可 + // // 当源文件运用了babel插件后会将原始文本内容转换为msgId + // // 如果是msgId则从scope.default中读取,scope.default=默认语言包={:} + // if(isMessageId(result)){ + // result = (scope.default as any)[result] || message + // } + // }else{ + // // 2.2 从当前语言包中取得翻译文本模板字符串 + // // 如果没有启用babel插件将源文本转换为msgId,需要先将文本内容转换为msgId + // let msgId = isMessageId(result) ? result : scope.idMap[result] + // result = (scope.current as any)[msgId] || result + // } + + if(isMessageId(message)){ + const msgId = scope.idMap[message] + result = (scope.current as any)[msgId] || message + }else{ + result = (scope.current as any)[message] || message } + // 2. 处理复数 // 经过上面的处理,content可能是字符串或者数组 // content = "原始文本内容" || 复数形式["原始文本内容","原始文本内容"....] diff --git a/packages/runtime/src/types.ts b/packages/runtime/src/types.ts index 0fc77ce..cdacac5 100644 --- a/packages/runtime/src/types.ts +++ b/packages/runtime/src/types.ts @@ -35,7 +35,7 @@ export interface VoerkaI18nLanguageDefine { export type VoerkaI18nFormatterConfigs = Record -export type VoerkaI18nFormatter = ((value: string,args?: any[],$config?:VoerkI18nFormatterConfigs) => string) +export type VoerkaI18nFormatter = ((value: string,args: any[],config: VoerkI18nFormatterConfigs) => string) export type VoerkaI18nTypesFormatters=Partial> export type VoerkaI18nTypesFormatterConfig= Partial> export type VoerkaI18nTypesFormatterConfigs= Partial>> @@ -64,7 +64,8 @@ export type VoerkaI18nDefaultMessageLoader = (this:VoerkaI18nScope,newLanguage:s export type TranslateMessageVars = number | boolean | string | Function | Date export interface VoerkaI18nTranslate { - (message: string, ...args: TranslateMessageVars[]): string + (message: string, ...vars: TranslateMessageVars[]): string + (message: string, vars: TranslateMessageVars[]): string (message: string, vars?: Record): string } export interface VoerkaI18nSupportedLanguages {