更新翻译函数和转码插件

This commit is contained in:
wxzhang 2022-03-03 18:02:24 +08:00
parent f5cac613bb
commit 2bfb0fea04
37 changed files with 674 additions and 436 deletions

View File

@ -1,4 +1,4 @@
module.exports = { export default {
"a1:aaaaa": 1, "a1:aaaaa": 1,
"no aaaaa": 2, "no aaaaa": 2,
"bbbbb": 3, "bbbbb": 3,

View File

@ -0,0 +1,7 @@
export default {
"a":1,
"b":2,
"c{}{}":3,
"d{a}{b}":4,
"e":5
}

View File

@ -0,0 +1,7 @@
module.exports = {
"a":1,
"b":2,
"c{}{}":3,
"d{a}{b}":4,
"e":5
}

View File

@ -1,10 +1,10 @@
const babel = require("@babel/core"); const babel = require("@babel/core");
const fs = require("fs"); const fs = require("fs");
const path = require("path"); 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, { babel.transform(code, {
plugins: [ plugins: [
[ [

7
demo/compile.demo.js Normal file
View File

@ -0,0 +1,7 @@
const compile = require('../src/compile');
const path = require("path")
compile(path.resolve(__dirname,"./apps/app/languages"))

View File

@ -1,9 +1,9 @@
const gulp = require('gulp'); const gulp = require('gulp');
const extract = require('./extract.plugin'); const extract = require('../src/extract.plugin');
const path = require('path'); 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({ ]).pipe(extract({
debug:true, debug:true,
// output: path.join(soucePath , 'languages'), // 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:{ // extractor:{
// default:[new RegExp()], // 默认匹配器,当文件类型没有对应的提取器时使用 // default:[new RegExp()], // 默认匹配器,当文件类型没有对应的提取器时使用
// "*" : [new RegExp()], // 所有类型均会执行的提取器 // "*" : [new RegExp()], // 所有类型均会执行的提取器
@ -26,4 +26,4 @@ gulp.src([
"b":"b", "b":"b",
} }
})) }))
.pipe(gulp.dest(path.join(__dirname,'../demoapps/app/languages'))); .pipe(gulp.dest(path.join(__dirname,'./apps/app/languages')));

View File

@ -17,13 +17,26 @@
* *
*/ */
const { getMessageId } = require('./utils');
const TRANSLATE_FUNCTION_NAME = "t"
const fs = require("fs"); const fs = require("fs");
const path = require("path");
const { isPlainObject } = require("./utils");
const DefaultI18nPluginOptions = { const DefaultI18nPluginOptions = {
translateFunctionName:"t", // 默认的翻译函数名称 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和翻译内容的映射关系,此参数用于测试使用
// 正常情况下会读取<location>/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) { module.exports = function voerkai18nPlugin(babel) {
const t = babel.types; const t = babel.types;
const pluginOptions = Object.assign({},DefaultI18nPluginOptions); const pluginOptions = Object.assign({},DefaultI18nPluginOptions);
let idMap = {}
return { return {
visitor:{ visitor:{
Program(path, state){ Program(path, state){
// 转码插件参数可以覆盖默认参数
Object.assign(pluginOptions,state.opts || {}); Object.assign(pluginOptions,state.opts || {});
const { location = "./languages", translateFunctionName } = pluginOptions const { location ,autoImport, translateFunctionName,moduleType } = pluginOptions
if(isEsModule(path)){ 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函数则自动导入 // 如果没有定义t函数则自动导入
if(!hasImportTranslateFunction(path)){ if(!hasImportTranslateFunction(path)){
path.node.body.unshift(t.importDeclaration([ path.node.body.unshift(t.importDeclaration([
t.ImportSpecifier(t.identifier(translateFunctionName),t.identifier(translateFunctionName) t.ImportSpecifier(t.identifier(translateFunctionName),t.identifier(translateFunctionName)
)],t.stringLiteral(location))) )],t.stringLiteral(autoImport)))
} }
}else{ }else{
if(!hasRequireTranslateFunction(path)){ if(!hasRequireTranslateFunction(path)){
path.node.body.unshift(t.variableDeclaration("const",[ path.node.body.unshift(t.variableDeclaration("const",[
t.variableDeclarator( t.variableDeclarator(
t.ObjectPattern([t.ObjectProperty(t.Identifier(translateFunctionName),t.Identifier(translateFunctionName),false,true)]), t.ObjectPattern([t.ObjectProperty(t.Identifier(translateFunctionName),t.Identifier(translateFunctionName),false,true)]),
t.CallExpression(t.Identifier("require"),[t.stringLiteral(location)]) t.CallExpression(t.Identifier("require"),[t.stringLiteral(autoImport)])
) )
])) ]))
} }
} }
}
}, },
// 将t函数的第一个参数转换为id
CallExpression(path,state){ CallExpression(path,state){
let options = state.opts if( path.node.callee.name === pluginOptions.translateFunctionName ){
// 只对翻译函数进行转码
if(path.node.callee.name===TRANSLATE_FUNCTION_NAME){
if(path.node.arguments.length>0 && t.isStringLiteral(path.node.arguments[0])){ if(path.node.arguments.length>0 && t.isStringLiteral(path.node.arguments[0])){
let text = path.node.arguments[0].value let message = path.node.arguments[0].value
path.node.arguments[0] = t.stringLiteral(`*${text}*`) const msgId =(message in idMap) ? idMap[message] : message
path.node.arguments[0] = t.stringLiteral(String(msgId))
} }
}else{ }else{
path.skip(); path.skip();

View File

@ -1,7 +0,0 @@
const compile = require('./compile');
const path = require("path")
compile(path.resolve(__dirname,"../demoapps/app/languages"))

View File

@ -6,32 +6,19 @@
* 编译原理如下 * 编译原理如下
* *
* *
* 编译输出: * 编译后会在目标文件夹输出:
* *
* hashId = getMessageId() * - languages
* * translates
* - languages/index.js 主源码用来引用语言文件 * - en.json
* { * - cn.json
* languages:{}, * - ...
* idMap.js // id映射列表
* } * settings.js // 配置文件
* - languages/messageIds.json 翻译文本的id映射表 * cn.js // 中文语言包
* { * en.js // 英文语言包
* [msg]:"<id>" * [lang].js // 其他语言包
* } * package.json // 包信息用来指定包类型以便在nodejs中能够正确加载
* - languages/en.js 英文语言文件
* {
* [region]:{
* [namespace]:{
* [hashId]:"<message>",
* },
* [namespace]:{...},
* },
* [region]:{...}
* }
*
* - languages/[lang].js 语言文件
* - formaters.js
* *
* @param {*} opts * @param {*} opts
*/ */
@ -52,8 +39,8 @@ function normalizeCompileOptions(opts={}) {
moduleType:"esm" // 指定编译后的语言文件的模块类型取值common,cjs,esm,es moduleType:"esm" // 指定编译后的语言文件的模块类型取值common,cjs,esm,es
}, opts) }, opts)
if(options.moduleType==="es") options.moduleType = "esm" if(options.moduleType==="es") options.moduleType = "esm"
if(options.moduleType==="cjs") options.moduleType = "common" if(options.moduleType==="cjs") options.moduleType = "commonjs"
if(["common","cjs","esm","es"].includes(options.moduleType)) options.moduleType = "esm" if(["commonjs","cjs","esm","es"].includes(options.moduleType)) options.moduleType = "esm"
return opts; 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 } ) const formattersContent = artTemplate(path.join(__dirname,"templates","formatters.js"), {languages,defaultLanguage,activeLanguage,namespaces,moduleType } )
fs.writeFileSync(formattersFile,formattersContent) 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))
}) })
} }

View File

@ -0,0 +1,24 @@
/**
*
* 提供处理货币格式化的功能
*
* import './languages';
* import "voerka-i18n/formatters/currency"; // 货币格式化
*
*
*/
if(globalThis.VoerkaI18n){
VoerkaI18n.registerFormatters({
{
currency
}
})
}

View File

@ -0,0 +1,28 @@
/**
*
* 提供日期时间格式化的功能
*
* import './languages';
* import "voerka-i18n/formatters/datetime"; // 货币格式化
*
*
*/
if(globalThis.VoerkaI18n){
VoerkaI18n.registerFormatters({
"*":{
},
cn:{
},
en:{
}
})
}

View File

@ -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 = { module.exports = {
"*":{
$types:{
Date:(value)=>value.toLocaleString()
},
time:(value)=> value.toLocaleTimeString(),
date: (value)=> value.toLocaleDateString(),
},
cn:{ cn:{
"*":{ // 适用于所有类型的格式化器 $types:{
default:null // 默认格式化器 Date:(value)=> `${value.getFullYear()}${value.getMonth()+1}${value.getDate()}${value.getHours()}${value.getMinutes()}${value.getSeconds()}`
}, },
Date:{ time:(value)=>`${value.getHours()}${value.getMinutes()}${value.getSeconds()}`,
default:(value)=>dayjs(value).format("YYYY年MM年DD日"), // 默认的格式化器 date: (value)=> `${value.getFullYear()}${value.getMonth()+1}${value.getDate()}`,
time:(value)=>dayjs(value).format("HH:mm:ss"), currency:(value)=>`${value}`,
date:(value)=>dayjs(value).format("YYYY/MM/DD")
},
Number:{
}
}, },
en:{ en:{
"Date":{ currency:(value)=>`$${value}`,
short:(value)=>dayjs(value).format("YYYY/MM/DD"),
time:(value)=>dayjs(value).format("HH:mm:ss")
}
} }
} }

