From 2bfb0fea0489f1333efe783ebb4011f5ce27b4fe Mon Sep 17 00:00:00 2001 From: wxzhang Date: Thu, 3 Mar 2022 18:02:24 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=BF=BB=E8=AF=91=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E5=92=8C=E8=BD=AC=E7=A0=81=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- {demoapps => demo/apps}/app/a/a1.js | 0 {demoapps => demo/apps}/app/a/a2.js | 0 {demoapps => demo/apps}/app/b/b1.js | 0 {demoapps => demo/apps}/app/c/c1.js | 0 {demoapps => demo/apps}/app/c/c2.js | 0 {demoapps => demo/apps}/app/c/h1.html | 0 {demoapps => demo/apps}/app/index.js | 0 {demoapps => demo/apps}/app/languages/cn.js | 0 {demoapps => demo/apps}/app/languages/de.js | 0 {demoapps => demo/apps}/app/languages/en.js | 0 .../apps}/app/languages/idMap.js | 2 +- .../apps}/app/languages/index.js | 0 {demoapps => demo/apps}/app/languages/jp.js | 0 .../apps}/app/languages/package.json | 0 .../apps}/app/languages/settings.js | 0 .../apps}/app/languages/translates/a.json | 0 .../apps}/app/languages/translates/b.json | 0 .../app/languages/translates/default.json | 0 .../apps}/app/languages/translates/x.json | 0 demo/apps/lib1/languages/idMap.js | 7 + demo/apps/lib2/languages/idMap.js | 7 + {src => demo}/babel.plugin.demo.js | 4 +- demo/compile.demo.js | 7 + {src => demo}/extract.demo.js | 8 +- src/babel-plugin-voerkai18n.js | 111 ++++++--- src/compile.demo.js | 7 - src/compile.js | 55 +++-- src/currency.formatters.js | 24 ++ src/datetime.formatters.js | 28 +++ src/formatters.js | 44 ++-- src/index.js | 228 +++++++++++------- src/templates/entry.js | 6 +- src/templates/formatters.js | 3 +- src/utils.js | 145 +++-------- test/babel.test.js | 83 +++++++ test/compile.test.js | 122 ---------- test/translate.test.js | 219 ++++++++++++++++- 37 files changed, 674 insertions(+), 436 deletions(-) rename {demoapps => demo/apps}/app/a/a1.js (100%) rename {demoapps => demo/apps}/app/a/a2.js (100%) rename {demoapps => demo/apps}/app/b/b1.js (100%) rename {demoapps => demo/apps}/app/c/c1.js (100%) rename {demoapps => demo/apps}/app/c/c2.js (100%) rename {demoapps => demo/apps}/app/c/h1.html (100%) rename {demoapps => demo/apps}/app/index.js (100%) rename {demoapps => demo/apps}/app/languages/cn.js (100%) rename {demoapps => demo/apps}/app/languages/de.js (100%) rename {demoapps => demo/apps}/app/languages/en.js (100%) rename {demoapps => demo/apps}/app/languages/idMap.js (95%) rename {demoapps => demo/apps}/app/languages/index.js (100%) rename {demoapps => demo/apps}/app/languages/jp.js (100%) rename {demoapps => demo/apps}/app/languages/package.json (100%) rename {demoapps => demo/apps}/app/languages/settings.js (100%) rename {demoapps => demo/apps}/app/languages/translates/a.json (100%) rename {demoapps => demo/apps}/app/languages/translates/b.json (100%) rename {demoapps => demo/apps}/app/languages/translates/default.json (100%) rename {demoapps => demo/apps}/app/languages/translates/x.json (100%) create mode 100644 demo/apps/lib1/languages/idMap.js create mode 100644 demo/apps/lib2/languages/idMap.js rename {src => demo}/babel.plugin.demo.js (73%) create mode 100644 demo/compile.demo.js rename {src => demo}/extract.demo.js (84%) delete mode 100644 src/compile.demo.js create mode 100644 src/currency.formatters.js create mode 100644 src/datetime.formatters.js create mode 100644 test/babel.test.js delete mode 100644 test/compile.test.js diff --git a/demoapps/app/a/a1.js b/demo/apps/app/a/a1.js similarity index 100% rename from demoapps/app/a/a1.js rename to demo/apps/app/a/a1.js diff --git a/demoapps/app/a/a2.js b/demo/apps/app/a/a2.js similarity index 100% rename from demoapps/app/a/a2.js rename to demo/apps/app/a/a2.js diff --git a/demoapps/app/b/b1.js b/demo/apps/app/b/b1.js similarity index 100% rename from demoapps/app/b/b1.js rename to demo/apps/app/b/b1.js diff --git a/demoapps/app/c/c1.js b/demo/apps/app/c/c1.js similarity index 100% rename from demoapps/app/c/c1.js rename to demo/apps/app/c/c1.js diff --git a/demoapps/app/c/c2.js b/demo/apps/app/c/c2.js similarity index 100% rename from demoapps/app/c/c2.js rename to demo/apps/app/c/c2.js diff --git a/demoapps/app/c/h1.html b/demo/apps/app/c/h1.html similarity index 100% rename from demoapps/app/c/h1.html rename to demo/apps/app/c/h1.html diff --git a/demoapps/app/index.js b/demo/apps/app/index.js similarity index 100% rename from demoapps/app/index.js rename to demo/apps/app/index.js diff --git a/demoapps/app/languages/cn.js b/demo/apps/app/languages/cn.js similarity index 100% rename from demoapps/app/languages/cn.js rename to demo/apps/app/languages/cn.js diff --git a/demoapps/app/languages/de.js b/demo/apps/app/languages/de.js similarity index 100% rename from demoapps/app/languages/de.js rename to demo/apps/app/languages/de.js diff --git a/demoapps/app/languages/en.js b/demo/apps/app/languages/en.js similarity index 100% rename from demoapps/app/languages/en.js rename to demo/apps/app/languages/en.js diff --git a/demoapps/app/languages/idMap.js b/demo/apps/app/languages/idMap.js similarity index 95% rename from demoapps/app/languages/idMap.js rename to demo/apps/app/languages/idMap.js index 1ab9b16..d139cfc 100644 --- a/demoapps/app/languages/idMap.js +++ b/demo/apps/app/languages/idMap.js @@ -1,4 +1,4 @@ -module.exports = { +export default { "a1:aaaaa": 1, "no aaaaa": 2, "bbbbb": 3, diff --git a/demoapps/app/languages/index.js b/demo/apps/app/languages/index.js similarity index 100% rename from demoapps/app/languages/index.js rename to demo/apps/app/languages/index.js diff --git a/demoapps/app/languages/jp.js b/demo/apps/app/languages/jp.js similarity index 100% rename from demoapps/app/languages/jp.js rename to demo/apps/app/languages/jp.js diff --git a/demoapps/app/languages/package.json b/demo/apps/app/languages/package.json similarity index 100% rename from demoapps/app/languages/package.json rename to demo/apps/app/languages/package.json diff --git a/demoapps/app/languages/settings.js b/demo/apps/app/languages/settings.js similarity index 100% rename from demoapps/app/languages/settings.js rename to demo/apps/app/languages/settings.js diff --git a/demoapps/app/languages/translates/a.json b/demo/apps/app/languages/translates/a.json similarity index 100% rename from demoapps/app/languages/translates/a.json rename to demo/apps/app/languages/translates/a.json diff --git a/demoapps/app/languages/translates/b.json b/demo/apps/app/languages/translates/b.json similarity index 100% rename from demoapps/app/languages/translates/b.json rename to demo/apps/app/languages/translates/b.json diff --git a/demoapps/app/languages/translates/default.json b/demo/apps/app/languages/translates/default.json similarity index 100% rename from demoapps/app/languages/translates/default.json rename to demo/apps/app/languages/translates/default.json diff --git a/demoapps/app/languages/translates/x.json b/demo/apps/app/languages/translates/x.json similarity index 100% rename from demoapps/app/languages/translates/x.json rename to demo/apps/app/languages/translates/x.json diff --git a/demo/apps/lib1/languages/idMap.js b/demo/apps/lib1/languages/idMap.js new file mode 100644 index 0000000..5adaca2 --- /dev/null +++ b/demo/apps/lib1/languages/idMap.js @@ -0,0 +1,7 @@ +export default { + "a":1, + "b":2, + "c{}{}":3, + "d{a}{b}":4, + "e":5 +} \ No newline at end of file diff --git a/demo/apps/lib2/languages/idMap.js b/demo/apps/lib2/languages/idMap.js new file mode 100644 index 0000000..3053d9d --- /dev/null +++ b/demo/apps/lib2/languages/idMap.js @@ -0,0 +1,7 @@ +module.exports = { + "a":1, + "b":2, + "c{}{}":3, + "d{a}{b}":4, + "e":5 +} \ No newline at end of file diff --git a/src/babel.plugin.demo.js b/demo/babel.plugin.demo.js similarity index 73% rename from src/babel.plugin.demo.js rename to demo/babel.plugin.demo.js index 56c2002..bae036d 100644 --- a/src/babel.plugin.demo.js +++ b/demo/babel.plugin.demo.js @@ -1,10 +1,10 @@ const babel = require("@babel/core"); const fs = require("fs"); const path = require("path"); -const i18nPlugin = require("./babel-plugin-voerkai18n"); +const i18nPlugin = require("../src/babel-plugin-voerkai18n"); -const code = fs.readFileSync(path.join(__dirname, "../demodata/index.js"), "utf-8"); +const code = fs.readFileSync(path.join(__dirname, "./apps/app/index.js"), "utf-8"); babel.transform(code, { plugins: [ [ diff --git a/demo/compile.demo.js b/demo/compile.demo.js new file mode 100644 index 0000000..2a0ca85 --- /dev/null +++ b/demo/compile.demo.js @@ -0,0 +1,7 @@ + + +const compile = require('../src/compile'); +const path = require("path") + + +compile(path.resolve(__dirname,"./apps/app/languages")) \ No newline at end of file diff --git a/src/extract.demo.js b/demo/extract.demo.js similarity index 84% rename from src/extract.demo.js rename to demo/extract.demo.js index b05a605..22e9a23 100644 --- a/src/extract.demo.js +++ b/demo/extract.demo.js @@ -1,9 +1,9 @@ const gulp = require('gulp'); -const extract = require('./extract.plugin'); +const extract = require('../src/extract.plugin'); const path = require('path'); -const soucePath = path.join(__dirname,'../demoapps/app') +const soucePath = path.join(__dirname,'./apps/app') @@ -13,7 +13,7 @@ gulp.src([ ]).pipe(extract({ debug:true, // output: path.join(soucePath , 'languages'), - languages: [{name:'en',title:"英文"},{name:'cn',title:"中文",default:true},{name:'de',title:"德语"},{name:'jp',title:"日本語"}], + languages: [{name:'en',title:"英文"},{name:'cn',title:"中文",default:true},{name:'de',title:"德语"},{name:'jp',title:"日語"}], // extractor:{ // default:[new RegExp()], // 默认匹配器,当文件类型没有对应的提取器时使用 // "*" : [new RegExp()], // 所有类型均会执行的提取器 @@ -26,4 +26,4 @@ gulp.src([ "b":"b", } })) -.pipe(gulp.dest(path.join(__dirname,'../demoapps/app/languages'))); +.pipe(gulp.dest(path.join(__dirname,'./apps/app/languages'))); diff --git a/src/babel-plugin-voerkai18n.js b/src/babel-plugin-voerkai18n.js index 6487dd1..eeb24f1 100644 --- a/src/babel-plugin-voerkai18n.js +++ b/src/babel-plugin-voerkai18n.js @@ -17,13 +17,26 @@ * */ -const { getMessageId } = require('./utils'); -const TRANSLATE_FUNCTION_NAME = "t" const fs = require("fs"); - +const path = require("path"); +const { isPlainObject } = require("./utils"); + const DefaultI18nPluginOptions = { translateFunctionName:"t", // 默认的翻译函数名称 - location:"./languages" // 默认的翻译文件存放的目录,即编译后的语言文件的文件夹 + // 翻译文件存放的目录,即编译后的语言文件的文件夹 + // 默认从当前目录下的languages文件夹中导入 + // 如果不存在则会 + location:"./languages", + // 自动创建import {t} from "#/languages" 或 const { t } = require("#/languages") + // 如果此值是空,则不会自动创建import语句 + autoImport:"#/languages", + // 自动导入时t函数时使用require或import,取值为 auto,require,import + // auto时会判断是否存在import语句,如果存在则使用import,否则使用require + // 也可以指定为require或import,主要用于测试时使用 + moduleType:"auto", + // 存放翻译函数的id和翻译内容的映射关系,此参数用于测试使用 + // 正常情况下会读取/idMap.js文件 + idMap:{} } /** @@ -66,39 +79,81 @@ function hasRequireTranslateFunction(path){ } } } + +function getIdMap(options){ + const { idMap,location } = options + if(isPlainObject(idMap) && Object.keys(idMap).length>0){ + return idMap + }else{ + let idMapFiles = [ + path.join((path.isAbsolute(location) ? location : path.join(process.cwd(),location)),"idMap.js"), + path.join((path.isAbsolute(location) ? path.join(location,"languages") : path.join(process.cwd(),location,"languages")),'idMap.js') + ] + let idMapFile + for( idMapFile of idMapFiles){ + // 如果不存在idMap文件,则尝试从location/languages/中导入 + if(fs.existsSync(idMapFile)){ + try{ + // 由于idMap.js可能是esm或cjs,并且babel插件不支持异步 + // 当require(idMap.js)失败时,对esm模块尝试采用直接读取的方式 + return require(idMapFile) + }catch(e){ + // 出错原因可能是因为无效require esm模块, + // 由于idMap.js文件格式相对简单,因此尝试直接读取解析 + try{ + let idMapContent = fs.readFileSync(idMapFile).toString() + idMapContent = idMapContent.trim().replace(/^\s*export\s*default\s/g,"") + return JSON.parse(idMapContent) + }catch{ } + } + } + } + // 所有尝试完成后触发错误 + throw new Error(`${idMapFile}文件不存在,无法对翻译文本进行转换。\n原因可能是babel-plugin-voerkai18n插件的location参数未指向有效的语言包所在的目录。`) + } +} + module.exports = function voerkai18nPlugin(babel) { const t = babel.types; const pluginOptions = Object.assign({},DefaultI18nPluginOptions); + let idMap = {} return { visitor:{ Program(path, state){ + // 转码插件参数可以覆盖默认参数 Object.assign(pluginOptions,state.opts || {}); - const { location = "./languages", translateFunctionName } = pluginOptions - if(isEsModule(path)){ - // 如果没有定义t函数,则自动导入 - if(!hasImportTranslateFunction(path)){ - path.node.body.unshift(t.importDeclaration([ - t.ImportSpecifier(t.identifier(translateFunctionName),t.identifier(translateFunctionName) - )],t.stringLiteral(location))) - } - }else{ - if(!hasRequireTranslateFunction(path)){ - path.node.body.unshift(t.variableDeclaration("const",[ - t.variableDeclarator( - t.ObjectPattern([t.ObjectProperty(t.Identifier(translateFunctionName),t.Identifier(translateFunctionName),false,true)]), - t.CallExpression(t.Identifier("require"),[t.stringLiteral(location)]) - ) - ])) - } - } + const { location ,autoImport, translateFunctionName,moduleType } = pluginOptions + idMap = getIdMap(pluginOptions) + // 是否自动导入t函数 + if(autoImport){ + let module = moduleType === 'auto' ? isEsModule(path) ? 'esm' : 'cjs' : moduleType + if(!["esm","es","cjs","commonjs"].includes(module)) module = 'esm' + if(module === 'esm'){ + // 如果没有定义t函数,则自动导入 + if(!hasImportTranslateFunction(path)){ + path.node.body.unshift(t.importDeclaration([ + t.ImportSpecifier(t.identifier(translateFunctionName),t.identifier(translateFunctionName) + )],t.stringLiteral(autoImport))) + } + }else{ + if(!hasRequireTranslateFunction(path)){ + path.node.body.unshift(t.variableDeclaration("const",[ + t.variableDeclarator( + t.ObjectPattern([t.ObjectProperty(t.Identifier(translateFunctionName),t.Identifier(translateFunctionName),false,true)]), + t.CallExpression(t.Identifier("require"),[t.stringLiteral(autoImport)]) + ) + ])) + } + } + } }, - CallExpression(path,state){ - let options = state.opts - // 只对翻译函数进行转码 - if(path.node.callee.name===TRANSLATE_FUNCTION_NAME){ + // 将t函数的第一个参数转换为id + CallExpression(path,state){ + if( path.node.callee.name === pluginOptions.translateFunctionName ){ if(path.node.arguments.length>0 && t.isStringLiteral(path.node.arguments[0])){ - let text = path.node.arguments[0].value - path.node.arguments[0] = t.stringLiteral(`*${text}*`) + let message = path.node.arguments[0].value + const msgId =(message in idMap) ? idMap[message] : message + path.node.arguments[0] = t.stringLiteral(String(msgId)) } }else{ path.skip(); diff --git a/src/compile.demo.js b/src/compile.demo.js deleted file mode 100644 index 01f1934..0000000 --- a/src/compile.demo.js +++ /dev/null @@ -1,7 +0,0 @@ - - -const compile = require('./compile'); -const path = require("path") - - -compile(path.resolve(__dirname,"../demoapps/app/languages")) \ No newline at end of file diff --git a/src/compile.js b/src/compile.js index d3d2f29..31f90ef 100644 --- a/src/compile.js +++ b/src/compile.js @@ -6,32 +6,19 @@ * 编译原理如下: * * - * 编译输出: - * - * hashId = getMessageId() - * - * - languages/index.js 主源码,用来引用语言文件 - * { - * languages:{}, - - * } - * - languages/messageIds.json 翻译文本的id映射表 - * { - * [msg]:"" - * } - * - languages/en.js 英文语言文件 - * { - * [region]:{ - * [namespace]:{ - * [hashId]:"", - * }, - * [namespace]:{...}, - * }, - * [region]:{...} - * } - * - * - languages/[lang].js 语言文件 - * - formaters.js + * 编译后会在目标文件夹输出: + * + * - languages + * translates + * - en.json + * - cn.json + * - ... + * idMap.js // id映射列表 + * settings.js // 配置文件 + * cn.js // 中文语言包 + * en.js // 英文语言包 + * [lang].js // 其他语言包 + * package.json // 包信息,用来指定包类型,以便在nodejs中能够正确加载 * * @param {*} opts */ @@ -52,8 +39,8 @@ function normalizeCompileOptions(opts={}) { moduleType:"esm" // 指定编译后的语言文件的模块类型,取值common,cjs,esm,es }, opts) if(options.moduleType==="es") options.moduleType = "esm" - if(options.moduleType==="cjs") options.moduleType = "common" - if(["common","cjs","esm","es"].includes(options.moduleType)) options.moduleType = "esm" + if(options.moduleType==="cjs") options.moduleType = "commonjs" + if(["commonjs","cjs","esm","es"].includes(options.moduleType)) options.moduleType = "esm" return opts; } @@ -122,5 +109,17 @@ module.exports = function compile(langFolder,opts={}){ const formattersContent = artTemplate(path.join(__dirname,"templates","formatters.js"), {languages,defaultLanguage,activeLanguage,namespaces,moduleType } ) fs.writeFileSync(formattersFile,formattersContent) } + // 7. 生成package.json + const packageJsonFile = path.join(langFolder,"package.json") + let packageJson = {} + if(moduleType==="esm"){ + packageJson = { + type:"module", + } + }else{ + packageJson = { + } + } + fs.writeFileSync(packageJsonFile,JSON.stringify(packageJson,null,4)) }) } \ No newline at end of file diff --git a/src/currency.formatters.js b/src/currency.formatters.js new file mode 100644 index 0000000..05e54d8 --- /dev/null +++ b/src/currency.formatters.js @@ -0,0 +1,24 @@ +/** + * + * 提供处理货币格式化的功能 + * + * import './languages'; + * import "voerka-i18n/formatters/currency"; // 货币格式化 + * + * + */ + + +if(globalThis.VoerkaI18n){ + + VoerkaI18n.registerFormatters({ + { + currency + } + + }) + +} + + + diff --git a/src/datetime.formatters.js b/src/datetime.formatters.js new file mode 100644 index 0000000..b42e50e --- /dev/null +++ b/src/datetime.formatters.js @@ -0,0 +1,28 @@ +/** + * + * 提供日期时间格式化的功能 + * + * import './languages'; + * import "voerka-i18n/formatters/datetime"; // 货币格式化 + * + * + */ + + +if(globalThis.VoerkaI18n){ + + VoerkaI18n.registerFormatters({ + "*":{ + + }, + cn:{ + + }, + en:{ + + } + }) +} + + + diff --git a/src/formatters.js b/src/formatters.js index 192bdb0..13de931 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -1,38 +1,26 @@ /** - * 默认的格式化器 - * - * 使用方法: - * - * { - * "My birthday is {date}":{ - * "zh-CN":"我的生日是{date|time}" - * } - * } - * - * t("My birthday is {date}",new Date(1975,11,25)) - * + * 内置的格式化器 * */ -const dayjs = require("dayjs"); -module.exports = { - cn:{ - "*":{ // 适用于所有类型的格式化器 - default:null // 默认格式化器 + +module.exports = { + "*":{ + $types:{ + Date:(value)=>value.toLocaleString() }, - Date:{ - default:(value)=>dayjs(value).format("YYYY年MM年DD日"), // 默认的格式化器 - time:(value)=>dayjs(value).format("HH:mm:ss"), - date:(value)=>dayjs(value).format("YYYY/MM/DD") + time:(value)=> value.toLocaleTimeString(), + date: (value)=> value.toLocaleDateString(), + }, + cn:{ + $types:{ + Date:(value)=> `${value.getFullYear()}年${value.getMonth()+1}月${value.getDate()}日 ${value.getHours()}点${value.getMinutes()}分${value.getSeconds()}秒` }, - Number:{ - - } + time:(value)=>`${value.getHours()}点${value.getMinutes()}分${value.getSeconds()}秒`, + date: (value)=> `${value.getFullYear()}年${value.getMonth()+1}月${value.getDate()}日`, + currency:(value)=>`${value}元`, }, en:{ - "Date":{ - short:(value)=>dayjs(value).format("YYYY/MM/DD"), - time:(value)=>dayjs(value).format("HH:mm:ss") - } + currency:(value)=>`$${value}`, } } \ No newline at end of file diff --git a/src/index.js b/src/index.js index e29c9a0..9ebb075 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ const deepMerge = require("deepmerge") const formatters = require("./formatters") - +const {isPlainObject ,isNumber , getDataTypeName} = require("./utils") // 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter } // 不支持参数: let varWithPipeRegexp = /\{\s*(?\w+)?(?(\s*\|\s*\w*\s*)*)\s*\}/g @@ -8,6 +8,8 @@ const formatters = require("./formatters") // 支持参数: { var | formatter(x,x,..) | formatter } let varWithPipeRegexp = /\{\s*(?\w+)?(?(\s*\|\s*\w*(\(.*\)){0,1}\s*)*)\s*\}/g +// 有效的语言名称列表 +const languages = ["af","am","ar-dz","ar-iq","ar-kw","ar-ly","ar-ma","ar-sa","ar-tn","ar","az","be","bg","bi","bm","bn","bo","br","bs","ca","cs","cv","cy","da","de-at","de-ch","de","dv","el","en-au","en-ca","en-gb","en-ie","en-il","en-in","en-nz","en-sg","en-tt","en","eo","es-do","es-mx","es-pr","es-us","es","et","eu","fa","fi","fo","fr-ca","fr-ch","fr","fy","ga","gd","gl","gom-latn","gu","he","hi","hr","ht","hu","hy-am","id","is","it-ch","it","ja","jv","ka","kk","km","kn","ko","ku","ky","lb","lo","lt","lv","me","mi","mk","ml","mn","mr","ms-my","ms","mt","my","nb","ne","nl-be","nl","nn","oc-lnc","pa-in","pl","pt-br","pt","ro","ru","rw","sd","se","si","sk","sl","sq","sr-cyrl","sr","ss","sv-fi","sv","sw","ta","te","tet","tg","th","tk","tl-ph","tlh","tr","tzl","tzm-latn","tzm","ug-cn","uk","ur","uz-latn","uz","vi","x-pseudo","yo","zh-cn","zh-hk","zh-tw","zh"] // 插值变量字符串替换正则 @@ -28,25 +30,7 @@ let varReplaceRegexp =String.raw`\{\s*{varname}\s*\}` */ function hasInterpolation(str){ return str.includes("{") && str.includes("}") -} -/** - * 获取指定变量类型名称 - * getDataTypeName(1) == Number - * getDataTypeName("") == String - * getDataTypeName(null) == Null - * getDataTypeName(undefined) == Undefined - * getDataTypeName(new Date()) == Date - * getDataTypeName(new Error()) == Error - * - * @param {*} v - * @returns - */ -function getDataTypeName(v){ - if (v === null) return 'Null' - if (v === undefined) return 'Undefined' - if(typeof(v)==="function") return "Function" - return v.constructor && v.constructor.name; -}; +} /** @@ -129,15 +113,26 @@ function getInterpolatedVars(str){ * @param {*} callback * @returns */ -function forEachInterpolatedVars(str,callback){ +function forEachInterpolatedVars(str,callback,options={}){ let result=str, match + let opts = Object.assign({ + replaceAll:true, // 是否替换所有插值变量,当使用命名插值时应置为true,当使用位置插值时应置为false + },options) varWithPipeRegexp.lastIndex=0 while ((match = varWithPipeRegexp.exec(result)) !== null) { const varname = match.groups.varname || "" // 解析格式化器和参数 = [,[,[,,...]]] const formatters = parseFormatters(match.groups.formatters) if(typeof(callback)==="function"){ - result=result.replaceAll(match[0],callback(varname,formatters)) + try{ + if(opts.replaceAll){ + result=result.replaceAll(match[0],callback(varname,formatters)) + }else{ + result=result.replace(match[0],callback(varname,formatters)) + } + }catch{// callback函数可能会抛出异常,如果抛出异常,则中断匹配过程 + break + } } varWithPipeRegexp.lastIndex=0 } @@ -156,7 +151,7 @@ function transformToString(value){ let result = value if(typeof(result)==="function") result = value() if(!(typeof(result)==="string")){ - if(Array.isArray(result) || typeof(result)==="object"){ + if(Array.isArray(result) || isPlainObject(result)){ result = JSON.stringify(result) }else{ result = result.toString() @@ -304,6 +299,28 @@ function buildFormatters(scope,activeLanguage,formatters){ } return results } + +/** + * 将value经过格式化器处理后返回 + * @param {*} scope + * @param {*} activeLanguage + * @param {*} formatters + * @param {*} value + * @returns + */ +function getFormattedValue(scope,activeLanguage,formatters,value){ + // 1. 取得格式化器函数列表 + const formatterFuncs = buildFormatters(scope,activeLanguage,formatters) + // 3. 查找每种数据类型默认格式化器,并添加到formatters最前面,默认数据类型格式化器优先级最高 + const defaultFormatter = getDataTypeDefaultFormatter(scope,activeLanguage,getDataTypeName(value)) + if(defaultFormatter){ + formatterFuncs.splice(0,0,defaultFormatter) + } + // 3. 执行格式化器 + value = executeFormatter(value,formatterFuncs) + return value +} + /** * 字符串可以进行变量插值替换, * replaceInterpolatedVars("<模板字符串>",{变量名称:变量值,变量名称:变量值,...}) @@ -326,52 +343,33 @@ function replaceInterpolatedVars(template,...args) { // 当前激活语言 const activeLanguage = scope.global.activeLanguage let result=template - if(!hasInterpolation(template)) return template + + // 没有变量插值则的返回原字符串 + if(args.length===0 || !hasInterpolation(template)) return template + // ****************************变量插值**************************** - if(args.length===1 && typeof(args[0]) === "object" && !Array.isArray(args[0])){ + if(args.length===1 && isPlainObject(args[0])){ // 读取模板字符串中的插值变量列表 // [[var1,[formatter,formatter,...],match],[var2,[formatter,formatter,...],match],...} let varValues = args[0] return forEachInterpolatedVars(template,(varname,formatters)=>{ - // 1. 取得格式化器函数列表 - const formatterFuncs = buildFormatters(scope,activeLanguage,formatters) - // 2. 取变量值 let value = (varname in varValues) ? varValues[varname] : '' - // 3. 查找每种数据类型默认格式化器,并添加到formatters最前面,默认数据类型格式化器优先级最高 - const defaultFormatter = getDataTypeDefaultFormatter(scope,activeLanguage,getDataTypeName(value)) - if(defaultFormatter){ - formatterFuncs.splice(0,0,defaultFormatter) - } - // 4. 执行格式化器 - value = executeFormatter(value,formatterFuncs) - return value + return getFormattedValue(scope,activeLanguage,formatters,value) }) }else{ // ****************************位置插值**************************** // 如果只有一个Array参数,则认为是位置变量列表,进行展开 const params=(args.length===1 && Array.isArray(args[0])) ? [...args[0]] : args - - // 取得模板字符串中的插值变量列表 , 包含命令变量和位置变量 - let interpVars = getInterpolatedVars(template,true) - if(interpVars.length===0) return template // 没有变量插值则的返回原字符串 - - let i=0 - for(let match of result.match(varWithPipeRegexp) || []){ - if(i{ + if(params.length>i){ + return getFormattedValue(scope,activeLanguage,formatters,params[i++]) }else{ - break + throw new Error() // 抛出异常,停止插值处理 } - } + },{replaceAll:false}) + } return result } @@ -390,16 +388,31 @@ const defaultLanguageSettings = { function isMessageId(content){ return parseInt(content)>0 } +/** + * 根据值的单数和复数形式,从messages中取得相应的消息 + * + * @param {*} messages 复数形式的文本内容 = [<=0时的内容>,<=1时的内容>,<=2时的内容>,...] + * @param {*} value + */ +function getPluraMessage(messages,value){ + try{ + if(Array.isArray(messages)){ + return messages.length > value ? messages[value] : messages[messages.length-1] + }else{ + return messages + } + }catch{ + return Array.isArray(messages) ? messages[0] : messages + } +} + /** * 翻译函数 * * translate("要翻译的文本内容") 如果默认语言是中文,则不会进行翻译直接返回 * translate("I am {} {}","man") == I am man 位置插值 * translate("I am {p}",{p:"man"}) 字典插值 -* translate("I am {p}",{p:"man",ns:""}) 指定名称空间 -* translate("I am {p}",{p:"man",namespace:""}) -* translate("I am {p}",{p:"man",namespace:""}) -* translate("total {count} items", {count:1}) //复数形式 +* translate("total {$count} items", {$count:1}) //复数形式 * translate("total {} {} {} items",a,b,c) // 位置变量插值 * * this===scope 当前绑定的scope @@ -407,14 +420,15 @@ function isMessageId(content){ */ function translate(message) { const scope = this - const activeLanguage = scope.settings.activeLanguage - let vars={} // 插值变量 - let pluralVars=[] // 复数变量 + const activeLanguage = scope.global.activeLanguage + let content = message + let vars=[] // 插值变量列表 + let pluralVars= [] // 复数变量 + let pluraValue = null // 复数值 try{ // 1. 预处理变量: 复数变量保存至pluralVars中 , 变量如果是Function则调用 - if(arguments.length === 2 && typeof(arguments[1])=='object'){ - Object.assign(vars,arguments[1]) - Object.entries(vars).forEach(([name,value])=>{ + if(arguments.length === 2 && isPlainObject(arguments[1])){ + Object.entries(arguments[1]).forEach(([name,value])=>{ if(typeof(value)==="function"){ try{ vars[name] = value() @@ -422,41 +436,64 @@ function translate(message) { vars[name] = value } } - // 复数变量 - if(name.startsWith("$")) pluralVars.push(name) + // 以$开头的视为复数变量 + if(name.startsWith("$")) pluralVars.push(name) }) + vars = [arguments[1]] }else if(arguments.length >= 2){ - vars = [...arguments].splice(1).map(arg=>typeof(arg)==="function" ? arg() : arg) - } - - // 默认语言,不需要查询加载,只需要做插值变换即可 + vars = [...arguments].splice(1).map((arg,index)=>{ + try{ + return typeof(arg)==="function" ? arg() : arg + }catch(e){ + return arg + } + // 位置参数中以第一个数值变量为复数变量 + if(isNumber(arg)) pluraValue = parseInt(arg) + }) + + } + + // 2. 取得翻译文本模板字符串 if(activeLanguage === scope.defaultLanguage){ + // 2.1 从默认语言中取得翻译文本模板字符串 + // 如果当前语言就是默认语言,不需要查询加载,只需要做插值变换即可 // 当源文件运用了babel插件后会将原始文本内容转换为msgId - if(isMessageId(message)){ - message = scope.default[message] || message + // 如果是msgId则从scope.default中读取,scope.default=默认语言包={:} + if(isMessageId(content)){ + content = scope.default[content] || message } - return replaceInterpolatedVars.call(scope,message,vars) }else{ - // 1. 获取翻译后的文本内容 - // 如果没有启用babel插件时,需要先将文本内容转换为msgId - let msgId = isMessageId(message) ? message : scope.idMap[message] - message = scope.messages[msgId] || msgId - - // 处理复数 - if(Array.isArray(message)){ - - }else{ // 普通 - - } - + // 2.2 从当前语言包中取得翻译文本模板字符串 + // 如果没有启用babel插件将源文本转换为msgId,需要先将文本内容转换为msgId + let msgId = isMessageId(content) ? content : scope.idMap[content] + content = scope.messages[msgId] || content } + + // 3. 处理复数 + // 经过上面的处理,content可能是字符串或者数组 + // content = "原始文本内容" || 复数形式["原始文本内容","原始文本内容"....] + // 如果是数组说明要启用复数机制,需要根据插值变量中的某个变量来判断复数形式 + if(Array.isArray(content) && content.length>0){ + // 如果存在复数命名变量,只取第一个复数变量 + if(pluraValue){ // 启用的是位置插值,pluraIndex=第一个数字变量的位置 + content = getPluraMessage(content,pluraValue) + }else if(pluralVar.length>0){ + content = getPluraMessage(content,parseInt(vars(pluralVar[0]))) + }else{ // 如果找不到复数变量,则使用第一个内容 + content = content[0] + } + } + // 进行插值处理 + if(vars.length==0){ + return content + }else{ + return replaceInterpolatedVars.call(scope,content,...vars) + } }catch(e){ - return message + return content // 出错则返回原始文本 } } - - - + /** * 多语言管理类 * @@ -587,13 +624,20 @@ function translate(message) { scope.global = this._settings this._scopes.push(scope) } + /** + * 注册全局格式化器 + * @param {*} formatters + */ + registerFormatters(formatters){ + + } } - module.exports ={ getInterpolatedVars, replaceInterpolatedVars, I18n, translate, + languages, defaultLanguageSettings } \ No newline at end of file diff --git a/src/templates/entry.js b/src/templates/entry.js index 8ab8a1e..a1c7a85 100644 --- a/src/templates/entry.js +++ b/src/templates/entry.js @@ -2,19 +2,19 @@ import messageIds from "./idMap.js" import { translate,i18n } from "voerka-i18n" import defaultMessages from "./{{defaultLanguage}}.js" -import i18nSettings from "./settings.js" +import scopeSettings from "./settings.js" import formatters from "../formatters" {{else}} const messageIds = require("./idMap") const { translate,i18n } = require("voerka-i18n") const defaultMessages = require("./{{defaultLanguage}}.js") -const i18nSettings = require("./settings.js") +const scopeSettings = require("./settings.js") const formatters = require("../formatters") {{/if}} // 自动创建全局VoerkaI18n实例 if(!globalThis.VoerkaI18n){ - globalThis.VoerkaI18n = new i18n(i18nSettings) + globalThis.VoerkaI18n = new i18n(scopeSettings) } let scope = { diff --git a/src/templates/formatters.js b/src/templates/formatters.js index 99c8995..780dbde 100644 --- a/src/templates/formatters.js +++ b/src/templates/formatters.js @@ -7,7 +7,8 @@ import dayjs from "dayjs"; const formatters = { "*":{ // 在所有语言下生效的格式化器 - $types:{...} // 只作用于特定数据类型的默认格式化器 + $types:{...}, // 只作用于特定数据类型的默认格式化器 + .... // 全局格式化器 }, cn:{ // 只作用于特定数据类型的格式化器 diff --git a/src/utils.js b/src/utils.js index 13d1d7f..e81afd5 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,25 +1,4 @@ - - -// 用来提取字符里面的插值变量参数 -// let varRegexp = /\{\s*(?\w*\.?\w*)\s*\}/g -let varRegexp = /\{\s*((?\w+)?(\s*\|\s*(?\w*))?)?\s*\}/g -// 插值变量字符串替换正则 -//let varReplaceRegexp =String.raw`\{\s*(?{name}\.?\w*)\s*\}` - - -let varReplaceRegexp =String.raw`\{\s*{varname}\s*\}` - -/** - * 考虑到通过正则表达式进行插件的替换可能较慢,因此提供一个简单方法来过滤掉那些 - * 不需要进行插值处理的字符串 - * 原理很简单,就是判断一下是否同时具有{、}字符 - * 注意:该方法只能快速判断一个字符串不包括插值变量 - * @param {*} str - * @returns {boolean} true=可能包含插值变量, - */ -function hasInterpolation(str){ - return str.includes("{") && str.includes("}") -} + /** * 获取指定变量类型名称 * getDataTypeName(1) == Number @@ -38,108 +17,46 @@ function getDataTypeName(v){ if(typeof(v)==="function") return "Function" return v.constructor && v.constructor.name; }; +function isPlainObject(obj){ + if (typeof obj !== 'object' || obj === null) return false; + var proto = Object.getPrototypeOf(obj); + if (proto === null) return true; + var baseProto = proto; + + while (Object.getPrototypeOf(baseProto) !== null) { + baseProto = Object.getPrototypeOf(baseProto); + } + return proto === baseProto; +} +function isNumber(value){ + return !isNaN(parseInt(value)) +} + /** - * 生成一个基于时间戳的文本id - * performance.now()返回的是当前node进程从启动到现在时间戳 - * 但是连续调用可能会导致重复id的生成,如果发现重复则会 - * 在生成的id后面加上一个随机数,以保证id的唯一性 + * 支持导入cjs和esm模块 + * @param {*} url */ -function getMessageId(){ - this.lastMsgId = null - let id = String(performance.now()).replace(".","") - if(this.lastMsgId === id){ - id = id + parseInt(Math.random() * 1000) .toString() - }else{ - this.lastMsgId = id - } - return id -} -/** - * 提取字符串中的插值变量 - * @param {*} str - * @returns {Array} 变量名称列表 - */ -function getInterpolatedVars(str){ - let result = [] - let match - while ((match = varRegexp.exec(str)) !== null) { - if (match.index === varRegexp.lastIndex) { - varRegexp.lastIndex++; - } - if(match.groups.varname) { - result.push(match.groups.formatter ? match.groups.varname+"|"+match.groups.formatter : match.groups.varname) - } - } - return result -} - -function transformVarValue(value){ - let result = value - if(typeof(result)==="function") result = value() - if(!(typeof(result)==="string")){ - if(Array.isArray(result) || typeof(result)==="object"){ - result = JSON.stringify(result) +async function importModule(url){ + try{ + return require(url) + }catch(e){ + // 当加载出错时,尝试加载esm模块 + if(e.code === "MODULE_NOT_FOUND"){ + return await import(url) }else{ - result = result.toString() + throw e } - } - return result + } } - /** - * 字符串可以进行变量插值替换, - - 当只有两个参数并且第2个参数是{}时,将第2个参数视为命名变量的字典 - replaceVars("this is {a}+{b},{a:1,b:2}) --> this is 1+2 - - 当只有两个参数并且第2个参数是[]时,将第2个参数视为位置参数 - replaceVars"this is {}+{}",[1,2]) --> this is 1+2 - - 普通位置参数替换 - replaceVars("this is {a}+{b}",1,2) --> this is 1+2 - - - - * @param {*} template - * @returns - */ -function replaceInterpolateVars(template,...args) { - let result=template - if(!hasInterpolation(template)) return - if(args.length===1 && typeof(args[0]) === "object" && !Array.isArray(args[0])){ // 变量插值 - for(let name in args[0]){ - // 如果变量中包括|管道符,则需要进行转换以适配更宽松的写法,比如data|time能匹配"data |time","data | time"等 - let nameRegexp = name.includes("|") ? name.split("|").join("\\s*\\|\\s*") : name - result=result.replaceAll(new RegExp(varReplaceRegexp.replaceAll("{varname}",nameRegexp),"g"),transformVarValue(args[0][name])) - } - }else{ // 位置插值 - const params=(args.length===1 && Array.isArray(args[0])) ? [...args[0]] : args - let i=0 - for(let match of result.match(varRegexp) || []){ - if(i"bob")) -// console.log(replaceInterpolateVars(str,"tom",[1,2],{a:1},1,2)) - module.exports = { - getMessageId, - hasInterpolation, - getInterpolatedVars, - replaceInterpolateVars + getDataTypeName, + isNumber, + isPlainObject, + importModule } diff --git a/test/babel.test.js b/test/babel.test.js new file mode 100644 index 0000000..a87d5ee --- /dev/null +++ b/test/babel.test.js @@ -0,0 +1,83 @@ +const babel = require("@babel/core"); +const fs = require("fs"); +const path = require("path"); +const i18nPlugin = require("../src/babel-plugin-voerkai18n"); + +const code = ` +function test(a,b){ + t("a") + t('b') + t('c{}{}',1,2) + t('d{a}{b}',{a:1,b:2}), + t('e',()=>{}) +}` + + +function expectBabelSuccess(result){ + expect(result.includes(`"#/languages"`)).toBeTruthy() + expect(result.includes(`t("1"`)).toBeTruthy() + expect(result.includes(`t("2"`)).toBeTruthy() + expect(result.includes(`t("3"`)).toBeTruthy() + expect(result.includes(`t("4"`)).toBeTruthy() + expect(result.includes(`t("5"`)).toBeTruthy() +} +test("翻译函数转换",done=>{ + babel.transform(code, { + plugins: [ + [ + i18nPlugin, + { + // location:"", + // 指定语言文件存放的目录,即保存编译后的语言文件的文件夹 + // 可以指定相对路径,也可以指定绝对路径 + autoImport:"#/languages", + moduleType:"esm", + // 此参数仅仅用于单元测试时使用,正常情况下,会读取location文件夹下的idMap", idMap:{ + "a":1, + "b":2, + "c{}{}":3, + "d{a}{b}":4, + "e":5 + } + } + ] + ] + }, function(err, result) { + expectBabelSuccess(result.code) + done() + }); +}) +test("读取esm格式的idMap后进行翻译函数转换",done=>{ + babel.transform(code, { + plugins: [ + [ + i18nPlugin, + { + location:path.join(__dirname, "../demo/apps/lib1/languages"), + autoImport:"#/languages", + moduleType:"esm", + } + ] + ] + }, function(err, result) { + expectBabelSuccess(result.code) + done() + }); +}) +test("读取commonjs格式的idMap后进行翻译函数转换",done=>{ + babel.transform(code, { + plugins: [ + [ + i18nPlugin, + { + location:path.join(__dirname, "../demo/apps/lib2/languages"), + autoImport:"#/languages", + moduleType:"esm", + } + ] + ] + }, function(err, result) { + expectBabelSuccess(result.code) + done() + }); +}) \ No newline at end of file diff --git a/test/compile.test.js b/test/compile.test.js deleted file mode 100644 index 09ab181..0000000 --- a/test/compile.test.js +++ /dev/null @@ -1,122 +0,0 @@ -const dayjs = require('dayjs'); -const { getInterpolatedVars, replaceInterpolatedVars} = require('../src/index.js') - -const scope1 ={ - defaultLanguage: "cn", // 默认语言名称 - default: { // 默认语言包 - - }, - messages : { // 当前语言包 - - }, - idMap:{ // 消息id映射列表 - - }, - formatters:{ // 当前作用域的格式化函数列表 - "*":{ - $types:{ - Date:(v)=>dayjs(v).format('YYYY/MM/DD'), - Boolean:(v)=>v?"True":"False", - }, - sum:(v,n=1)=>v+n, - double:(v)=>v*2, - upper:(v)=>v.toUpperCase(), - lower:(v)=>v.toLowerCase() - }, - cn:{ - $types:{ - Date:(v)=>dayjs(v).format('YYYY年MM月DD日'), - Boolean:(v)=>v?"是":"否", - } - }, - en:{ - $types:{ - - } - }, - }, - loaders:{}, // 异步加载语言文件的函数列表 - global:{// 引用全局VoerkaI18n配置 - defaultLanguage: "cn", - activeLanguage: "cn", - languages:[ - {name:"cn",title:"中文",default:true}, - {name:"en",title:"英文"}, - {name:"de",title:"德语"}, - {name:"jp",title:"日语"} - ], - formatters:{ // 当前作用域的格式化函数列表 - "*":{ - $types:{ - - } - }, - cn:{ - $types:{ - - } - }, - en:{ - $types:{ - - } - }, - } - } -} - -const replaceVars = replaceInterpolatedVars.bind(scope1) - - -test("获取翻译内容中的插值变量",done=>{ - const results = getInterpolatedVars("中华人民共和国成立于{date | year | time }年,首都是{city}市"); - expect(results.map(r=>r[0]).join(",")).toBe("date,city"); - expect(results[0][0]).toEqual("date"); - expect(results[0][1]).toEqual(["year","time"]); - expect(results[1][0]).toEqual("city"); - expect(results[1][1]).toEqual([]); - done() -}) - -test("获取翻译内容中定义了重复的插值变量",done=>{ - const results = getInterpolatedVars("{a}{a}{a|x}{a|x}{a|x|y}{a|x|y}"); - expect(results.length).toEqual(3); - expect(results[0][0]).toEqual("a"); - expect(results[0][1]).toEqual([]); - expect(results[1][0]).toEqual("a"); - expect(results[1][1]).toEqual(["x"]); - expect(results[2][0]).toEqual("a"); - expect(results[2][1]).toEqual(["x","y"]); - done() -}) - -test("替换翻译内容的位置插值变量",done=>{ - - expect(replaceVars("{}{}{}",1,2,3)).toBe("123"); - expect(replaceVars("{a}{b}{c}",1,2,3)).toBe("123"); - // 定义了一些无效的格式化器,直接忽略 - expect(replaceVars("{a|xxx}{b|dd}{c|}",1,2,3)).toBe("123"); - expect(replaceVars("{a|xxx}{b|dd}{c|}",1,2,3,4,5,6)).toBe("123"); - expect(replaceVars("{ a|}{b|dd}{c|}{}",1,2,3)).toBe("123{}"); - // 数据值进行 - expect(replaceVars("{}{}{}",1,"2",true)).toBe("12true"); - - done() -}) - -test("替换翻译内容的命名插值变量",done=>{ - expect(replaceVars("{a}{b}{c}",{a:11,b:22,c:33})).toBe("112233"); - expect(replaceVars("{a}{b}{c}{a}{b}{c}",{a:1,b:"2",c:3})).toBe("123123"); - done() -}) - -test("命名插值变量使用格式化器",done=>{ - // 提供无效的格式化器,直接忽略 - expect(replaceVars("{a|x}{b|x|y}{c|}",{a:1,b:2,c:3})).toBe("123"); - expect(replaceVars("{a|x}{b|x|y}{c|double}",{a:1,b:2,c:3})).toBe("126"); - // 默认的字符串格式化器,不需要定义使用字符串方法 - expect(replaceVars("{a|x}{b|x|y}{c|double}",{a:1,b:2,c:3})).toBe("126"); - expect(replaceVars("{a|padStart(10)}",{a:"123"})).toBe(" 123"); - expect(replaceVars("{a|padStart(10)|trim}",{a:"123"})).toBe("123"); - done() -}) \ No newline at end of file diff --git a/test/translate.test.js b/test/translate.test.js index 7e4f175..1a8fcfe 100644 --- a/test/translate.test.js +++ b/test/translate.test.js @@ -1,18 +1,225 @@ +const dayjs = require('dayjs'); +const { getInterpolatedVars, replaceInterpolatedVars , translate} = require('../src/index.js') + +const messages = { + cn:{ + 1:"你好", + 2:"现在是{}", + 3:"我出生于{year}年,今年{age}岁", + }, + en :{ + 1:"hello", + 2:"Now is {}", + 3:"I was born in {year}, now is {age} years old", + } +} -import { translate } from "../src/index.js" +const idMap = { + "你好":1, + "现在是{}":2, + "我出生于{year}年,今年{age}岁":3 +} -test("默认语言翻译",done=>{ + + +let scope1 ={ + defaultLanguage: "cn", // 默认语言名称 + default: messages.cn, + messages :messages.cn, + idMap, + formatters:{ // 当前作用域的格式化函数列表 + "*":{ + $types:{ + Date:(v)=>dayjs(v).format('YYYY-MM-DD HH:mm:ss'), + Boolean:(v)=>v?"True":"False", + }, + sum:(v,n=1)=>v+n, + double:(v)=>v*2, + upper:(v)=>v.toUpperCase(), + lower:(v)=>v.toLowerCase() + }, + cn:{ + $types:{ + Date:(v)=>dayjs(v).format('YYYY年MM月DD日 HH点mm分ss秒'), + Boolean:(v)=>v?"是":"否", + }, + book:(v)=>`《${v}》`, + }, + en:{ + $types:{ + + }, + book:(v)=>`<${v}>`, + }, + }, + loaders:{}, // 异步加载语言文件的函数列表 + global:{// 引用全局VoerkaI18n配置 + defaultLanguage: "cn", + activeLanguage: "cn", + languages:[ + {name:"cn",title:"中文",default:true}, + {name:"en",title:"英文"}, + {name:"de",title:"德语"}, + {name:"jp",title:"日语"} + ], + formatters:{ // 当前作用域的格式化函数列表 + "*":{ + $types:{ + + } + }, + cn:{ + $types:{ + + } + }, + en:{ + $types:{ + + } + }, + } + } +} + +const replaceVars = replaceInterpolatedVars.bind(scope1) +const t = translate.bind(scope1) + + +function changeLanguage(language){ + scope1.global.activeLanguage = language + scope1.messages = messages[language] + +} + +beforeEach(() => { + scope1.global.activeLanguage = "cn" // 切换到中文 +}); + + +test("获取翻译内容中的插值变量",done=>{ + const results = getInterpolatedVars("中华人民共和国成立于{date | year | time }年,首都是{city}市"); + expect(results.map(r=>r[0]).join(",")).toBe("date,city"); + expect(results[0][0]).toEqual("date"); + expect(results[0][1]).toEqual(["year","time"]); + expect(results[1][0]).toEqual("city"); + expect(results[1][1]).toEqual([]); + done() +}) + +test("获取翻译内容中定义了重复的插值变量",done=>{ + const results = getInterpolatedVars("{a}{a}{a|x}{a|x}{a|x|y}{a|x|y}"); + expect(results.length).toEqual(3); + expect(results[0][0]).toEqual("a"); + expect(results[0][1]).toEqual([]); + expect(results[1][0]).toEqual("a"); + expect(results[1][1]).toEqual(["x"]); + expect(results[2][0]).toEqual("a"); + expect(results[2][1]).toEqual(["x","y"]); + done() +}) + +test("替换翻译内容的位置插值变量",done=>{ + expect(replaceVars("{}{}{}",1,2,3)).toBe("123"); + expect(replaceVars("{a}{b}{c}",1,2,3)).toBe("123"); + // 定义了一些无效的格式化器,直接忽略 + expect(replaceVars("{a|xxx}{b|dd}{c|}",1,2,3)).toBe("123"); + expect(replaceVars("{a|xxx}{b|dd}{c|}",1,2,3,4,5,6)).toBe("123"); + expect(replaceVars("{ a|}{b|dd}{c|}{}",1,2,3)).toBe("123{}"); + // 中文状态下true和false被转换成中文的"是"和"否" + expect(replaceVars("{}{}{}",1,"2",true)).toBe("12是"); + expect(replaceVars("{|double}{}{}",1,"2",true)).toBe("22是"); + done() +}) + +test("替换翻译内容的命名插值变量",done=>{ + expect(replaceVars("{a}{b}{c}",{a:11,b:22,c:33})).toBe("112233"); + expect(replaceVars("{a}{b}{c}{a}{b}{c}",{a:1,b:"2",c:3})).toBe("123123"); + done() +}) + +test("命名插值变量使用格式化器",done=>{ + // 提供无效的格式化器,直接忽略 + expect(replaceVars("{a|x}{b|x|y}{c|}",{a:1,b:2,c:3})).toBe("123"); + expect(replaceVars("{a|x}{b|x|y}{c|double}",{a:1,b:2,c:3})).toBe("126"); + // 默认的字符串格式化器,不需要定义使用字符串方法 + expect(replaceVars("{a|x}{b|x|y}{c|double}",{a:1,b:2,c:3})).toBe("126"); + // padStart格式化器是字符串的方法,不需要额外定义可以直接使用 + expect(replaceVars("{a|padStart(10)}",{a:"123"})).toBe(" 123"); + expect(replaceVars("{a|padStart(10)|trim}",{a:"123"})).toBe("123"); + done() +}) + + +test("命名插值变量使用格式化器",done=>{ + // 提供无效的格式化器,直接忽略 + expect(replaceVars("{a|x}{b|x|y}{c|}",{a:1,b:2,c:3})).toBe("123"); + expect(replaceVars("{a|x}{b|x|y}{c|double}",{a:1,b:2,c:3})).toBe("126"); + // 默认的字符串格式化器,不需要定义使用字符串方法 + expect(replaceVars("{a|x}{b|x|y}{c|double}",{a:1,b:2,c:3})).toBe("126"); + expect(replaceVars("{a|padStart(10)}",{a:"123"})).toBe(" 123"); + expect(replaceVars("{a|padStart(10)|trim}",{a:"123"})).toBe("123"); + done() + }) + + + +test("切换到其他语言时的自动匹配同名格式化器",done=>{ + // 默认的字符串类型的格式化器 + expect(replaceVars("{a}",{a:true})).toBe("是"); + expect(replaceVars("{name|book}是毛泽东思想的重要载体","毛泽东选集")).toBe("《毛泽东选集》是毛泽东思想的重要载体"); + changeLanguage("en") + expect(replaceVars("{a}",{a:false})).toBe("False"); + expect(replaceVars("{name|book}是毛泽东思想的重要载体","毛泽东选集")).toBe("<毛泽东选集>是毛泽东思想的重要载体"); done() }) -test("启用位置插值变量翻译",done=>{ +test("位置插值翻译文本内容",done=>{ + const now = new Date() + expect(t("你好")).toBe("你好"); + expect(t("现在是{}",now)).toBe(`现在是${dayjs(now).format('YYYY年MM月DD日 HH点mm分ss秒')}`); + + // 经babel自动码换后,文本内容会根据idMap自动转为id + expect(t("1")).toBe("你好"); + expect(t("2",now)).toBe(`现在是${dayjs(now).format('YYYY年MM月DD日 HH点mm分ss秒')}`); + + changeLanguage("en") + expect(t("你好")).toBe("hello"); + expect(t("现在是{}",now)).toBe(`Now is ${dayjs(now).format('YYYY-MM-DD HH:mm:ss')}`); + expect(t("1")).toBe("hello"); + expect(t("2",now)).toBe(`Now is ${dayjs(now).format('YYYY-MM-DD HH:mm:ss')}`); + done() +}) + +test("命名插值翻译文本内容",done=>{ + const now = new Date() + expect(t("你好")).toBe("你好"); + expect(t("现在是{}",now)).toBe(`现在是${dayjs(now).format('YYYY年MM月DD日 HH点mm分ss秒')}`); + + + changeLanguage("en") + expect(t("你好")).toBe("hello"); + expect(t("现在是{}",now)).toBe(`Now is ${dayjs(now).format('YYYY-MM-DD HH:mm:ss')}`); + expect(t("1")).toBe("hello"); + expect(t("2",now)).toBe(`Now is ${dayjs(now).format('YYYY-MM-DD HH:mm:ss')}`); done() }) - -test("启用字典插值变量翻译",done=>{ +test("当没有对应的语言翻译时",done=>{ + expect(t("我是中国人")).toBe("我是中国人"); + changeLanguage("en") + expect(t("我是中国人")).toBe("我是中国人"); done() -}) \ No newline at end of file +}) + + +test("切换到未知语言",done=>{ + expect(t("我是中国人")).toBe("我是中国人"); + changeLanguage("en") + expect(t("我是中国人")).toBe("我是中国人"); + done() +}) +