This commit is contained in:
wxzhang 2022-03-04 18:38:38 +08:00
parent 5b18f5a0e5
commit feb72c01a2
51 changed files with 2044 additions and 1073 deletions

6
.gitignore vendored
View File

@ -1,3 +1,7 @@
/.vscode
/node_modules
/demo/apps/app/languages
node_modules
/packages/demo/apps/app/languages
/packages/demo/apps/*/languages
/packages/demo/*/node_modules
/coverage

View File

@ -1,27 +0,0 @@
module.exports = {
"1": "a1:aaaaa",
"2": "no aaaaa",
"3": "bbbbb",
"4": "eeeeee",
"5": "x:from a",
"6": "中华人民共和国",
"7": "a2:aaaaa",
"8": "no erer",
"9": "no aaareraa",
"10": "no fdfd",
"11": "fdgfdgfdg",
"12": "aaaaa",
"13": "aaaaa1",
"14": "aaaaa2",
"15": "aaaaa 3",
"16": "ccccc",
"17": "cccccdc",
"18": "ddddd中国",
"19": "html-a",
"20": "html-b",
"21": "html-c",
"22": "from a",
"23": "from b",
"24": "from c1",
"25": "from c2"
}

View File

@ -1,27 +0,0 @@
module.exports = {
"1": "德文:aaaaa",
"2": "no aaaaa",
"3": "bbbbb",
"4": "eeeeee",
"5": "x:from a",
"6": "中华人民共和国",
"7": "a2:aaaaa",
"8": "no erer",
"9": "no aaareraa",
"10": "no fdfd",
"11": "fdgfdgfdg",
"12": "aaaaa",
"13": "aaaaa1",
"14": "aaaaa2",
"15": "aaaaa 3",
"16": "ccccc",
"17": "cccccdc",
"18": "ddddd中国",
"19": "html-a",
"20": "html-b",
"21": "html-c",
"22": "from a",
"23": "from b",
"24": "from c1",
"25": "from c2"
}

View File

@ -1,27 +0,0 @@
module.exports = {
"1": "a1:aaaaa",
"2": "no aaaaa",
"3": "bbbbb",
"4": "eeeeee",
"5": "x:from a",
"6": "中华人民共和国",
"7": "a2:aaaaa",
"8": "no erer",
"9": "no aaareraa",
"10": "no fdfd",
"11": "fdgfdgfdg",
"12": "aaaaa",
"13": "aaaaa1",
"14": "aaaaa2",
"15": "aaaaa 3",
"16": "ccccc",
"17": "cccccdc",
"18": "ddddd中国",
"19": "html-a",
"20": "html-b",
"21": "html-c",
"22": "from a",
"23": "from b",
"24": "from c1",
"25": "from c2"
}

View File

@ -1,27 +0,0 @@
export default {
"a1:aaaaa": 1,
"no aaaaa": 2,
"bbbbb": 3,
"eeeeee": 4,
"x:from a": 5,
"中华人民共和国": 6,
"a2:aaaaa": 7,
"no erer": 8,
"no aaareraa": 9,
"no fdfd": 10,
"fdgfdgfdg": 11,
"aaaaa": 12,
"aaaaa1": 13,
"aaaaa2": 14,
"aaaaa 3": 15,
"ccccc": 16,
"cccccdc": 17,
"ddddd中国": 18,
"html-a": 19,
"html-b": 20,
"html-c": 21,
"from a": 22,
"from b": 23,
"from c1": 24,
"from c2": 25
}

View File

@ -1,43 +0,0 @@
const messageIds = require("./idMap")
const { translate,i18n } = require("voerka-i18n")
const defaultMessages = require("./cn.js")
const i18nSettings = require("./settings.js")
const formatters = require("../formatters")
// 自动创建全局VoerkaI18n实例
if(!globalThis.VoerkaI18n){
globalThis.VoerkaI18n = new i18n(i18nSettings)
}
let scope = {
defaultLanguage: "cn", // 默认语言名称
default: defaultMessages, // 默认语言包
messages : defaultMessages, // 当前语言包
idMap:messageIds, // 消息id映射列表
formatters, // 当前作用域的格式化函数列表
loaders:{}, // 异步加载语言文件的函数列表
global:{} // 引用全局VoerkaI18n配置
}
let supportedlanguages = {}
messages["cn"]= defaultMessages
scope.loaders["en"] = ()=>import("./en.js")
scope.loaders["de"] = ()=>import("./de.js")
scope.loaders["jp"] = ()=>import("./jp.js")
const t = ()=> translate.bind(scope)(...arguments)
// 注册当前作用域到全局VoerkaI18n实例
VoerkaI18n.register(scope)
module.exports.scope = scope
module.exports.t = t

View File

@ -1,27 +0,0 @@
module.exports = {
"1": "a1:aaaaa",
"2": "no aaaaa",
"3": "bbbbb",
"4": "eeeeee",
"5": "x:from a",
"6": "中华人民共和国",
"7": "a2:aaaaa",
"8": "no erer",
"9": "no aaareraa",
"10": "no fdfd",
"11": "fdgfdgfdg",
"12": "aaaaa",
"13": "aaaaa1",
"14": "aaaaa2",
"15": "aaaaa 3",
"16": "ccccc",
"17": "cccccdc",
"18": "ddddd中国",
"19": "html-a",
"20": "html-b",
"21": "html-c",
"22": "from a",
"23": "from b",
"24": "from c1",
"25": "from c2"
}

View File

@ -1,3 +0,0 @@
{
"type": "module"
}

View File

