This commit is contained in:
wxzhang 2022-02-24 18:49:54 +08:00
parent 5fb36e4a45
commit b9eff3ea5b
6 changed files with 246 additions and 19 deletions

View File

@ -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": ""

View File

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

View File

@ -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={}){
}

View File

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

33
src/formatters.js Normal file
View File

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

126
src/utils.js Normal file
View File

@ -0,0 +1,126 @@
// 用来提取字符里面的插值变量参数
// 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("") == 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<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 = {
hasInterpolation,
getInterpolatedVars,
replaceInterpolateVars
}