diff --git a/package.json b/package.json index 0e55b38..6dc0367 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "voerka-i18n", "version": "1.0.0", "description": "", - "main": "index.js", + "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "extract": "" diff --git a/readme.md b/readme.md index 94dc581..2c4e23b 100644 --- a/readme.md +++ b/readme.md @@ -113,3 +113,57 @@ t("第{}章",7) // == Chapter Seven t("第{}章",100) // == Chapter 100 ``` + +## 插值变量格式化 + +voerka-i18n支持对插值变量进行格式化 + +```javascript + +new VoerkaI18n({ + formats:{ + Date:{ // 日期格式 + en:{ + default:(value)=>dayjs(value).format("YYYY/MM/DD"), + time:(value)=>dayjs(value).format("HH:hh:mm"), + // 可以定义多种自定义格式... + }, + cn:{ + default:(value)=>dayjs(value).format("YYYY年MM月DD日"), + time:(value)=>dayjs(value).format("HH:hh:mm"), + // 可以定义多种自定义格式... + }, + }, + String:{ + en:{ + firstUpper:(value)=>value[0].toUpperCase()+value.substr(1) // 首字母大写 + } + } + } +}) +``` +以上代码定义了: +- `Date`类型的英文和中文的两个格式化函数 +- `String`类型变量的`firstUpper`格式化函数 + +接下来,在翻译内容中使用。 + +```javascript + +// languages/translates/default.json +{ + "今天是{date}":{ + en:"Today is {date}", // 使用默认格式 + }, + "现在是北京时间:{date}":{ + en:"Now is {date.time}" + }, + "":{} +} + +t("今天是{date}",{date:new Date()}) + +``` + + + diff --git a/src/compile.js b/src/compile.js index 139597f..90515b9 100644 --- a/src/compile.js +++ b/src/compile.js @@ -1,2 +1,25 @@ +/** + * 将extract插件扫描的文件编译为语言文件 + * + * 编译后的语言文件用于运行环境使用 + * + * 编译原理如下: + * + * + * + * + * @param {*} opts + */ +function normalizeCompileOptions(opts={}) { + let options = Object.assign({ + input:null, // 指定要编译的文件夹,即extract输出的语言文件夹 + output:null, // 指定编译后的语言文件夹,如果没有指定,则使用input目录 + formatters:{}, // 对插值变量进行格式化的函数清单 + }, opts) + return opts; +} +module.exports = function compile(opts={}){ + +} \ No newline at end of file diff --git a/src/extract.plugin.js b/src/extract.plugin.js index b9f837e..57b681d 100644 --- a/src/extract.plugin.js +++ b/src/extract.plugin.js @@ -10,8 +10,8 @@ const deepmerge = require("deepmerge") const path = require('path') const fs = require('fs') const readJson = require("readjson") -const createLogger = require("logsets") - +const createLogger = require("logsets") +const { replaceInterpolateVars,getDataTypeName } = require("./utils") const logger = createLogger() // 捕获翻译文本的默认正则表达式 @@ -190,7 +190,7 @@ function normalizeLanguageOptions(options){ // 以下变量会被用来传递给提取器正则表达式 translation : { funcName : "t", // 翻译函数名称 - attrName :"data-i18n", // 用在html组件上的翻译属性名称 + attrName :"data-i18n", // 用在html组件上的翻译属性名称 } },options) // 输出配置 @@ -270,8 +270,7 @@ function normalizeLanguageOptions(options){ } }) } - - + logger.log("Supported languages\t: {}",options.languages.map(item=>`${item.title}(${item.name})`)) logger.log("Default language\t: {}",options.defaultLanguage) logger.log("Active language\t\t: {}",options.activeLanguage) @@ -321,17 +320,9 @@ function updateLanguageFile(fromTexts,toLangFile,options){ if(langName.startsWith("$")) return // const langExists = langName in targetLangs const targetText = targetLangs[langName] - // 如果目标语言已经存在并且内容不为空,则不需要更新 - if(!langExists){ - targetLangs[langName] = sourceText - }else if(typeof(targetText) === "string" && targetText.trim().length==0){ - targetLangs[langName] = sourceText - }else if(Array.isArray(targetText)){// 当文本内容支持复数时,可以用[单数文本,复数文本]的形式 - if(targetText.length>0){ - targetLangs[langName] = sourceText - }else{ - - } + // 如果目标语言已经存在并且内容不为空,则不需要更新 + if(!langExists){ // 不存在则创建新的翻译条目 + targetLangs[langName] = sourceText } }) }else{ @@ -397,14 +388,14 @@ module.exports = function(options={}){ } } // 将元数据生成到 i18n.meta.json - const metaFile = path.join(outputPath,"i18n.meta.json") + const metaFile = path.join(outputPath,"i18n.settings.js") const meta = { languages : options.languages, defaultLanguage: options.defaultLanguage, activeLanguage : options.activeLanguage, namespaces : options.namespaces } - fs.writeFileSync(metaFile,JSON.stringify(meta,null,4)) + fs.writeFileSync(metaFile,`export default ${JSON.stringify(meta,null,4)}`) logger.log(" - Generate language metadata : {}",metaFile) callback() }); diff --git a/src/formatters.js b/src/formatters.js new file mode 100644 index 0000000..92777b3 --- /dev/null +++ b/src/formatters.js @@ -0,0 +1,33 @@ +/** + * 默认的格式化器 + * + * 使用方法: + * + * 在translates/xxx.json文件中进行翻译时,可以对插值变量进行格式化, + * + * { + * "Now is {date}":{ + * "zh-CN":"现在是{date|time}" + * } + * } + * + * + */ +export default { + cn:{ + Date:{ + default:(value)=>dayjs(value).format("YYYY年MM年DD日"), // 默认的变量格式化器 + time:(value)=>dayjs(value).format("HH:mm:ss"), + short:(value)=>dayjs(value).format("YYYY/MM/DD"), + }, + Number:{ + + } + }, + en:{ + "Date":{ + short:(value)=>dayjs(value).format("YYYY/MM/DD"), + time:(value)=>dayjs(value).format("HH:mm:ss") + } + } +} \ No newline at end of file diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..ea7d136 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,126 @@ + + +// 用来提取字符里面的插值变量参数 +// 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 + * 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; +}; + +/** + * 提取字符串中的插值变量 + * @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"bob")) +console.log(replaceInterpolateVars(str,"tom",[1,2],{a:1},1,2)) + +module.exports = { + hasInterpolation, + getInterpolatedVars, + replaceInterpolateVars +} \ No newline at end of file