@ -1,31 +0,0 @@
export default {
"languages": [
{
"name": "en",
"title": "英文"
},
{
"name": "cn",
"title": "中文",
"default": true
},
{
"name": "de",
"title": "德语"
},
{
"name": "jp",
"title": "日本語"
}
],
"defaultLanguage": "cn",
"activeLanguage": "cn",
"namespaces": {
"a": [
"a"
],
"b": [
"b"
]
}
}

View File

@ -1,91 +0,0 @@
{
"a1:aaaaa": {
"en": "a1:aaaaa",
"de": "德文:aaaaa",
"jp": "a1:aaaaa",
"$file": [
"a\\a1.js"
]
},
"no aaaaa": {
"en": "no aaaaa",
"de": "no aaaaa",
"jp": "no aaaaa",
"$file": [
"a\\a1.js"
]
},
"bbbbb": {
"en": "bbbbb",
"de": "bbbbb",
"jp": "bbbbb",
"$file": [
"a\\a1.js"
]
},
"eeeeee": {
"en": "eeeeee",
"de": "eeeeee",
"jp": "eeeeee",
"$file": [
"a\\a1.js",
"a\\a2.js"
]
},
"x:from a": {
"en": "x:from a",
"de": "x:from a",
"jp": "x:from a",
"$file": [
"a\\a1.js"
]
},
"中华人民共和国": {
"en": "中华人民共和国",
"de": "中华人民共和国",
"jp": "中华人民共和国",
"$file": [
"a\\a1.js"
]
},
"a2:aaaaa": {
"en": "a2:aaaaa",
"de": "a2:aaaaa",
"jp": "a2:aaaaa",
"$file": [
"a\\a2.js"
]
},
"no erer": {
"en": "no erer",
"de": "no erer",
"jp": "no erer",
"$file": [
"a\\a2.js"
]
},
"no aaareraa": {
"en": "no aaareraa",
"de": "no aaareraa",
"jp": "no aaareraa",
"$file": [
"a\\a2.js"
]
},
"no fdfd": {
"en": "no fdfd",
"de": "no fdfd",
"jp": "no fdfd",
"$file": [
"a\\a2.js"
]
},
"fdgfdgfdg": {
"en": "fdgfdgfdg",
"de": "fdgfdgfdg",
"jp": "fdgfdgfdg",
"$file": [
"a\\a2.js"
]
}
}

View File

@ -1,26 +0,0 @@
{
"aaaaa": {
"en": "aaaaa",
"de": "aaaaa",
"jp": "aaaaa",
"$file": [
"b\\b1.js"
]
},
"bbbbb": {
"en": "bbbbb",
"de": "bbbbb",
"jp": "bbbbb",
"$file": [
"b\\b1.js"
]
},
"eeeeee": {
"en": "eeeeee",
"de": "eeeeee",
"jp": "eeeeee",
"$file": [
"b\\b1.js"
]
}
}

View File

@ -1,139 +0,0 @@
{
"aaaaa": {
"en": "aaaaa",
"de": "aaaaa",
"jp": "aaaaa",
"$file": [
"index.js"
]
},
"aaaaa1": {
"en": "aaaaa1",
"de": "aaaaa1",
"jp": "aaaaa1",
"$file": [
"index.js"
]
},
"aaaaa2": {
"en": "aaaaa2",
"de": "aaaaa2",
"jp": "aaaaa2",
"$file": [
"index.js"
]
},
"aaaaa 3": {
"en": "aaaaa 3",
"de": "aaaaa 3",
"jp": "aaaaa 3",
"$file": [
"index.js"
]
},
"bbbbb": {
"en": "bbbbb",
"de": "bbbbb",
"jp": "bbbbb",
"$file": [
"index.js"
]
},
"eeeeee": {
"en": "eeeeee",
"de": "eeeeee",
"jp": "eeeeee",
"$file": [
"index.js",
"c\\c2.js"
]
},
"ccccc": {
"en": "ccccc",
"de": "ccccc",
"jp": "ccccc",
"$file": [
"c\\c1.js"
]
},
"cccccdc": {
"en": "cccccdc",
"de": "cccccdc",
"jp": "cccccdc",
"$file": [
"c\\c1.js"
]
},
"a2:aaaaa": {
"en": "a2:aaaaa",
"de": "a2:aaaaa",
"jp": "a2:aaaaa",
"$file": [
"c\\c2.js"
]
},
"no erer": {
"en": "no erer",
"de": "no erer",
"jp": "no erer",
"$file": [
"c\\c2.js"
]
},
"no aaareraa": {
"en": "no aaareraa",
"de": "no aaareraa",
"jp": "no aaareraa",
"$file": [
"c\\c2.js"
]
},
"no fdfd": {
"en": "no fdfd",
"de": "no fdfd",
"jp": "no fdfd",
"$file": [
"c\\c2.js"
]
},
"fdgfdgfdg": {
"en": "fdgfdgfdg",
"de": "fdgfdgfdg",
"jp": "fdgfdgfdg",
"$file": [
"c\\c2.js"
]
},
"ddddd中国": {
"en": "ddddd中国",
"de": "ddddd中国",
"jp": "ddddd中国",
"$file": [
"c\\c2.js"
]
},
"html-a": {
"en": "html-a",
"de": "html-a",
"jp": "html-a",
"$file": [
"c\\h1.html"
]
},
"html-b": {
"en": "html-b",
"de": "html-b",
"jp": "html-b",
"$file": [
"c\\h1.html"
]
},
"html-c": {
"en": "html-c",
"de": "html-c",
"jp": "html-c",
"$file": [
"c\\h1.html"
]
}
}

View File