View File

@ -1,6 +1,6 @@
const deepMerge = require("deepmerge") const deepMerge = require("deepmerge")
const formatters = require("./formatters") const formatters = require("./formatters")
const {isPlainObject ,isNumber , getDataTypeName} = require("./utils")
// 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter } // 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter }
// 不支持参数: let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*\s*)*)\s*\}/g // 不支持参数: let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*\s*)*)\s*\}/g
@ -8,6 +8,8 @@ const formatters = require("./formatters")
// 支持参数: { var | formatter(x,x,..) | formatter } // 支持参数: { var | formatter(x,x,..) | formatter }
let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*(\(.*\)){0,1}\s*)*)\s*\}/g let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\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"]
// 插值变量字符串替换正则 // 插值变量字符串替换正则
@ -29,24 +31,6 @@ let varReplaceRegexp =String.raw`\{\s*{varname}\s*\}`
function hasInterpolation(str){ function hasInterpolation(str){
return str.includes("{") && str.includes("}") 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 * @param {*} callback
* @returns * @returns
*/ */
function forEachInterpolatedVars(str,callback){ function forEachInterpolatedVars(str,callback,options={}){
let result=str, match let result=str, match
let opts = Object.assign({
replaceAll:true, // 是否替换所有插值变量当使用命名插值时应置为true当使用位置插值时应置为false
},options)
varWithPipeRegexp.lastIndex=0 varWithPipeRegexp.lastIndex=0
while ((match = varWithPipeRegexp.exec(result)) !== null) { while ((match = varWithPipeRegexp.exec(result)) !== null) {
const varname = match.groups.varname || "" const varname = match.groups.varname || ""
// 解析格式化器和参数 = [<formatterName>,[<formatterName>,[<arg>,<arg>,...]]] // 解析格式化器和参数 = [<formatterName>,[<formatterName>,[<arg>,<arg>,...]]]
const formatters = parseFormatters(match.groups.formatters) const formatters = parseFormatters(match.groups.formatters)
if(typeof(callback)==="function"){ if(typeof(callback)==="function"){
try{
if(opts.replaceAll){
result=result.replaceAll(match[0],callback(varname,formatters)) result=result.replaceAll(match[0],callback(varname,formatters))
}else{
result=result.replace(match[0],callback(varname,formatters))
}
}catch{// callback函数可能会抛出异常如果抛出异常则中断匹配过程
break
}
} }
varWithPipeRegexp.lastIndex=0 varWithPipeRegexp.lastIndex=0
} }
@ -156,7 +151,7 @@ function transformToString(value){
let result = value let result = value
if(typeof(result)==="function") result = value() if(typeof(result)==="function") result = value()
if(!(typeof(result)==="string")){ if(!(typeof(result)==="string")){
if(Array.isArray(result) || typeof(result)==="object"){ if(Array.isArray(result) || isPlainObject(result)){
result = JSON.stringify(result) result = JSON.stringify(result)
}else{ }else{
result = result.toString() result = result.toString()
@ -304,6 +299,28 @@ function buildFormatters(scope,activeLanguage,formatters){
} }
return results 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("<模板字符串>",{变量名称:变量值,变量名称:变量值,...}) * replaceInterpolatedVars("<模板字符串>",{变量名称:变量值,变量名称:变量值,...})
@ -326,52 +343,33 @@ function replaceInterpolatedVars(template,...args) {
// 当前激活语言 // 当前激活语言
const activeLanguage = scope.global.activeLanguage const activeLanguage = scope.global.activeLanguage
let result=template 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],...} // [[var1,[formatter,formatter,...],match],[var2,[formatter,formatter,...],match],...}
let varValues = args[0] let varValues = args[0]
return forEachInterpolatedVars(template,(varname,formatters)=>{ return forEachInterpolatedVars(template,(varname,formatters)=>{
// 1. 取得格式化器函数列表
const formatterFuncs = buildFormatters(scope,activeLanguage,formatters)
// 2. 取变量值
let value = (varname in varValues) ? varValues[varname] : '' let value = (varname in varValues) ? varValues[varname] : ''
// 3. 查找每种数据类型默认格式化器,并添加到formatters最前面默认数据类型格式化器优先级最高 return getFormattedValue(scope,activeLanguage,formatters,value)
const defaultFormatter = getDataTypeDefaultFormatter(scope,activeLanguage,getDataTypeName(value))
if(defaultFormatter){
formatterFuncs.splice(0,0,defaultFormatter)
}
// 4. 执行格式化器
value = executeFormatter(value,formatterFuncs)
return value
}) })
}else{ }else{
// ****************************位置插值**************************** // ****************************位置插值****************************
// 如果只有一个Array参数则认为是位置变量列表进行展开 // 如果只有一个Array参数则认为是位置变量列表进行展开
const params=(args.length===1 && Array.isArray(args[0])) ? [...args[0]] : args const params=(args.length===1 && Array.isArray(args[0])) ? [...args[0]] : args
if(params.length===0) return template // 没有变量则不需要进行插值处理,返回原字符串
// 取得模板字符串中的插值变量列表 , 包含命令变量和位置变量
let interpVars = getInterpolatedVars(template,true)
if(interpVars.length===0) return template // 没有变量插值则的返回原字符串
let i = 0 let i = 0
for(let match of result.match(varWithPipeRegexp) || []){ return forEachInterpolatedVars(template,(varname,formatters)=>{
if(i<params.length){ if(params.length>i){
let value = params[i] return getFormattedValue(scope,activeLanguage,formatters,params[i++])
const formatterFuncs = buildFormatters(scope,activeLanguage,interpVars[i][1])
// 执行默认的数据类型格式化器
const defaultFormatter = getDataTypeDefaultFormatter(scope,activeLanguage,getDataTypeName(value))
if(defaultFormatter){
formatterFuncs.splice(0,0,defaultFormatter)
}
value = executeFormatter(value,formatterFuncs)
result = result.replace(match,transformToString(value))
i+=1
}else{ }else{
break throw new Error() // 抛出异常,停止插值处理
}
} }
},{replaceAll:false})
} }
return result return result
} }
@ -390,16 +388,31 @@ const defaultLanguageSettings = {
function isMessageId(content){ function isMessageId(content){
return parseInt(content)>0 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("要翻译的文本内容") 如果默认语言是中文则不会进行翻译直接返回
* translate("I am {} {}","man") == I am man 位置插值 * translate("I am {} {}","man") == I am man 位置插值
* translate("I am {p}",{p:"man"}) 字典插值 * translate("I am {p}",{p:"man"}) 字典插值
* translate("I am {p}",{p:"man",ns:""}) 指定名称空间 * translate("total {$count} items", {$count:1}) //复数形式
* translate("I am {p}",{p:"man",namespace:""})
* translate("I am {p}",{p:"man",namespace:""})
* translate("total {count} items", {count:1}) //复数形式
* translate("total {} {} {} items",a,b,c) // 位置变量插值 * translate("total {} {} {} items",a,b,c) // 位置变量插值
* *
* this===scope 当前绑定的scope * this===scope 当前绑定的scope
@ -407,14 +420,15 @@ function isMessageId(content){
*/ */
function translate(message) { function translate(message) {
const scope = this const scope = this
const activeLanguage = scope.settings.activeLanguage const activeLanguage = scope.global.activeLanguage
let vars={} // 插值变量 let content = message
let vars=[] // 插值变量列表
let pluralVars= [] // 复数变量 let pluralVars= [] // 复数变量
let pluraValue = null // 复数值
try{ try{
// 1. 预处理变量: 复数变量保存至pluralVars中 , 变量如果是Function则调用 // 1. 预处理变量: 复数变量保存至pluralVars中 , 变量如果是Function则调用
if(arguments.length === 2 && typeof(arguments[1])=='object'){ if(arguments.length === 2 && isPlainObject(arguments[1])){
Object.assign(vars,arguments[1]) Object.entries(arguments[1]).forEach(([name,value])=>{
Object.entries(vars).forEach(([name,value])=>{
if(typeof(value)==="function"){ if(typeof(value)==="function"){
try{ try{
vars[name] = value() vars[name] = value()
@ -422,41 +436,64 @@ function translate(message) {
vars[name] = value vars[name] = value
} }
} }
// 复数变量 // 以$开头的视为复数变量
if(name.startsWith("$")) pluralVars.push(name) if(name.startsWith("$")) pluralVars.push(name)
}) })
vars = [arguments[1]]
}else if(arguments.length >= 2){ }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){ if(activeLanguage === scope.defaultLanguage){
// 2.1 从默认语言中取得翻译文本模板字符串
// 如果当前语言就是默认语言,不需要查询加载,只需要做插值变换即可
// 当源文件运用了babel插件后会将原始文本内容转换为msgId // 当源文件运用了babel插件后会将原始文本内容转换为msgId
if(isMessageId(message)){ // 如果是msgId则从scope.default中读取,scope.default=默认语言包={<id>:<message>}
message = scope.default[message] || message if(isMessageId(content)){
content = scope.default[content] || message
} }
return replaceInterpolatedVars.call(scope,message,vars)
}else{ }else{
// 1. 获取翻译后的文本内容 // 2.2 从当前语言包中取得翻译文本模板字符串
// 如果没有启用babel插件时需要先将文本内容转换为msgId // 如果没有启用babel插件将源文本转换为msgId需要先将文本内容转换为msgId
let msgId = isMessageId(message) ? message : scope.idMap[message] let msgId = isMessageId(content) ? content : scope.idMap[content]
message = scope.messages[msgId] || msgId content = scope.messages[msgId] || content
// 处理复数
if(Array.isArray(message)){
}else{ // 普通
} }
// 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){ }catch(e){
return message return content // 出错则返回原始文本
} }
} }
/** /**
* 多语言管理类 * 多语言管理类
* *
@ -587,13 +624,20 @@ function translate(message) {
scope.global = this._settings scope.global = this._settings
this._scopes.push(scope) this._scopes.push(scope)
} }
} /**
* 注册全局格式化器
* @param {*} formatters
*/
registerFormatters(formatters){
}
}
module.exports ={ module.exports ={
getInterpolatedVars, getInterpolatedVars,
replaceInterpolatedVars, replaceInterpolatedVars,
I18n, I18n,
translate, translate,
languages,
defaultLanguageSettings defaultLanguageSettings
} }

View File

@ -2,19 +2,19 @@
import messageIds from "./idMap.js" import messageIds from "./idMap.js"
import { translate,i18n } from "voerka-i18n" import { translate,i18n } from "voerka-i18n"
import defaultMessages from "./{{defaultLanguage}}.js" import defaultMessages from "./{{defaultLanguage}}.js"
import i18nSettings from "./settings.js" import scopeSettings from "./settings.js"
import formatters from "../formatters" import formatters from "../formatters"
{{else}} {{else}}
const messageIds = require("./idMap") const messageIds = require("./idMap")
const { translate,i18n } = require("voerka-i18n") const { translate,i18n } = require("voerka-i18n")
const defaultMessages = require("./{{defaultLanguage}}.js") const defaultMessages = require("./{{defaultLanguage}}.js")
const i18nSettings = require("./settings.js") const scopeSettings = require("./settings.js")
const formatters = require("../formatters") const formatters = require("../formatters")
{{/if}} {{/if}}
// 自动创建全局VoerkaI18n实例 // 自动创建全局VoerkaI18n实例
if(!globalThis.VoerkaI18n){ if(!globalThis.VoerkaI18n){
globalThis.VoerkaI18n = new i18n(i18nSettings) globalThis.VoerkaI18n = new i18n(scopeSettings)
} }
let scope = { let scope = {

View File

@ -7,7 +7,8 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
const formatters = { const formatters = {
"*":{ // 在所有语言下生效的格式化器 "*":{ // 在所有语言下生效的格式化器
$types:{...} // 只作用于特定数据类型的默认格式化器 $types:{...}, // 只作用于特定数据类型的默认格式化器
.... // 全局格式化器
}, },
cn:{ cn:{
// 只作用于特定数据类型的格式化器 // 只作用于特定数据类型的格式化器

View File

@ -1,25 +1,4 @@
// 用来提取字符里面的插值变量参数
// let varRegexp = /\{\s*(?<var>\w*\.?\w*)\s*\}/g
let varRegexp = /\{\s*((?<varname>\w+)?(\s*\|\s*(?<formatter>\w*))?)?\s*\}/g
// 插值变量字符串替换正则
//let varReplaceRegexp =String.raw`\{\s*(?<var>{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 * getDataTypeName(1) == Number
@ -38,108 +17,46 @@ function getDataTypeName(v){
if(typeof(v)==="function") return "Function" if(typeof(v)==="function") return "Function"
return v.constructor && v.constructor.name; 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 * 支持导入cjs和esm模块
* performance.now()返回的是当前node进程从启动到现在时间戳 * @param {*} url
* 但是连续调用可能会导致重复id的生成如果发现重复则会
* 在生成的id后面加上一个随机数以保证id的唯一性
*/ */
function getMessageId(){ async function importModule(url){
this.lastMsgId = null try{
let id = String(performance.now()).replace(".","") return require(url)
if(this.lastMsgId === id){ }catch(e){
id = id + parseInt(Math.random() * 1000) .toString() // 当加载出错时尝试加载esm模块
if(e.code === "MODULE_NOT_FOUND"){
return await import(url)
}else{ }else{
this.lastMsgId = id throw e
}
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)
}else{
result = result.toString()
}
}
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<params.length){
let param = transformVarValue(params[i])
result=result.replace(match,param)
i+=1
}
}
}
return result
} }
// const str = "I am {name}, I am {age} years old. you are {name},Now is {date},time={date | time}?"
// console.log("vars=",getInterpolatedVars(str).join())
// console.log(replaceInterpolateVars(str,{name:"tom",age:18,date:new Date(),"date|time":new Date().getTime()}))
// console.log(replaceInterpolateVars(str,"tom",18,"jack"))
// console.log(replaceInterpolateVars(str,["tom",18,"jack",1,2]))
// console.log(replaceInterpolateVars(str,"tom",18,()=>"bob"))
// console.log(replaceInterpolateVars(str,"tom",[1,2],{a:1},1,2))
module.exports = { module.exports = {
getMessageId, getDataTypeName,
hasInterpolation, isNumber,
getInterpolatedVars, isPlainObject,
replaceInterpolateVars importModule
} }

83
test/babel.test.js Normal file
View File

@ -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()
});
})

View File

@ -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()
})

View File

@ -1,18 +1,225 @@
const dayjs = require('dayjs');
const { getInterpolatedVars, replaceInterpolatedVars , translate} = require('../src/index.js')
import { translate } from "../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",
}
}
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() done()
}) })
test("启用位置插值变量翻译",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() done()
}) })
test("启用字典插值变量翻译",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() 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=>{
expect(t("我是中国人")).toBe("我是中国人");
changeLanguage("en")
expect(t("我是中国人")).toBe("我是中国人");
done()
})
test("切换到未知语言",done=>{
expect(t("我是中国人")).toBe("我是中国人");
changeLanguage("en")
expect(t("我是中国人")).toBe("我是中国人");
done()
})