@ -1,34 +0,0 @@
{
"from a": {
"en": "from a",
"de": "from a",
"jp": "from a",
"$file": [
"a\\a2.js"
]
},
"from b": {
"en": "from b",
"de": "from b",
"jp": "from b",
"$file": [
"b\\b1.js"
]
},
"from c1": {
"en": "from c1",
"de": "from c1",
"jp": "from c1",
"$file": [
"c\\c1.js"
]
},
"from c2": {
"en": "from c2",
"de": "from c2",
"jp": "from c2",
"$file": [
"c\\c2.js"
]
}
}

View File

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

View File

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

View File

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

View File

@ -5,25 +5,28 @@
"main": "index.js",
"scripts": {
"test": "jest",
"extract": "",
"babeldemo": "babel ./demodata/a/a1.js --plugins=./src/babel-plugin-voerkai18n.js"
"test:babelplugin": "jest babel",
"test:extract": "jest extract",
"test:translate": "jest translate",
"demo:extract": "node ./packages/demo/extract.demo.js",
"demo:compile": "node ./packages/demo/compile.demo.js",
"demo:babel": "babel ./demodata/a/a1.js --plugins=./src/babel-plugin-voerkai18n.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"art-template": "^4.13.2",
"devDependencies": {
"@babel/core": "^7.17.5",
"@babel/plugin-transform-runtime": "^7.17.0",
"@babel/preset-env": "^7.16.11",
"@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-commonjs": "^21.0.2",
"dayjs": "^1.10.8",
"deepmerge": "^4.2.2",
"glob": "^7.2.0",
"gulp": "^4.0.2",
"logsets": "^1.0.6",
"readjson": "^2.2.2",
"through2": "^4.0.2"
},
"devDependencies": {
"@babel/cli": "^7.17.6",
"@babel/core": "^7.17.5",
"jest": "^27.5.1",
"rollup": "^2.69.0",
"rollup-plugin-clear": "^2.0.7",
"rollup-plugin-uglify": "^6.0.4",
"vinyl": "^2.2.1"
}
}

View File

@ -1,6 +1,7 @@
// import {t } from "./languages"
// import { nanoid } from "./nanoid"
//const {t } = reuire("./languages")
const { t,languages,scope } = reuire("./languages")
function output(){
t("aaaaa")

View File

@ -1,7 +1,7 @@
const babel = require("@babel/core");
const fs = require("fs");
const path = require("path");
const i18nPlugin = require("../src/babel-plugin-voerkai18n");
const i18nPlugin = require("@voerkai18n/tools/babel-plugin-voerkai18n");
const code = fs.readFileSync(path.join(__dirname, "./apps/app/index.js"), "utf-8");

View File

@ -0,0 +1,7 @@
const compile = require('@voerkai18n/tools/compile');
const path = require("path")
compile(path.resolve(__dirname,"./apps/app/languages")).then(()=>{})

View File

@ -1,5 +1,5 @@
const gulp = require('gulp');
const extract = require('../src/extract.plugin');
const extract = require('@voerkai18n/tools/extract.plugin');
const path = require('path');
@ -13,7 +13,15 @@ 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:'fr',title:"法语"},
{name:'es',title:"西班牙语"},
{name:'it',title:"意大利语"},
{name:'jp',title:"日語"}
],
// extractor:{
// default:[new RegExp()], // 默认匹配器,当文件类型没有对应的提取器时使用
// "*" : [new RegExp()], // 所有类型均会执行的提取器

View File

@ -0,0 +1,20 @@
{
"name": "@voerkai18n/demo",
"version": "1.0.0",
"description": "",
"main": "babel.plugin.demo.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"@voerkai18n/runtime": "workspace:^1.0.0",
"@voerkai18n/tools": "workspace:^1.0.0"
},
"devDependencies": {
"deepmerge": "^4.2.2",
"gulp": "^4.0.2",
"vinyl": "^2.2.1"
}
}

View File

@ -7,5 +7,10 @@
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
"license": "ISC",
"devDependencies": {
"deepmerge": "^4.2.2",
"gulp": "^4.0.2",
"vinyl": "^2.2.1"
}
}

View File

@ -0,0 +1,16 @@
{
"name": "@voerkai18n/react",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"deepmerge": "^4.2.2",
"gulp": "^4.0.2",
"vinyl": "^2.2.1"
}
}

View File

@ -0,0 +1,60 @@
/**
* 内置的格式化器
*
*/
/**
* 字典格式化器
* 根据输入data的值返回后续参数匹配的结果
* dict(data,<value1>,<result1>,<value2>,<result1>,<value3>,<result1>,...)
*
*
* dict(1,1,"one",2,"two",3,"three"4,"four") == "one"
* dict(2,1,"one",2,"two",3,"three"4,"four") == "two"
* dict(3,1,"one",2,"two",3,"three"4,"four") == "three"
* dict(4,1,"one",2,"two",3,"three"4,"four") == "four"
* // 无匹配时返回原始值
* dict(5,1,"one",2,"two",3,"three"4,"four") == 5
* // 无匹配时并且后续参数个数是奇数,则返回最后一个参数
* dict(5,1,"one",2,"two",3,"three"4,"four","more") == "more"
*
* 在翻译中使用
* I have { value | dict(1,"one",2,"two",3,"three",4,"four")} apples
*
* @param {*} value
* @param {...any} args
* @returns
*/
function dict(value,...args){
for(let i=0;i<args.length;i+=2){
if(args[i]===value){
return args[i+1]
}
}
if(args.length >0 && (args.length % 2!==0)) return args[args.length-1]
return value
}
module.exports = {
"*":{
$types:{
Date:(value)=>value.toLocaleString()
},
time:(value)=> value.toLocaleTimeString(),
date: (value)=> value.toLocaleDateString(),
dict, //字典格式化器
},
cn:{
$types:{
Date:(value)=> `${value.getFullYear()}${value.getMonth()+1}${value.getDate()}${value.getHours()}${value.getMinutes()}${value.getSeconds()}`
},
time:(value)=>`${value.getHours()}${value.getMinutes()}${value.getSeconds()}`,
date: (value)=> `${value.getFullYear()}${value.getMonth()+1}${value.getDate()}`,
currency:(value)=>`${value}`,
},
en:{
currency:(value)=>`$${value}`,
}
}

View File

@ -49,7 +49,7 @@ function hasInterpolation(str){
目前对参数采用简单的split(",")来解析因为无法正确解析aaa(1,1,"dd,,dd")形式的参数
在此场景下基本够用了如果需要支持更复杂的参数解析可以后续考虑使用正则表达式来解析
@returns [<formatterName>,[<formatterName>,[<arg>,<arg>,...]]]
@returns [[<formatterName>,[<arg>,<arg>,...]]]
*/
function parseFormatters(formatters){
if(!formatters) return []
@ -89,29 +89,51 @@ function parseFormatters(formatters){
/**
* 提取字符串中的插值变量
* // [
// {
name:<变量名称>,formatters:[{name:<格式化器名称>,args:[<参数>,<参数>,....]]],<匹配字符串>],
// ....
//
* @param {*} str
* @param {*} isFull =true 保留所有插值变量 =false 进行去重
* @returns {Array} [[变量名称,[],match],[变量名称,[formatter,formatter,...],match],...]
* @returns {Array}
* [
* {
* name:"<变量名称>",
* formatters:[
* {name:"<格式化器名称>",args:[<参数>,<参数>,....]},
* {name:"<格式化器名称>",args:[<参数>,<参数>,....]},
* ],
* match:"<匹配字符串>"
* },
* ...
* ]
*/
function getInterpolatedVars(str){
let results = [], match
while ((match = varWithPipeRegexp.exec(str)) !== null) {
if (match.index === varWithPipeRegexp.lastIndex) {
varWithPipeRegexp.lastIndex++;
}
const varname = match.groups.varname || ""
// 解析格式化器和参数 = [<formatterName>,[<formatterName>,[<arg>,<arg>,...]]]
const formatters = parseFormatters(match.groups.formatters)
const varInfo = [varname,formatters,match]
results.push(varInfo)
}
return results
let vars = []
forEachInterpolatedVars(str,(varName,formatters,match)=>{
let varItem = {
name:varName,
formatters:formatters.map(([formatter,args])=>{
return {
name:formatter,
args:args
}
}),
match:match
}
if(vars.findIndex(varDef=>((varDef.name===varItem.name) && (varItem.formatters.toString() == varDef.formatters.toString())))===-1){
vars.push(varItem)
}
return ""
})
return vars
}
/**
* 遍历插值变量并进行替换插值变量
* 遍历str中的所有插值变量传递给callback将callback返回的结果替换到str中对应的位置
* @param {*} str
* @param {*} callback
* @returns
* @param {Function(<变量名称>,[formatters],match[0])} callback
* @returns 返回替换后的字符串
*/
function forEachInterpolatedVars(str,callback,options={}){
let result=str, match
@ -126,9 +148,9 @@ function forEachInterpolatedVars(str,callback,options={}){
if(typeof(callback)==="function"){
try{
if(opts.replaceAll){
result=result.replaceAll(match[0],callback(varname,formatters))
result=result.replaceAll(match[0],callback(varname,formatters,match[0]))
}else{
result=result.replace(match[0],callback(varname,formatters))
result=result.replace(match[0],callback(varname,formatters,match[0]))
}
}catch{// callback函数可能会抛出异常如果抛出异常则中断匹配过程
break
@ -443,12 +465,11 @@ function translate(message) {
}else if(arguments.length >= 2){
vars = [...arguments].splice(1).map((arg,index)=>{
try{
return typeof(arg)==="function" ? arg() : arg
}catch(e){
return arg
}
// 位置参数中以第一个数值变量为复数变量
if(isNumber(arg)) pluraValue = parseInt(arg)
arg = typeof(arg)==="function" ? arg() : arg
// 位置参数中以第一个数值变量为复数变量
if(isNumber(arg)) pluraValue = parseInt(arg)
}catch(e){ }
return arg
})
}
@ -475,7 +496,7 @@ function translate(message) {
// 如果是数组说明要启用复数机制,需要根据插值变量中的某个变量来判断复数形式
if(Array.isArray(content) && content.length>0){
// 如果存在复数命名变量,只取第一个复数变量
if(pluraValue){ // 启用的是位置插值,pluraIndex=第一个数字变量的位置
if(pluraValue!==null){ // 启用的是位置插值,pluraIndex=第一个数字变量的位置
content = getPluraMessage(content,pluraValue)
}else if(pluralVar.length>0){
content = getPluraMessage(content,parseInt(vars(pluralVar[0])))
@ -509,7 +530,7 @@ function translate(message) {
* VoerkaI18n.off("change",(language)=>{})
*
* */
class I18n{
class I18nManager{
static instance = null; // 单例引用
callbacks = [] // 当切换语言时的回调事件
constructor(settings={}){
@ -650,7 +671,7 @@ function translate(message) {
module.exports ={
getInterpolatedVars,
replaceInterpolatedVars,
I18n,
I18nManager,
translate,
languages,
defaultLanguageSettings

View File

@ -1,11 +1,14 @@
{
"name": "@voerkai18n/runtime",
"version": "1.0.0",
"description": "",
"description": "Voerkai18n Runtime",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
"license": "ISC",
"devDependencies": {
"deepmerge": "^4.2.2"
}
}

View File

@ -0,0 +1,33 @@
import clear from 'rollup-plugin-clear'
import { uglify } from "rollup-plugin-uglify";
import { babel } from '@rollup/plugin-babel';
import commonjs from '@rollup/plugin-commonjs';
export default [
{
input: './index.js',
output: [
{
file: 'dist/index.mjs',
format:"es"
},
{
file: 'dist/index.cjs',
exports:"default",
format:"cjs"
}
],
plugins: [
//resolve(),
commonjs(),
babel({
babelHelpers:"runtime",
exclude: 'node_modules/**'
}),
clear({targets:["dist"]}),
uglify()
],
external:["@babel/runtime"]
}
]

View File

@ -31,32 +31,11 @@ function isPlainObject(obj){
function isNumber(value){
return !isNaN(parseInt(value))
}
/**
* 支持导入cjs和esm模块
* @param {*} url
*/
async function importModule(url){
try{
return require(url)
}catch(e){
// 当加载出错时尝试加载esm模块
if(e.code === "MODULE_NOT_FOUND"){
return await import(url)
}else{
throw e
}
}
}
module.exports = {
getDataTypeName,
isNumber,
isPlainObject,
importModule
isPlainObject
}

View File

@ -19,7 +19,7 @@
const fs = require("fs");
const path = require("path");
const { isPlainObject } = require("./utils");
const { isPlainObject } = require("../runtime/utils");
const DefaultI18nPluginOptions = {
translateFunctionName:"t", // 默认的翻译函数名称

View File

@ -27,7 +27,7 @@ const readJson = require("readjson")
const glob = require("glob")
const createLogger = require("logsets")
const path = require("path")
const { getMessageId } = require("./utils")
const { importModule } = require("./utils")
const fs = require("fs")
const logger = createLogger()
const artTemplate = require("art-template")
@ -40,19 +40,30 @@ function normalizeCompileOptions(opts={}) {
}, opts)
if(options.moduleType==="es") options.moduleType = "esm"
if(options.moduleType==="cjs") options.moduleType = "commonjs"
if(["commonjs","cjs","esm","es"].includes(options.moduleType)) options.moduleType = "esm"
return opts;
if(!["commonjs","cjs","esm","es"].includes(options.moduleType)) options.moduleType = "esm"
return options;
}
module.exports = function compile(langFolder,opts={}){
module.exports =async function compile(langFolder,opts={}){
const options = normalizeCompileOptions(opts);
const { output,moduleType } = options;
//1. 加载多语言配置文件
import(`file:///${path.join(langFolder,"settings.js")}`).then(module=>{
// 加载多语言配置文件
const settingsFile = path.join(langFolder,"settings.js")
try{
// 读取多语言配置文件
const module =await importModule(`file:///${settingsFile}`)
const langSettings = module.default;
let { languages,defaultLanguage,activeLanguage,namespaces } = langSettings
logger.log("支持的语言\t: {}",languages.map(item=>`${item.title}(${item.name})`))
logger.log("默认语言\t: {}",defaultLanguage)
logger.log("激活语言\t: {}",activeLanguage)
logger.log("名称空间\t: {}",Object.keys(namespaces).join(","))
logger.log("模块类型\t: {}",moduleType)
logger.log("")
logger.log("编译结果输出至:{}",langFolder)
// 1. 合并生成最终的语言文件
let messages = {} ,msgId =1
glob.sync(path.join(langFolder,"translates/*.json")).forEach(file=>{
@ -69,6 +80,8 @@ module.exports = function compile(langFolder,opts={}){
logger.log("读取语言文件{}失败:{}",file,e.message)
}
})
logger.log(" - 合成语言包文本,共{}条",Object.keys(messages).length)
// 2. 为每一个文本内容生成一个唯一的id
let messageIds = {}
Object.entries(messages).forEach(([msg,langs])=>{
@ -88,6 +101,7 @@ module.exports = function compile(langFolder,opts={}){
}else{
fs.writeFileSync(langFile,`module.exports = ${JSON.stringify(langMessages,null,4)}`)
}
logger.log(" - 语言包文件: {}",path.basename(langFile))
})
// 4. 生成id映射文件
@ -97,29 +111,56 @@ module.exports = function compile(langFolder,opts={}){
}else{
fs.writeFileSync(idMapFile,`module.exports = ${JSON.stringify(messageIds,null,4)}`)
}
logger.log(" - idMap文件: {}",path.basename(idMapFile))
// 5. 生成编译后的访问入口文件
const entryFile = path.join(langFolder,"index.js")
const entryContent = artTemplate(path.join(__dirname,"templates","entry.js"), {languages,defaultLanguage,activeLanguage,namespaces,moduleType } )
const entryContent = artTemplate(path.join(__dirname,"templates","entry.js"), {languages,defaultLanguage,activeLanguage,namespaces,moduleType,JSON } )
fs.writeFileSync(entryFile,entryContent)
logger.log(" - 访问入口文件: {}",path.basename(entryFile))
// 6 . 生成编译后的格式化函数文件
const formattersFile = path.join(langFolder,"formatters.js")
if(!fs.existsSync(formattersFile)){
const formattersContent = artTemplate(path.join(__dirname,"templates","formatters.js"), {languages,defaultLanguage,activeLanguage,namespaces,moduleType } )
fs.writeFileSync(formattersFile,formattersContent)
}
// 7. 生成package.json
logger.log(" - 格式化器:{}",path.basename(formattersFile))
}else{ // 格式化器如果存在,则需要更改对应的模块类型
let formattersContent = fs.readFileSync(formattersFile,"utf8").toString()
if(moduleType == "esm"){
formattersContent = formattersContent.replaceAll(/\s*module.exports\s*\=/g,"export default ")
formattersContent = formattersContent.replaceAll(/\s*module.exports\./g,"export ")
}else{
formattersContent = formattersContent.replaceAll(/\s*export\s*default\s*/g,"module.exports = ")
formattersContent = formattersContent.replaceAll(/\s*export\s*/g,"module.exports.")
}
fs.writeFileSync(formattersFile,formattersContent)
logger.log(" - 更新格式化器:{}",path.basename(formattersFile))
}
// 7. 重新生成settings ,需要确保settings.js匹配模块类型
if(moduleType==="esm"){
fs.writeFileSync(settingsFile,`export default ${JSON.stringify(langSettings,null,4)}`)
}else{
fs.writeFileSync(settingsFile,`module.exports = ${JSON.stringify(langSettings,null,4)}`)
}
// 8. 生成package.json
const packageJsonFile = path.join(langFolder,"package.json")
let packageJson = {}
if(moduleType==="esm"){
packageJson = {
version:"1.0.0",
type:"module",
}
}else{
packageJson = {
version:"1.0.0",
}
}
fs.writeFileSync(packageJsonFile,JSON.stringify(packageJson,null,4))
})
}catch(e){
logger.log("加载多语言配置文件<{}>失败: {} ",settingsFile,e.message)
}
}

View File

@ -11,7 +11,7 @@ const path = require('path')
const fs = require('fs')
const readJson = require("readjson")
const createLogger = require("logsets")
const { replaceInterpolateVars,getDataTypeName } = require("./utils")
const { replaceInterpolateVars,getDataTypeName } = require("../runtime/utils")
const logger = createLogger()
// 捕获翻译文本的默认正则表达式
@ -177,18 +177,23 @@ function getTranslateTexts(content,file,options){
},texts)
}
const defaultExtractLanguages = [
{name:'en',title:"英文"},
{name:'cn',title:"中文",default:true},
{name:'de',title:"德语"},
{name:'fr',title:"法语"},
{name:'es',title:"西班牙语"},
{name:'it',title:"意大利语"},
{name:'jp',title:"日語"}
]
function normalizeLanguageOptions(options){
options = Object.assign({
debug : true, // 输出调试信息,控制台输出相关的信息
languages : [ // 支持的语言列表
{name:"en",title:"英文"},
{name:"cn",title:"中文",active:true,default:true} // 通过default指定默认语言
],
languages :defaultExtractLanguages, // 默认要支持的语言
defaultLanguage: "cn", // 默认语言:指的是在源代码中的原始文本语言
activeLanguage : "cn", // 当前激活语言:指的是当前启用的语言,比如在源码中使用中文,在默认激活的是英文
extractor : { // 匹配翻译函数并提取内容的正则表达式
//default : DefaultTranslateExtractor,
"*" : DefaultTranslateExtractor,
"html,vue,jsx" : DefaultHtmlAttrExtractor
},
@ -359,14 +364,8 @@ module.exports = function(options={}){
if(!outputPath){
outputPath = path.join(file.base,"languages")
}
// 跳过空文件
if(file.isNull()){
return callback()
}
// 跳过流文件
if(file.isStream()){
return callback()
}
if(file.isNull()) return callback()
if(file.isStream()) return callback()
// 提取翻译文本
try{
@ -375,10 +374,10 @@ module.exports = function(options={}){
fileCount++
if(debug){
const textCount = Object.values(texts).reduce((sum,item)=>sum+Object.keys(item).length,0)
logger.log("Extract <{}>, found [{}] namespaces and {} texts.",file.relative,Object.keys(texts).join(),textCount)
logger.log("Extract <{}>, found [{}] namespaces and {} messages.",file.relative,Object.keys(texts).join(),textCount)
}
}catch(err){
logger.log("Error while extract text from <{}> : {}",file.relative,err.message)
logger.log("Error while extract messages from <{}> : {}",file.relative,err.message)
}
callback()
@ -388,6 +387,7 @@ module.exports = function(options={}){
logger.log(" - Total of files\t: {}",fileCount)
logger.log(" - Output location\t: {}",outputPath)
const translatesPath = path.join(outputPath,"translates")
if(!fs.existsSync(outputPath)) fs.mkdirSync(outputPath)
if(!fs.existsSync(translatesPath)) fs.mkdirSync(translatesPath)
// 每个名称空间对应一个文件
for(let [namespace,texts] of Object.entries(results)){
@ -402,16 +402,20 @@ module.exports = function(options={}){
logger.log(" Save language file : {}",path.relative(outputPath,langFile))
}
}
// 将元数据生成到 i18n.meta.json
const metaFile = path.join(outputPath,"settings.js")
const meta = {
languages : options.languages,
defaultLanguage: options.defaultLanguage,
activeLanguage : options.activeLanguage,
namespaces : options.namespaces
}
fs.writeFileSync(metaFile,`export default ${JSON.stringify(meta,null,4)}`)
logger.log(" - Generate language metadata : {}",metaFile)
// 生成语言配置文件 settings.js , 仅当不存在时才生成
const settingsFile = path.join(outputPath,"settings.js")
if(!fs.existsSync(settingsFile)){
const settings = {
languages : options.languages,
defaultLanguage: options.defaultLanguage,
activeLanguage : options.activeLanguage,
namespaces : options.namespaces
}
fs.writeFileSync(settingsFile,`module.exports = ${JSON.stringify(settings,null,4)}`)
logger.log(" - Generate settings of language : {}",settingsFile)
}else{
logger.log(" - Settings of language already exists : {}",settingsFile)
}
callback()
});
}

23
packages/tools/index.js Normal file
View File

@ -0,0 +1,23 @@
const { Command } = require('commander');
const program = new Command();
program
.option('-d, --debug', '输出调试信息')
program
.command('extract <source> [destination]')
.description('扫描指定的项目目录,提取文件中的国际化字符串')
.option('-d, --debug', '输出调试信息')
.option('-l, --languages', '支持的语言', 'cn,en,de,fr,es,it,jp')
.option('-d, --default', '默认语言', 'cn')
.option('-a, --active', '激活语言', 'cn')
.action((source, destination) => {
console.log('clone command called');
});
program.parse(process.argv);
const options = program.opts();

View File

@ -0,0 +1,26 @@
{
"name": "@voerkai18n/tools",
"version": "1.0.0",
"description": "VoerkaI18n Tools",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"@babel/cli": "^7.17.6",
"@babel/core": "^7.17.5",
"art-template": "^4.13.2",
"commander": "^9.0.0",
"glob": "^7.2.0",
"logsets": "^1.0.6",
"readjson": "^2.2.2",
"through2": "^4.0.2"
},
"devDependencies": {
"deepmerge": "^4.2.2",
"gulp": "^4.0.2",
"vinyl": "^2.2.1"
}
}

View File

@ -1,20 +1,18 @@
{{if moduleTye === "esm"}}
{{if moduleType === "esm"}}
import messageIds from "./idMap.js"
import { translate,i18n } from "voerka-i18n"
import { translate,I18nManager } from "@voerkai18n/runtime"
import defaultMessages from "./{{defaultLanguage}}.js"
import scopeSettings from "./settings.js"
import formatters from "../formatters"
{{else}}
const messageIds = require("./idMap")
const { translate,i18n } = require("voerka-i18n")
const { translate,i18n } = require("@voerkai18n/runtime")
const defaultMessages = require("./{{defaultLanguage}}.js")
const scopeSettings = require("./settings.js")
const formatters = require("../formatters")
const scopeSettings = require("./settings.js")
{{/if}}
// 自动创建全局VoerkaI18n实例
if(!globalThis.VoerkaI18n){
globalThis.VoerkaI18n = new i18n(scopeSettings)
globalThis.VoerkaI18n = new I18nManager(scopeSettings)
}
let scope = {
@ -22,7 +20,7 @@ let scope = {
default: defaultMessages, // 默认语言包
messages : defaultMessages, // 当前语言包
idMap:messageIds, // 消息id映射列表
formatters, // 当前作用域的格式化函数列表
formatters:{}, // 当前作用域的格式化函数列表
loaders:{}, // 异步加载语言文件的函数列表
global:{} // 引用全局VoerkaI18n配置注册后自动引用
}
@ -35,14 +33,16 @@ scope.loaders["{{$value.name}}"] = ()=>import("./{{$value.name}}.js")
{{/if}}{{/each}}
const t = ()=> translate.bind(scope)(...arguments)
const languages = {{@ JSON.stringify(languages,null,4) }}
// 注册当前作用域到全局VoerkaI18n实例
VoerkaI18n.register(scope)
{{if moduleTye === "esm"}}
{{if moduleType === "esm"}}
export languages
export scope
export t
{{else}}
module.exports.languages = languages
module.exports.scope = scope
module.exports.t = t
{{/if}}

View File

@ -76,7 +76,7 @@ const formatters = {
{{/each}}
}
{{if moduleTye === "esm"}}
{{if moduleType === "esm"}}
export default formatters
{{else}}
module.exports = formatters

15
packages/tools/utils.js Normal file
View File

@ -0,0 +1,15 @@
async function importModule(url){
try{
return require(url)
}catch{
return await import(url)
}
}
module.exports = {
importModule
}

View File

@ -1,5 +1,5 @@
{
"name": "@voerkai18n/scripts",
"name": "@voerkai18n/vue",
"version": "1.0.0",
"description": "",
"main": "index.js",
@ -7,5 +7,10 @@
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
"license": "ISC",
"devDependencies": {
"deepmerge": "^4.2.2",
"gulp": "^4.0.2",
"vinyl": "^2.2.1"
}
}

1916
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -151,7 +151,7 @@ t("第{}章",100) // == Chapter 100
## 插值变量格式化
voerka-i18n支持对插值变量进行格式化
{{value | filter}} 过滤器语法类似管道操作符,它的上一个输出作为下一个输入。
```javascript
new VoerkaI18n({
@ -195,6 +195,31 @@ new VoerkaI18n({
t("今天是{date}",{date:new Date()})
```
### 字典
当翻译内容是一个{}时,启用字典插值模式。
```javascript
// 源文件
// 假设网络状态取值0=初始化,1=正在连接2=已连接,3=正在断开.4=已断开,>4=未知
t("当前状态:{status}",{status})
// translates/default.json
{
"当前状态:{status}":{
cn:{0:"初始化",1:"正在连接"2:"已连接",3:"正在断开",4:"已断开",unknow:"未知"},
en:{
to:"Status:{}",
vars:{"Init","Connecting","Connected","Disconnecting","Disconnected","unknow"}
},
en:"Status : {status | dict({0:"Init",1:"Connecting",2:"Connected",3:"Disconnecting",4:"Disconnected",5:"unknow"})}"
}
}
```

View File

@ -1,26 +0,0 @@
/**
* 内置的格式化器
*
*/
module.exports = {
"*":{
$types:{
Date:(value)=>value.toLocaleString()
},
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()}`
},
time:(value)=>`${value.getHours()}${value.getMinutes()}${value.getSeconds()}`,
date: (value)=> `${value.getFullYear()}${value.getMonth()+1}${value.getDate()}`,
currency:(value)=>`${value}`,
},
en:{
currency:(value)=>`$${value}`,
}
}

13
test/app.test.js Normal file
View File

@ -0,0 +1,13 @@
/**
* 测试demp app的语言运行环境
*/
const compile = require('../packages/tools/compile');
const path = require("path")
test("导入多语言包",async ()=>{
await compile(path.resolve(__dirname,'../packages/demo/apps/app/languages'),{moduleType:"commonjs"})
const { t,scope,languages } = require(path.join(__dirname,'../packages/demo/apps/app/languages/index.js'));
expect(t).toBeFunction()
})

View File

@ -1,7 +1,16 @@
/**
* 测试babel-plugin-voerkai18n插件
*
*
* babel-plugin-voerkai18n插件可以根据idMap.js文件将源文件中的t函数翻译内容转化为对应的id
* 并且可以自动导入t函数
*
*/
const babel = require("@babel/core");
const fs = require("fs");
const path = require("path");
const i18nPlugin = require("../src/babel-plugin-voerkai18n");
const i18nPlugin = require("../packages/tools/babel-plugin-voerkai18n");
const code = `
function test(a,b){
@ -32,7 +41,8 @@ test("翻译函数转换",done=>{
// 可以指定相对路径,也可以指定绝对路径
autoImport:"#/languages",
moduleType:"esm",
// 此参数仅仅用于单元测试时使用,正常情况下会读取location文件夹下的idMap", idMap:{
// 此参数仅仅用于单元测试时使用,正常情况下会读取location文件夹下的idMap",
idMap:{
"a":1,
"b":2,
"c{}{}":3,
@ -53,7 +63,7 @@ test("读取esm格式的idMap后进行翻译函数转换",done=>{
[
i18nPlugin,
{
location:path.join(__dirname, "../demo/apps/lib1/languages"),
location:path.join(__dirname, "../packages/demo/apps/lib1/languages"),
autoImport:"#/languages",
moduleType:"esm",
}
@ -70,7 +80,7 @@ test("读取commonjs格式的idMap后进行翻译函数转换",done=>{
[
i18nPlugin,
{
location:path.join(__dirname, "../demo/apps/lib2/languages"),
location:path.join(__dirname, "../packages/demo/apps/lib2/languages"),
autoImport:"#/languages",
moduleType:"esm",
}

View File

@ -1,8 +1,8 @@
const extract = require("../src/extract.plugin");
const extract = require("../packages/tools/extract.plugin");
const gulp = require('gulp');
const path = require('path');
const Vinyl = require('vinyl');
const { getTranslateTexts, normalizeLanguageOptions } = require("../src/extract.plugin");
const { getTranslateTexts, normalizeLanguageOptions } = require("../packages/tools/extract.plugin");
const languages = [{name:'en',title:"英文"},{name:'cn',title:"中文",default:true},{name:'de',title:"德语"},{name:'jp',title:"日本語"}]

View File

@ -1,23 +1,26 @@
const dayjs = require('dayjs');
const { getInterpolatedVars, replaceInterpolatedVars , translate} = require('../src/index.js')
const { getInterpolatedVars, replaceInterpolatedVars , translate} = require('../packages/runtime/index.js')
const messages = {
cn:{
1:"你好",
2:"现在是{}",
3:"我出生于{year}年,今年{age}岁",
4:"我有{}个朋友",
},
en :{
1:"hello",
2:"Now is {}",
3:"I was born in {year}, now is {age} years old",
4:["I have no friends","I have one friends","I have two friends","I have {} friends"],
}
}
const idMap = {
"你好":1,
"现在是{}":2,
"我出生于{year}年,今年{age}岁":3
"我出生于{year}年,今年{age}岁":3,
"我有{}个朋友":4
}
@ -99,24 +102,48 @@ beforeEach(() => {
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([]);
const results = getInterpolatedVars("中华人民共和国成立于{date | year(1,2) | time('a','b') | rel }年,首都是{city}市");
expect(results.length).toEqual(2);
//
expect(results[0].name).toEqual("date");
expect(results[0].formatters.length).toEqual(3);
// year(1,2)
expect(results[0].formatters[0].name).toEqual("year");
expect(results[0].formatters[0].args).toEqual([1,2]);
// time('a','b')
expect(results[0].formatters[1].name).toEqual("time");
expect(results[0].formatters[1].args).toEqual(["a","b"]);
// rel
expect(results[0].formatters[2].name).toEqual("rel");
expect(results[0].formatters[2].args).toEqual([]);
expect(results[1].name).toEqual("city");
expect(results[1].formatters.length).toEqual(0);
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"]);
expect(results[0].name).toEqual("a");
expect(results[0].formatters.length).toEqual(0);
expect(results[1].name).toEqual("a");
expect(results[1].formatters.length).toEqual(1);
expect(results[1].formatters[0].name).toEqual("x");
expect(results[1].formatters[0].args).toEqual([]);
expect(results[2].name).toEqual("a");
expect(results[2].formatters.length).toEqual(2);
expect(results[2].formatters[0].name).toEqual("x");
expect(results[2].formatters[0].args).toEqual([]);
expect(results[2].formatters[1].name).toEqual("y");
expect(results[2].formatters[1].args).toEqual([]);
done()
})
@ -223,3 +250,14 @@ test("切换到未知语言",done=>{
done()
})
test("翻译复数支持",done=>{
changeLanguage("en")
expect(t("我有{}个朋友",0)).toBe("I have no friends");
expect(t("我有{}个朋友",1)).toBe("I have one friends");
expect(t("我有{}个朋友",2)).toBe("I have two friends");
expect(t("我有{}个朋友",3)).toBe("I have 3 friends");
expect(t("我有{}个朋友",4)).toBe("I have 4 friends");
done()
})