update
This commit is contained in:
parent
afa4f1a184
commit
f4cf83cfea
@ -4,6 +4,7 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"build:runtime":"pnpm build --filter \"@voerkai18n/runtime\"",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:babelplugin": "jest babel",
|
"test:babelplugin": "jest babel",
|
||||||
"test:extract": "jest extract",
|
"test:extract": "jest extract",
|
||||||
@ -26,7 +27,6 @@
|
|||||||
"jest": "^27.5.1",
|
"jest": "^27.5.1",
|
||||||
"rollup": "^2.69.0",
|
"rollup": "^2.69.0",
|
||||||
"rollup-plugin-clear": "^2.0.7",
|
"rollup-plugin-clear": "^2.0.7",
|
||||||
"rollup-plugin-uglify": "^6.0.4",
|
|
||||||
"vinyl": "^2.2.1"
|
"vinyl": "^2.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
t("a1:aaaaa")
|
|
||||||
|
|
||||||
t('no aaaaa')
|
|
||||||
t("no aaaaa")
|
|
||||||
t('no aaaaa')
|
|
||||||
|
|
||||||
t('bbbbb',a,b,c)
|
|
||||||
|
|
||||||
t("cccc Arrow Function",()=>{},c)
|
|
||||||
|
|
||||||
|
|
||||||
t("eeeeee", )
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
t("x:from a")
|
|
||||||
|
|
||||||
|
|
||||||
t("中华人民共和国")
|
|
@ -1,18 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
t("a2:aaaaa")
|
|
||||||
|
|
||||||
t('no erer')
|
|
||||||
t("no aaareraa")
|
|
||||||
t('no fdfd')
|
|
||||||
|
|
||||||
t('fdgfdgfdg',a,b,c)
|
|
||||||
|
|
||||||
t("cccc Arrow Function",()=>{},c)
|
|
||||||
|
|
||||||
|
|
||||||
t("eeeeee", )
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
t("x::from a")
|
|
15
packages/demo/apps/app/auth/changepassword.js
Normal file
15
packages/demo/apps/app/auth/changepassword.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
|
||||||
|
t("请输入旧密码:")
|
||||||
|
|
||||||
|
t("请再次输入旧密码:")
|
||||||
|
|
||||||
|
|
||||||
|
t("请输入新密码:")
|
||||||
|
|
||||||
|
t("请再次输入新密码:")
|
||||||
|
|
||||||
|
t("密码至少需要6位,并且至少包含数字、字符或特殊符号中的两种")
|
||||||
|
|
||||||
|
t("密码强度: {strength}",2)
|
||||||
|
|
7
packages/demo/apps/app/auth/login.html
Normal file
7
packages/demo/apps/app/auth/login.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
<a data-i18n="登录" src="sdds" ></a>
|
||||||
|
<input data-i18n="请输入用户名:"></input>
|
||||||
|
<input data-i18n="请输入密码:"></input>
|
||||||
|
<img data-i18n="头像" />
|
||||||
|
<img data-i18n="相片" />
|
6
packages/demo/apps/app/auth/login.js
Normal file
6
packages/demo/apps/app/auth/login.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
t("用户名或密码错误");
|
||||||
|
t('请输入用户名:')
|
||||||
|
t("请输入密码:");
|
||||||
|
t("欢迎您: {}","张三丰")
|
@ -1,16 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
t("aaaaa")
|
|
||||||
|
|
||||||
|
|
||||||
t('bbbbb',a,b,c)
|
|
||||||
|
|
||||||
t("cccc",()=>{},c)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
t("ddddd中国",()=>{},c)
|
|
||||||
t("eeeeee")
|
|
||||||
|
|
||||||
t("ddddd中国",()=>{},c)
|
|
||||||
t("x::from b")
|
|
@ -1,9 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
t("ccccc")
|
|
||||||
|
|
||||||
t('cccccdc')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
t("x::from c1",ddd)
|
|
@ -1,20 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
t("a2:aaaaa")
|
|
||||||
|
|
||||||
t('no erer')
|
|
||||||
t("no aaareraa")
|
|
||||||
t('no fdfd')
|
|
||||||
|
|
||||||
t('fdgfdgfdg',a,b,c)
|
|
||||||
|
|
||||||
t("cccc Arrow Function",()=>{},c)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
t("ddddd中国",c)
|
|
||||||
t("eeeeee", )
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
t("x::from c2")
|
|
@ -1,5 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
<a data-i18n="html-a" src="sdds" ></a>
|
|
||||||
<img src="sdds" data-i18n="html-b" />
|
|
||||||
<img data-i18n="html-c" src="sdds" />
|
|
7
packages/demo/apps/app/db/index.js
Normal file
7
packages/demo/apps/app/db/index.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
t('数据库类型:{}、{}、{}',"sqlite","mysql","mssql");
|
||||||
|
|
||||||
|
t("数据库密码:{pwd}","123456");
|
||||||
|
t("数据库地址:{url}","");
|
||||||
|
|
||||||
|
t("编码:{encode}","utf-8");
|
||||||
|
|
20
packages/demo/apps/app/db/models.js
Normal file
20
packages/demo/apps/app/db/models.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {Table, Column} from "typeorm";
|
||||||
|
|
||||||
|
@Table()
|
||||||
|
export class Photo {
|
||||||
|
|
||||||
|
@Column(t("编码"))
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column(t("名称"))
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column(t("描述"))
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@Column(t("文件名称"))
|
||||||
|
fileName: string;
|
||||||
|
|
||||||
|
@Column("视图")
|
||||||
|
views: number;
|
||||||
|
}
|
@ -1,21 +1,26 @@
|
|||||||
const { t,languages,scope } = reuire("./languages")
|
import { t,languages,scope } from "./languages/index.js"
|
||||||
|
|
||||||
|
VoerkaI18n.on((language)=>{
|
||||||
|
console.log("切换到语言:",language)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function output(){
|
||||||
|
console.log(t("用户名或密码错误"))
|
||||||
function output(){
|
console.log(t('请输入用户名:'))
|
||||||
t("aaaaa")
|
console.log(t("请输入密码:"))
|
||||||
|
console.log(t("欢迎您: {}","张三丰"))
|
||||||
t('aaaaa1')
|
console.log("----------------")
|
||||||
t("aaaaa2")
|
await VoerkaI18n.change("en")
|
||||||
t('aaaaa 3')
|
console.log(t("用户名或密码错误"))
|
||||||
|
console.log(t('请输入用户名:'))
|
||||||
t('bbbbb',a,b,c)
|
console.log(t("请输入密码:"))
|
||||||
|
console.log(t("欢迎您: {}","tom"))
|
||||||
t("cccc",()=>{},c)
|
console.log("----------------")
|
||||||
|
await VoerkaI18n.change("cn")
|
||||||
|
console.log(t("用户名或密码错误"))
|
||||||
|
console.log(t('请输入用户名:'))
|
||||||
t("ddddd中国",()=>{},c)
|
console.log(t("请输入密码:"))
|
||||||
t("eeeeee", )
|
console.log(t("欢迎您: {}","tom"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output().then(()=>{})
|
5
packages/demo/apps/app/messages/index.js
Normal file
5
packages/demo/apps/app/messages/index.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
|
||||||
|
t("您有{}条未读消息",100)
|
||||||
|
t("消息总数:{$count}",100)
|
||||||
|
t("消息类型:{type}",1)
|
15
packages/demo/apps/app/package.json
Normal file
15
packages/demo/apps/app/package.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@voerkai18n/tools": "workspace:^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
@ -27,12 +27,14 @@
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function dict(value,...args){
|
function dict(value,...args){
|
||||||
for(let i=0;i<args.length;i+=2){
|
try{
|
||||||
if(args[i]===value){
|
for(let i=0;i<args.length;i+=2){
|
||||||
return args[i+1]
|
if(args[i]===value){
|
||||||
|
return args[i+1]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
if(args.length >0 && (args.length % 2!==0)) return args[args.length-1]
|
||||||
if(args.length >0 && (args.length % 2!==0)) return args[args.length-1]
|
}catch{}
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
883
packages/runtime/index.cjs
Normal file
883
packages/runtime/index.cjs
Normal file
@ -0,0 +1,883 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var isMergeableObject = function isMergeableObject(value) {
|
||||||
|
return isNonNullObject(value)
|
||||||
|
&& !isSpecial(value)
|
||||||
|
};
|
||||||
|
|
||||||
|
function isNonNullObject(value) {
|
||||||
|
return !!value && typeof value === 'object'
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSpecial(value) {
|
||||||
|
var stringValue = Object.prototype.toString.call(value);
|
||||||
|
|
||||||
|
return stringValue === '[object RegExp]'
|
||||||
|
|| stringValue === '[object Date]'
|
||||||
|
|| isReactElement(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// see https://github.com/facebook/react/blob/b5ac963fb791d1298e7f396236383bc955f916c1/src/isomorphic/classic/element/ReactElement.js#L21-L25
|
||||||
|
var canUseSymbol = typeof Symbol === 'function' && Symbol.for;
|
||||||
|
var REACT_ELEMENT_TYPE = canUseSymbol ? Symbol.for('react.element') : 0xeac7;
|
||||||
|
|
||||||
|
function isReactElement(value) {
|
||||||
|
return value.$$typeof === REACT_ELEMENT_TYPE
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyTarget(val) {
|
||||||
|
return Array.isArray(val) ? [] : {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneUnlessOtherwiseSpecified(value, options) {
|
||||||
|
return (options.clone !== false && options.isMergeableObject(value))
|
||||||
|
? deepmerge(emptyTarget(value), value, options)
|
||||||
|
: value
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultArrayMerge(target, source, options) {
|
||||||
|
return target.concat(source).map(function(element) {
|
||||||
|
return cloneUnlessOtherwiseSpecified(element, options)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMergeFunction(key, options) {
|
||||||
|
if (!options.customMerge) {
|
||||||
|
return deepmerge
|
||||||
|
}
|
||||||
|
var customMerge = options.customMerge(key);
|
||||||
|
return typeof customMerge === 'function' ? customMerge : deepmerge
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEnumerableOwnPropertySymbols(target) {
|
||||||
|
return Object.getOwnPropertySymbols
|
||||||
|
? Object.getOwnPropertySymbols(target).filter(function(symbol) {
|
||||||
|
return target.propertyIsEnumerable(symbol)
|
||||||
|
})
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
|
||||||
|
function getKeys(target) {
|
||||||
|
return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target))
|
||||||
|
}
|
||||||
|
|
||||||
|
function propertyIsOnObject(object, property) {
|
||||||
|
try {
|
||||||
|
return property in object
|
||||||
|
} catch(_) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protects from prototype poisoning and unexpected merging up the prototype chain.
|
||||||
|
function propertyIsUnsafe(target, key) {
|
||||||
|
return propertyIsOnObject(target, key) // Properties are safe to merge if they don't exist in the target yet,
|
||||||
|
&& !(Object.hasOwnProperty.call(target, key) // unsafe if they exist up the prototype chain,
|
||||||
|
&& Object.propertyIsEnumerable.call(target, key)) // and also unsafe if they're nonenumerable.
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeObject(target, source, options) {
|
||||||
|
var destination = {};
|
||||||
|
if (options.isMergeableObject(target)) {
|
||||||
|
getKeys(target).forEach(function(key) {
|
||||||
|
destination[key] = cloneUnlessOtherwiseSpecified(target[key], options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getKeys(source).forEach(function(key) {
|
||||||
|
if (propertyIsUnsafe(target, key)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (propertyIsOnObject(target, key) && options.isMergeableObject(source[key])) {
|
||||||
|
destination[key] = getMergeFunction(key, options)(target[key], source[key], options);
|
||||||
|
} else {
|
||||||
|
destination[key] = cloneUnlessOtherwiseSpecified(source[key], options);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return destination
|
||||||
|
}
|
||||||
|
|
||||||
|
function deepmerge(target, source, options) {
|
||||||
|
options = options || {};
|
||||||
|
options.arrayMerge = options.arrayMerge || defaultArrayMerge;
|
||||||
|
options.isMergeableObject = options.isMergeableObject || isMergeableObject;
|
||||||
|
// cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge()
|
||||||
|
// implementations can use it. The caller may not replace it.
|
||||||
|
options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified;
|
||||||
|
|
||||||
|
var sourceIsArray = Array.isArray(source);
|
||||||
|
var targetIsArray = Array.isArray(target);
|
||||||
|
var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray;
|
||||||
|
|
||||||
|
if (!sourceAndTargetTypesMatch) {
|
||||||
|
return cloneUnlessOtherwiseSpecified(source, options)
|
||||||
|
} else if (sourceIsArray) {
|
||||||
|
return options.arrayMerge(target, source, options)
|
||||||
|
} else {
|
||||||
|
return mergeObject(target, source, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deepmerge.all = function deepmergeAll(array, options) {
|
||||||
|
if (!Array.isArray(array)) {
|
||||||
|
throw new Error('first argument should be an array')
|
||||||
|
}
|
||||||
|
|
||||||
|
return array.reduce(function(prev, next) {
|
||||||
|
return deepmerge(prev, next, options)
|
||||||
|
}, {})
|
||||||
|
};
|
||||||
|
|
||||||
|
var deepmerge_1 = deepmerge;
|
||||||
|
|
||||||
|
var cjs = deepmerge_1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内置的格式化器
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典格式化器
|
||||||
|
* 根据输入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){
|
||||||
|
try{
|
||||||
|
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]
|
||||||
|
}catch{}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var formatters$1 = {
|
||||||
|
"*":{
|
||||||
|
$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}`,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定变量类型名称
|
||||||
|
* getDataTypeName(1) == Number
|
||||||
|
* getDataTypeName("") == String
|
||||||
|
* getDataTypeName(null) == Null
|
||||||
|
* getDataTypeName(undefined) == Undefined
|
||||||
|
* getDataTypeName(new Date()) == Date
|
||||||
|
* getDataTypeName(new Error()) == Error
|
||||||
|
*
|
||||||
|
* @param {*} v
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function getDataTypeName$1(v){
|
||||||
|
if (v === null) return 'Null'
|
||||||
|
if (v === undefined) return 'Undefined'
|
||||||
|
if(typeof(v)==="function") return "Function"
|
||||||
|
return v.constructor && v.constructor.name;
|
||||||
|
}function isPlainObject$1(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$1(value){
|
||||||
|
return !isNaN(parseInt(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
var utils = {
|
||||||
|
getDataTypeName: getDataTypeName$1,
|
||||||
|
isNumber: isNumber$1,
|
||||||
|
isPlainObject: isPlainObject$1
|
||||||
|
};
|
||||||
|
|
||||||
|
const deepMerge = cjs;
|
||||||
|
const formatters = formatters$1;
|
||||||
|
const {isPlainObject ,isNumber , getDataTypeName} = utils;
|
||||||
|
|
||||||
|
// 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter }
|
||||||
|
// 不支持参数: let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*\s*)*)\s*\}/g
|
||||||
|
|
||||||
|
// 支持参数: { var | formatter(x,x,..) | formatter }
|
||||||
|
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"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 考虑到通过正则表达式进行插件的替换可能较慢,因此提供一个简单方法来过滤掉那些
|
||||||
|
* 不需要进行插值处理的字符串
|
||||||
|
* 原理很简单,就是判断一下是否同时具有{和}字符,如果有则认为可能有插值变量,如果没有则一定没有插件变量,则就不需要进行正则匹配
|
||||||
|
* 从而可以减少不要的正则匹配
|
||||||
|
* 注意:该方法只能快速判断一个字符串不包括插值变量
|
||||||
|
* @param {*} str
|
||||||
|
* @returns {boolean} true=可能包含插值变量,
|
||||||
|
*/
|
||||||
|
function hasInterpolation(str){
|
||||||
|
return str.includes("{") && str.includes("}")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
通过正则表达式对原始文本内容进行解析匹配后得到的
|
||||||
|
formatters="| aaa(1,1) | bbb "
|
||||||
|
|
||||||
|
需要统一解析为
|
||||||
|
|
||||||
|
[
|
||||||
|
[aaa,[1,1]], // [formatter'name,[args,...]]
|
||||||
|
[bbb,[]],
|
||||||
|
]
|
||||||
|
|
||||||
|
formatters="| aaa(1,1,"dddd") | bbb "
|
||||||
|
|
||||||
|
目前对参数采用简单的split(",")来解析,因为无法正确解析aaa(1,1,"dd,,dd")形式的参数
|
||||||
|
在此场景下基本够用了,如果需要支持更复杂的参数解析,可以后续考虑使用正则表达式来解析
|
||||||
|
|
||||||
|
@returns [[<formatterName>,[<arg>,<arg>,...]]]
|
||||||
|
*/
|
||||||
|
function parseFormatters(formatters){
|
||||||
|
if(!formatters) return []
|
||||||
|
// 1. 先解析为 ["aaa()","bbb"]形式
|
||||||
|
let result = formatters.trim().substr(1).trim().split("|").map(r=>r.trim());
|
||||||
|
|
||||||
|
// 2. 解析格式化器参数
|
||||||
|
return result.map(formatter=>{
|
||||||
|
let firstIndex = formatter.indexOf("(");
|
||||||
|
let lastIndex = formatter.lastIndexOf(")");
|
||||||
|
if(firstIndex!==-1 && lastIndex!==-1){ // 带参数的格式化器
|
||||||
|
const argsContent = formatter.substr(firstIndex+1,lastIndex-firstIndex-1).trim();
|
||||||
|
let args = argsContent=="" ? [] : argsContent.split(",").map(arg=>{
|
||||||
|
arg = arg.trim();
|
||||||
|
if(!isNaN(parseInt(arg))){
|
||||||
|
return parseInt(arg) // 数字
|
||||||
|
}else if((arg.startsWith('\"') && arg.endsWith('\"')) || (arg.startsWith('\'') && arg.endsWith('\'')) ){
|
||||||
|
return arg.substr(1,arg.length-2) // 字符串
|
||||||
|
}else if(arg.toLowerCase()==="true" || arg.toLowerCase()==="false"){
|
||||||
|
return arg.toLowerCase()==="true" // 布尔值
|
||||||
|
}else if((arg.startsWith('{') && arg.endsWith('}')) || (arg.startsWith('[') && arg.endsWith(']'))){
|
||||||
|
try{
|
||||||
|
return JSON.parse(arg)
|
||||||
|
}catch(e){
|
||||||
|
return String(arg)
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
return String(arg)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return [formatter.substr(0,firstIndex),args]
|
||||||
|
}else {// 不带参数的格式化器
|
||||||
|
return [formatter,[]]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取字符串中的插值变量
|
||||||
|
* // [
|
||||||
|
// {
|
||||||
|
name:<变量名称>,formatters:[{name:<格式化器名称>,args:[<参数>,<参数>,....]]}],<匹配字符串>],
|
||||||
|
// ....
|
||||||
|
//
|
||||||
|
* @param {*} str
|
||||||
|
* @param {*} isFull =true 保留所有插值变量 =false 进行去重
|
||||||
|
* @returns {Array}
|
||||||
|
* [
|
||||||
|
* {
|
||||||
|
* name:"<变量名称>",
|
||||||
|
* formatters:[
|
||||||
|
* {name:"<格式化器名称>",args:[<参数>,<参数>,....]},
|
||||||
|
* {name:"<格式化器名称>",args:[<参数>,<参数>,....]},
|
||||||
|
* ],
|
||||||
|
* match:"<匹配字符串>"
|
||||||
|
* },
|
||||||
|
* ...
|
||||||
|
* ]
|
||||||
|
*/
|
||||||
|
function getInterpolatedVars(str){
|
||||||
|
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 {Function(<变量名称>,[formatters],match[0])} callback
|
||||||
|
* @returns 返回替换后的字符串
|
||||||
|
*/
|
||||||
|
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 || "";
|
||||||
|
// 解析格式化器和参数 = [<formatterName>,[<formatterName>,[<arg>,<arg>,...]]]
|
||||||
|
const formatters = parseFormatters(match.groups.formatters);
|
||||||
|
if(typeof(callback)==="function"){
|
||||||
|
try{
|
||||||
|
if(opts.replaceAll){
|
||||||
|
result=result.replaceAll(match[0],callback(varname,formatters,match[0]));
|
||||||
|
}else {
|
||||||
|
result=result.replace(match[0],callback(varname,formatters,match[0]));
|
||||||
|
}
|
||||||
|
}catch{// callback函数可能会抛出异常,如果抛出异常,则中断匹配过程
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
varWithPipeRegexp.lastIndex=0;
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 缓存数据类型的格式化器,避免每次都调用getDataTypeDefaultFormatter
|
||||||
|
let datatypeFormattersCache ={
|
||||||
|
$activeLanguage:null,
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 取得指定数据类型的默认格式化器
|
||||||
|
*
|
||||||
|
* 可以为每一个数据类型指定一个格式化器,当传入插值变量时,会自动调用该格式化器来对值进行格式化转换
|
||||||
|
|
||||||
|
const formatters = {
|
||||||
|
"*":{
|
||||||
|
$types:{...} // 在所有语言下只作用于特定数据类型的格式化器
|
||||||
|
}, // 在所有语言下生效的格式化器
|
||||||
|
cn:{
|
||||||
|
$types:{
|
||||||
|
[数据类型]:(value)=>{...},
|
||||||
|
},
|
||||||
|
[格式化器名称]:(value)=>{...},
|
||||||
|
[格式化器名称]:(value)=>{...},
|
||||||
|
[格式化器名称]:(value)=>{...},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
* @param {*} scope
|
||||||
|
* @param {*} activeLanguage
|
||||||
|
* @param {*} dataType 数字类型
|
||||||
|
* @returns {Function} 格式化函数
|
||||||
|
*/
|
||||||
|
function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){
|
||||||
|
if(datatypeFormattersCache.$activeLanguage === activeLanguage) {
|
||||||
|
if(dataType in datatypeFormattersCache) return datatypeFormattersCache[dataType]
|
||||||
|
}else {// 清空缓存
|
||||||
|
datatypeFormattersCache = { $activeLanguage:activeLanguage };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先在当前作用域中查找,再在全局查找
|
||||||
|
const targets = [scope.formatters,scope.global.formatters];
|
||||||
|
for(const target of targets){
|
||||||
|
if(activeLanguage in target){
|
||||||
|
// 在当前语言的$types中查找
|
||||||
|
let formatters = target[activeLanguage].$types || {};
|
||||||
|
for(let [name,formatter] of Object.entries(formatters)){
|
||||||
|
if(name === dataType && typeof(formatter)==="function") {
|
||||||
|
datatypeFormattersCache[dataType] = formatter;
|
||||||
|
return formatter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 在所有语言的$types中查找
|
||||||
|
let formatters = target["*"].$types || {};
|
||||||
|
for(let [name,formatter] of Object.entries(formatters)){
|
||||||
|
if(name === dataType && typeof(formatter)==="function") {
|
||||||
|
datatypeFormattersCache[dataType] = formatter;
|
||||||
|
return formatter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定名称的格式化器函数
|
||||||
|
* @param {*} scope
|
||||||
|
* @param {*} activeLanguage
|
||||||
|
* @param {*} name 格式化器名称
|
||||||
|
* @returns {Function} 格式化函数
|
||||||
|
*/
|
||||||
|
let formattersCache = { $activeLanguage:null};
|
||||||
|
function getFormatter(scope,activeLanguage,name){
|
||||||
|
if(formattersCache.$activeLanguage === activeLanguage) {
|
||||||
|
if(name in formattersCache) return formattersCache[name]
|
||||||
|
}else { // 当切换语言时需要清空缓存
|
||||||
|
formattersCache = { $activeLanguage:activeLanguage };
|
||||||
|
}
|
||||||
|
// 先在当前作用域中查找,再在全局查找
|
||||||
|
const targets = [scope.formatters,scope.global.formatters];
|
||||||
|
for(const target of targets){
|
||||||
|
// 优先在当前语言查找
|
||||||
|
if(activeLanguage in target){
|
||||||
|
let formatters = target[activeLanguage] || {};
|
||||||
|
if((name in formatters) && typeof(formatters[name])==="function") return formattersCache[name] = formatters[name]
|
||||||
|
}
|
||||||
|
// 在所有语言的$types中查找
|
||||||
|
let formatters = target["*"] || {};
|
||||||
|
if((name in formatters) && typeof(formatters[name])==="function") return formattersCache[name] = formatters[name]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行格式化器并返回结果
|
||||||
|
* @param {*} value
|
||||||
|
* @param {*} formatters
|
||||||
|
*/
|
||||||
|
function executeFormatter(value,formatters){
|
||||||
|
if(formatters.length===0) return value
|
||||||
|
let result = value;
|
||||||
|
try{
|
||||||
|
for(let formatter of formatters){
|
||||||
|
if(typeof(formatter) === "function") {
|
||||||
|
result = formatter(result);
|
||||||
|
}else {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}catch(e){
|
||||||
|
console.error(`Error while execute i18n formatter for ${value}: ${e.message} ` );
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 将 [[格式化器名称,[参数,参数,...]],[格式化器名称,[参数,参数,...]]]格式化器转化为
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param {*} scope
|
||||||
|
* @param {*} activeLanguage
|
||||||
|
* @param {*} formatters
|
||||||
|
*/
|
||||||
|
function buildFormatters(scope,activeLanguage,formatters){
|
||||||
|
let results = [];
|
||||||
|
for(let formatter of formatters){
|
||||||
|
if(formatter[0]){
|
||||||
|
const func = getFormatter(scope,activeLanguage,formatter[0]);
|
||||||
|
if(typeof(func)==="function"){
|
||||||
|
results.push((v)=>{
|
||||||
|
return func(v,...formatter[1])
|
||||||
|
});
|
||||||
|
}else {// 格式化器无效或者没有定义时,查看当前值是否具有同名的方法,如果有则执行调用
|
||||||
|
results.push((v)=>{
|
||||||
|
if(typeof(v[formatter[0]])==="function"){
|
||||||
|
return v[formatter[0]].call(v,...formatter[1])
|
||||||
|
}else {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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("<模板字符串>",[变量值,变量值,...])
|
||||||
|
* replaceInterpolatedVars("<模板字符串>",变量值,变量值,...])
|
||||||
|
*
|
||||||
|
- 当只有两个参数并且第2个参数是{}时,将第2个参数视为命名变量的字典
|
||||||
|
replaceInterpolatedVars("this is {a}+{b},{a:1,b:2}) --> this is 1+2
|
||||||
|
- 当只有两个参数并且第2个参数是[]时,将第2个参数视为位置参数
|
||||||
|
replaceInterpolatedVars"this is {}+{}",[1,2]) --> this is 1+2
|
||||||
|
- 普通位置参数替换
|
||||||
|
replaceInterpolatedVars("this is {a}+{b}",1,2) --> this is 1+2
|
||||||
|
-
|
||||||
|
this == scope == { formatters: {}, ... }
|
||||||
|
* @param {*} template
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function replaceInterpolatedVars(template,...args) {
|
||||||
|
const scope = this;
|
||||||
|
// 当前激活语言
|
||||||
|
const activeLanguage = scope.global.activeLanguage;
|
||||||
|
|
||||||
|
// 没有变量插值则的返回原字符串
|
||||||
|
if(args.length===0 || !hasInterpolation(template)) return template
|
||||||
|
|
||||||
|
// ****************************变量插值****************************
|
||||||
|
if(args.length===1 && isPlainObject(args[0])){
|
||||||
|
// 读取模板字符串中的插值变量列表
|
||||||
|
// [[var1,[formatter,formatter,...],match],[var2,[formatter,formatter,...],match],...}
|
||||||
|
let varValues = args[0];
|
||||||
|
return forEachInterpolatedVars(template,(varname,formatters)=>{
|
||||||
|
let value = (varname in varValues) ? varValues[varname] : '';
|
||||||
|
return getFormattedValue(scope,activeLanguage,formatters,value)
|
||||||
|
})
|
||||||
|
}else {
|
||||||
|
// ****************************位置插值****************************
|
||||||
|
// 如果只有一个Array参数,则认为是位置变量列表,进行展开
|
||||||
|
const params=(args.length===1 && Array.isArray(args[0])) ? [...args[0]] : args;
|
||||||
|
if(params.length===0) return template // 没有变量则不需要进行插值处理,返回原字符串
|
||||||
|
let i = 0;
|
||||||
|
return forEachInterpolatedVars(template,(varname,formatters)=>{
|
||||||
|
if(params.length>i){
|
||||||
|
return getFormattedValue(scope,activeLanguage,formatters,params[i++])
|
||||||
|
}else {
|
||||||
|
throw new Error() // 抛出异常,停止插值处理
|
||||||
|
}
|
||||||
|
},{replaceAll:false})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认语言配置
|
||||||
|
const defaultLanguageSettings = {
|
||||||
|
defaultLanguage: "cn",
|
||||||
|
activeLanguage: "cn",
|
||||||
|
languages:{
|
||||||
|
cn:{name:"cn",title:"中文",default:true},
|
||||||
|
en:{name:"en",title:"英文"},
|
||||||
|
},
|
||||||
|
formatters
|
||||||
|
};
|
||||||
|
|
||||||
|
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("total {$count} items", {$count:1}) //复数形式
|
||||||
|
* translate("total {} {} {} items",a,b,c) // 位置变量插值
|
||||||
|
*
|
||||||
|
* this===scope 当前绑定的scope
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function translate(message) {
|
||||||
|
const scope = this;
|
||||||
|
const activeLanguage = scope.global.activeLanguage;
|
||||||
|
let content = message;
|
||||||
|
let vars=[]; // 插值变量列表
|
||||||
|
let pluralVars= []; // 复数变量
|
||||||
|
let pluraValue = null; // 复数值
|
||||||
|
try{
|
||||||
|
// 1. 预处理变量: 复数变量保存至pluralVars中 , 变量如果是Function则调用
|
||||||
|
if(arguments.length === 2 && isPlainObject(arguments[1])){
|
||||||
|
Object.entries(arguments[1]).forEach(([name,value])=>{
|
||||||
|
if(typeof(value)==="function"){
|
||||||
|
try{
|
||||||
|
vars[name] = value();
|
||||||
|
}catch(e){
|
||||||
|
vars[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 以$开头的视为复数变量
|
||||||
|
if(name.startsWith("$")) pluralVars.push(name);
|
||||||
|
});
|
||||||
|
vars = [arguments[1]];
|
||||||
|
}else if(arguments.length >= 2){
|
||||||
|
vars = [...arguments].splice(1).map((arg,index)=>{
|
||||||
|
try{
|
||||||
|
arg = typeof(arg)==="function" ? arg() : arg;
|
||||||
|
// 位置参数中以第一个数值变量为复数变量
|
||||||
|
if(isNumber(arg)) pluraValue = parseInt(arg);
|
||||||
|
}catch(e){ }
|
||||||
|
return arg
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 取得翻译文本模板字符串
|
||||||
|
if(activeLanguage === scope.defaultLanguage){
|
||||||
|
// 2.1 从默认语言中取得翻译文本模板字符串
|
||||||
|
// 如果当前语言就是默认语言,不需要查询加载,只需要做插值变换即可
|
||||||
|
// 当源文件运用了babel插件后会将原始文本内容转换为msgId
|
||||||
|
// 如果是msgId则从scope.default中读取,scope.default=默认语言包={<id>:<message>}
|
||||||
|
if(isMessageId(content)){
|
||||||
|
content = scope.default[content] || 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!==null){ // 启用的是位置插值,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 content // 出错则返回原始文本
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多语言管理类
|
||||||
|
*
|
||||||
|
* 当导入编译后的多语言文件时(import("./languages")),会自动生成全局实例VoerkaI18n
|
||||||
|
*
|
||||||
|
* VoerkaI18n.languages // 返回支持的语言列表
|
||||||
|
* VoerkaI18n.defaultLanguage // 默认语言
|
||||||
|
* VoerkaI18n.language // 当前语言
|
||||||
|
* VoerkaI18n.change(language) // 切换到新的语言
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* VoerkaI18n.on("change",(language)=>{}) // 注册语言切换事件
|
||||||
|
* VoerkaI18n.off("change",(language)=>{})
|
||||||
|
*
|
||||||
|
* */
|
||||||
|
class I18nManager{
|
||||||
|
static instance = null; // 单例引用
|
||||||
|
callbacks = [] // 当切换语言时的回调事件
|
||||||
|
constructor(settings={}){
|
||||||
|
if(I18nManager.instance!=null){
|
||||||
|
return I18nManager.instance;
|
||||||
|
}
|
||||||
|
I18nManager.instance = this;
|
||||||
|
this._settings = deepMerge(defaultLanguageSettings,settings);
|
||||||
|
this._scopes=[];
|
||||||
|
return I18nManager.instance;
|
||||||
|
}
|
||||||
|
get settings(){ return this._settings }
|
||||||
|
get scopes(){ return this._scopes }
|
||||||
|
// 当前激活语言
|
||||||
|
get activeLanguage(){ return this._settings.activeLanguage}
|
||||||
|
// 默认语言
|
||||||
|
get defaultLanguage(){ return this.this._settings.defaultLanguage}
|
||||||
|
// 支持的语言列表
|
||||||
|
get languages(){ return this._settings.languages}
|
||||||
|
// 订阅语言切换事件
|
||||||
|
on(callback){
|
||||||
|
this.callbacks.push(callback);
|
||||||
|
}
|
||||||
|
off(callback){
|
||||||
|
for(let i=0;i<this.callbacks.length;i++){
|
||||||
|
if(this.callbacks[i]===callback ){
|
||||||
|
this.callbacks.splice(i,1);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offAll(){
|
||||||
|
this.callbacks=[];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 切换语言时触发语言切换事件回调
|
||||||
|
*/
|
||||||
|
async _triggerChangeEvents(newLanguage){
|
||||||
|
try{
|
||||||
|
await this._updateScopes(newLanguage);
|
||||||
|
await (Promise.allSettled || Promise.all)(this.callbacks.map(async cb=>await cb(newLanguage)));
|
||||||
|
}catch(e){
|
||||||
|
console.warn("Error while executing language change events",e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 切换语言
|
||||||
|
*/
|
||||||
|
async change(value){
|
||||||
|
if(value in this.languages){
|
||||||
|
await this._triggerChangeEvents(value);
|
||||||
|
this._settings.activeLanguage = value;
|
||||||
|
}else {
|
||||||
|
throw new Error("Not supported language:"+value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取指定作用域的下的语言包加载器
|
||||||
|
*
|
||||||
|
* 同时会进行语言兼容性处理
|
||||||
|
*
|
||||||
|
* 如scope里面定义了一个cn的语言包,当切换到zh-cn时,会自动加载cn语言包
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param {*} scope
|
||||||
|
* @param {*} lang
|
||||||
|
*/
|
||||||
|
_getScopeLoader(scope,lang){
|
||||||
|
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 当切换语言时调用此方法来加载更新语言包
|
||||||
|
* @param {*} newLanguage
|
||||||
|
*/
|
||||||
|
async _updateScopes(newLanguage){
|
||||||
|
// 并发执行所有作用域语言包的加载
|
||||||
|
try{
|
||||||
|
await (Promise.allSettled || Promise.all)(this._scopes.map(scope=>{
|
||||||
|
return async ()=>{
|
||||||
|
// 默认语言,所有均默认语言均采用静态加载方式,只需要简单的替换即可
|
||||||
|
if(newLanguage === scope.defaultLanguage){
|
||||||
|
scope.messages = scope.default;
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 异步加载语言文件
|
||||||
|
const loader = scope.loaders[newLanguage];
|
||||||
|
if(typeof(loader) === "function"){
|
||||||
|
try{
|
||||||
|
scope.messages = await loader();
|
||||||
|
}catch(e){
|
||||||
|
console.warn(`Error loading language ${newLanguage} : ${e.message}`);
|
||||||
|
scope.messages = defaultMessages; // 出错时回退到默认语言
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
scope.messages = defaultMessages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}catch(e){
|
||||||
|
console.warn("Error while refreshing scope:",e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* 注册一个新的作用域
|
||||||
|
*
|
||||||
|
* 每一个库均对应一个作用域,每个作用域可以有多个语言包,且对应一个翻译函数
|
||||||
|
* scope={
|
||||||
|
* defaultLanguage:"cn",
|
||||||
|
default: defaultMessages, // 转换文本信息
|
||||||
|
messages : defaultMessages, // 当前语言的消息
|
||||||
|
idMap:messageIds,
|
||||||
|
formatters:{
|
||||||
|
...formatters,
|
||||||
|
...i18nSettings.formatters || {}
|
||||||
|
},
|
||||||
|
loaders:{}, // 异步加载语言文件的函数列表
|
||||||
|
settings:{} // 引用全局VoerkaI18n实例的配置
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* 除了默认语言外,其他语言采用动态加载的方式
|
||||||
|
*
|
||||||
|
* @param {*} scope
|
||||||
|
*/
|
||||||
|
register(scope){
|
||||||
|
scope.global = this._settings;
|
||||||
|
this._scopes.push(scope);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 注册全局格式化器
|
||||||
|
* @param {*} formatters
|
||||||
|
*/
|
||||||
|
registerFormatters(formatters){
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var runtime ={
|
||||||
|
getInterpolatedVars,
|
||||||
|
replaceInterpolatedVars,
|
||||||
|
I18nManager,
|
||||||
|
translate,
|
||||||
|
languages,
|
||||||
|
defaultLanguageSettings
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = runtime;
|
881
packages/runtime/index.esm.js
Normal file
881
packages/runtime/index.esm.js
Normal file
@ -0,0 +1,881 @@
|
|||||||
|
var isMergeableObject = function isMergeableObject(value) {
|
||||||
|
return isNonNullObject(value)
|
||||||
|
&& !isSpecial(value)
|
||||||
|
};
|
||||||
|
|
||||||
|
function isNonNullObject(value) {
|
||||||
|
return !!value && typeof value === 'object'
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSpecial(value) {
|
||||||
|
var stringValue = Object.prototype.toString.call(value);
|
||||||
|
|
||||||
|
return stringValue === '[object RegExp]'
|
||||||
|
|| stringValue === '[object Date]'
|
||||||
|
|| isReactElement(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// see https://github.com/facebook/react/blob/b5ac963fb791d1298e7f396236383bc955f916c1/src/isomorphic/classic/element/ReactElement.js#L21-L25
|
||||||
|
var canUseSymbol = typeof Symbol === 'function' && Symbol.for;
|
||||||
|
var REACT_ELEMENT_TYPE = canUseSymbol ? Symbol.for('react.element') : 0xeac7;
|
||||||
|
|
||||||
|
function isReactElement(value) {
|
||||||
|
return value.$$typeof === REACT_ELEMENT_TYPE
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyTarget(val) {
|
||||||
|
return Array.isArray(val) ? [] : {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneUnlessOtherwiseSpecified(value, options) {
|
||||||
|
return (options.clone !== false && options.isMergeableObject(value))
|
||||||
|
? deepmerge(emptyTarget(value), value, options)
|
||||||
|
: value
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultArrayMerge(target, source, options) {
|
||||||
|
return target.concat(source).map(function(element) {
|
||||||
|
return cloneUnlessOtherwiseSpecified(element, options)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMergeFunction(key, options) {
|
||||||
|
if (!options.customMerge) {
|
||||||
|
return deepmerge
|
||||||
|
}
|
||||||
|
var customMerge = options.customMerge(key);
|
||||||
|
return typeof customMerge === 'function' ? customMerge : deepmerge
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEnumerableOwnPropertySymbols(target) {
|
||||||
|
return Object.getOwnPropertySymbols
|
||||||
|
? Object.getOwnPropertySymbols(target).filter(function(symbol) {
|
||||||
|
return target.propertyIsEnumerable(symbol)
|
||||||
|
})
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
|
||||||
|
function getKeys(target) {
|
||||||
|
return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target))
|
||||||
|
}
|
||||||
|
|
||||||
|
function propertyIsOnObject(object, property) {
|
||||||
|
try {
|
||||||
|
return property in object
|
||||||
|
} catch(_) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protects from prototype poisoning and unexpected merging up the prototype chain.
|
||||||
|
function propertyIsUnsafe(target, key) {
|
||||||
|
return propertyIsOnObject(target, key) // Properties are safe to merge if they don't exist in the target yet,
|
||||||
|
&& !(Object.hasOwnProperty.call(target, key) // unsafe if they exist up the prototype chain,
|
||||||
|
&& Object.propertyIsEnumerable.call(target, key)) // and also unsafe if they're nonenumerable.
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeObject(target, source, options) {
|
||||||
|
var destination = {};
|
||||||
|
if (options.isMergeableObject(target)) {
|
||||||
|
getKeys(target).forEach(function(key) {
|
||||||
|
destination[key] = cloneUnlessOtherwiseSpecified(target[key], options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getKeys(source).forEach(function(key) {
|
||||||
|
if (propertyIsUnsafe(target, key)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (propertyIsOnObject(target, key) && options.isMergeableObject(source[key])) {
|
||||||
|
destination[key] = getMergeFunction(key, options)(target[key], source[key], options);
|
||||||
|
} else {
|
||||||
|
destination[key] = cloneUnlessOtherwiseSpecified(source[key], options);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return destination
|
||||||
|
}
|
||||||
|
|
||||||
|
function deepmerge(target, source, options) {
|
||||||
|
options = options || {};
|
||||||
|
options.arrayMerge = options.arrayMerge || defaultArrayMerge;
|
||||||
|
options.isMergeableObject = options.isMergeableObject || isMergeableObject;
|
||||||
|
// cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge()
|
||||||
|
// implementations can use it. The caller may not replace it.
|
||||||
|
options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified;
|
||||||
|
|
||||||
|
var sourceIsArray = Array.isArray(source);
|
||||||
|
var targetIsArray = Array.isArray(target);
|
||||||
|
var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray;
|
||||||
|
|
||||||
|
if (!sourceAndTargetTypesMatch) {
|
||||||
|
return cloneUnlessOtherwiseSpecified(source, options)
|
||||||
|
} else if (sourceIsArray) {
|
||||||
|
return options.arrayMerge(target, source, options)
|
||||||
|
} else {
|
||||||
|
return mergeObject(target, source, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deepmerge.all = function deepmergeAll(array, options) {
|
||||||
|
if (!Array.isArray(array)) {
|
||||||
|
throw new Error('first argument should be an array')
|
||||||
|
}
|
||||||
|
|
||||||
|
return array.reduce(function(prev, next) {
|
||||||
|
return deepmerge(prev, next, options)
|
||||||
|
}, {})
|
||||||
|
};
|
||||||
|
|
||||||
|
var deepmerge_1 = deepmerge;
|
||||||
|
|
||||||
|
var cjs = deepmerge_1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内置的格式化器
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典格式化器
|
||||||
|
* 根据输入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){
|
||||||
|
try{
|
||||||
|
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]
|
||||||
|
}catch{}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var formatters$1 = {
|
||||||
|
"*":{
|
||||||
|
$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}`,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定变量类型名称
|
||||||
|
* getDataTypeName(1) == Number
|
||||||
|
* getDataTypeName("") == String
|
||||||
|
* getDataTypeName(null) == Null
|
||||||
|
* getDataTypeName(undefined) == Undefined
|
||||||
|
* getDataTypeName(new Date()) == Date
|
||||||
|
* getDataTypeName(new Error()) == Error
|
||||||
|
*
|
||||||
|
* @param {*} v
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function getDataTypeName$1(v){
|
||||||
|
if (v === null) return 'Null'
|
||||||
|
if (v === undefined) return 'Undefined'
|
||||||
|
if(typeof(v)==="function") return "Function"
|
||||||
|
return v.constructor && v.constructor.name;
|
||||||
|
}function isPlainObject$1(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$1(value){
|
||||||
|
return !isNaN(parseInt(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
var utils = {
|
||||||
|
getDataTypeName: getDataTypeName$1,
|
||||||
|
isNumber: isNumber$1,
|
||||||
|
isPlainObject: isPlainObject$1
|
||||||
|
};
|
||||||
|
|
||||||
|
const deepMerge = cjs;
|
||||||
|
const formatters = formatters$1;
|
||||||
|
const {isPlainObject ,isNumber , getDataTypeName} = utils;
|
||||||
|
|
||||||
|
// 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter }
|
||||||
|
// 不支持参数: let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*\s*)*)\s*\}/g
|
||||||
|
|
||||||
|
// 支持参数: { var | formatter(x,x,..) | formatter }
|
||||||
|
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"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 考虑到通过正则表达式进行插件的替换可能较慢,因此提供一个简单方法来过滤掉那些
|
||||||
|
* 不需要进行插值处理的字符串
|
||||||
|
* 原理很简单,就是判断一下是否同时具有{和}字符,如果有则认为可能有插值变量,如果没有则一定没有插件变量,则就不需要进行正则匹配
|
||||||
|
* 从而可以减少不要的正则匹配
|
||||||
|
* 注意:该方法只能快速判断一个字符串不包括插值变量
|
||||||
|
* @param {*} str
|
||||||
|
* @returns {boolean} true=可能包含插值变量,
|
||||||
|
*/
|
||||||
|
function hasInterpolation(str){
|
||||||
|
return str.includes("{") && str.includes("}")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
通过正则表达式对原始文本内容进行解析匹配后得到的
|
||||||
|
formatters="| aaa(1,1) | bbb "
|
||||||
|
|
||||||
|
需要统一解析为
|
||||||
|
|
||||||
|
[
|
||||||
|
[aaa,[1,1]], // [formatter'name,[args,...]]
|
||||||
|
[bbb,[]],
|
||||||
|
]
|
||||||
|
|
||||||
|
formatters="| aaa(1,1,"dddd") | bbb "
|
||||||
|
|
||||||
|
目前对参数采用简单的split(",")来解析,因为无法正确解析aaa(1,1,"dd,,dd")形式的参数
|
||||||
|
在此场景下基本够用了,如果需要支持更复杂的参数解析,可以后续考虑使用正则表达式来解析
|
||||||
|
|
||||||
|
@returns [[<formatterName>,[<arg>,<arg>,...]]]
|
||||||
|
*/
|
||||||
|
function parseFormatters(formatters){
|
||||||
|
if(!formatters) return []
|
||||||
|
// 1. 先解析为 ["aaa()","bbb"]形式
|
||||||
|
let result = formatters.trim().substr(1).trim().split("|").map(r=>r.trim());
|
||||||
|
|
||||||
|
// 2. 解析格式化器参数
|
||||||
|
return result.map(formatter=>{
|
||||||
|
let firstIndex = formatter.indexOf("(");
|
||||||
|
let lastIndex = formatter.lastIndexOf(")");
|
||||||
|
if(firstIndex!==-1 && lastIndex!==-1){ // 带参数的格式化器
|
||||||
|
const argsContent = formatter.substr(firstIndex+1,lastIndex-firstIndex-1).trim();
|
||||||
|
let args = argsContent=="" ? [] : argsContent.split(",").map(arg=>{
|
||||||
|
arg = arg.trim();
|
||||||
|
if(!isNaN(parseInt(arg))){
|
||||||
|
return parseInt(arg) // 数字
|
||||||
|
}else if((arg.startsWith('\"') && arg.endsWith('\"')) || (arg.startsWith('\'') && arg.endsWith('\'')) ){
|
||||||
|
return arg.substr(1,arg.length-2) // 字符串
|
||||||
|
}else if(arg.toLowerCase()==="true" || arg.toLowerCase()==="false"){
|
||||||
|
return arg.toLowerCase()==="true" // 布尔值
|
||||||
|
}else if((arg.startsWith('{') && arg.endsWith('}')) || (arg.startsWith('[') && arg.endsWith(']'))){
|
||||||
|
try{
|
||||||
|
return JSON.parse(arg)
|
||||||
|
}catch(e){
|
||||||
|
return String(arg)
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
return String(arg)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return [formatter.substr(0,firstIndex),args]
|
||||||
|
}else {// 不带参数的格式化器
|
||||||
|
return [formatter,[]]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取字符串中的插值变量
|
||||||
|
* // [
|
||||||
|
// {
|
||||||
|
name:<变量名称>,formatters:[{name:<格式化器名称>,args:[<参数>,<参数>,....]]}],<匹配字符串>],
|
||||||
|
// ....
|
||||||
|
//
|
||||||
|
* @param {*} str
|
||||||
|
* @param {*} isFull =true 保留所有插值变量 =false 进行去重
|
||||||
|
* @returns {Array}
|
||||||
|
* [
|
||||||
|
* {
|
||||||
|
* name:"<变量名称>",
|
||||||
|
* formatters:[
|
||||||
|
* {name:"<格式化器名称>",args:[<参数>,<参数>,....]},
|
||||||
|
* {name:"<格式化器名称>",args:[<参数>,<参数>,....]},
|
||||||
|
* ],
|
||||||
|
* match:"<匹配字符串>"
|
||||||
|
* },
|
||||||
|
* ...
|
||||||
|
* ]
|
||||||
|
*/
|
||||||
|
function getInterpolatedVars(str){
|
||||||
|
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 {Function(<变量名称>,[formatters],match[0])} callback
|
||||||
|
* @returns 返回替换后的字符串
|
||||||
|
*/
|
||||||
|
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 || "";
|
||||||
|
// 解析格式化器和参数 = [<formatterName>,[<formatterName>,[<arg>,<arg>,...]]]
|
||||||
|
const formatters = parseFormatters(match.groups.formatters);
|
||||||
|
if(typeof(callback)==="function"){
|
||||||
|
try{
|
||||||
|
if(opts.replaceAll){
|
||||||
|
result=result.replaceAll(match[0],callback(varname,formatters,match[0]));
|
||||||
|
}else {
|
||||||
|
result=result.replace(match[0],callback(varname,formatters,match[0]));
|
||||||
|
}
|
||||||
|
}catch{// callback函数可能会抛出异常,如果抛出异常,则中断匹配过程
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
varWithPipeRegexp.lastIndex=0;
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 缓存数据类型的格式化器,避免每次都调用getDataTypeDefaultFormatter
|
||||||
|
let datatypeFormattersCache ={
|
||||||
|
$activeLanguage:null,
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 取得指定数据类型的默认格式化器
|
||||||
|
*
|
||||||
|
* 可以为每一个数据类型指定一个格式化器,当传入插值变量时,会自动调用该格式化器来对值进行格式化转换
|
||||||
|
|
||||||
|
const formatters = {
|
||||||
|
"*":{
|
||||||
|
$types:{...} // 在所有语言下只作用于特定数据类型的格式化器
|
||||||
|
}, // 在所有语言下生效的格式化器
|
||||||
|
cn:{
|
||||||
|
$types:{
|
||||||
|
[数据类型]:(value)=>{...},
|
||||||
|
},
|
||||||
|
[格式化器名称]:(value)=>{...},
|
||||||
|
[格式化器名称]:(value)=>{...},
|
||||||
|
[格式化器名称]:(value)=>{...},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
* @param {*} scope
|
||||||
|
* @param {*} activeLanguage
|
||||||
|
* @param {*} dataType 数字类型
|
||||||
|
* @returns {Function} 格式化函数
|
||||||
|
*/
|
||||||
|
function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){
|
||||||
|
if(datatypeFormattersCache.$activeLanguage === activeLanguage) {
|
||||||
|
if(dataType in datatypeFormattersCache) return datatypeFormattersCache[dataType]
|
||||||
|
}else {// 清空缓存
|
||||||
|
datatypeFormattersCache = { $activeLanguage:activeLanguage };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先在当前作用域中查找,再在全局查找
|
||||||
|
const targets = [scope.formatters,scope.global.formatters];
|
||||||
|
for(const target of targets){
|
||||||
|
if(activeLanguage in target){
|
||||||
|
// 在当前语言的$types中查找
|
||||||
|
let formatters = target[activeLanguage].$types || {};
|
||||||
|
for(let [name,formatter] of Object.entries(formatters)){
|
||||||
|
if(name === dataType && typeof(formatter)==="function") {
|
||||||
|
datatypeFormattersCache[dataType] = formatter;
|
||||||
|
return formatter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 在所有语言的$types中查找
|
||||||
|
let formatters = target["*"].$types || {};
|
||||||
|
for(let [name,formatter] of Object.entries(formatters)){
|
||||||
|
if(name === dataType && typeof(formatter)==="function") {
|
||||||
|
datatypeFormattersCache[dataType] = formatter;
|
||||||
|
return formatter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定名称的格式化器函数
|
||||||
|
* @param {*} scope
|
||||||
|
* @param {*} activeLanguage
|
||||||
|
* @param {*} name 格式化器名称
|
||||||
|
* @returns {Function} 格式化函数
|
||||||
|
*/
|
||||||
|
let formattersCache = { $activeLanguage:null};
|
||||||
|
function getFormatter(scope,activeLanguage,name){
|
||||||
|
if(formattersCache.$activeLanguage === activeLanguage) {
|
||||||
|
if(name in formattersCache) return formattersCache[name]
|
||||||
|
}else { // 当切换语言时需要清空缓存
|
||||||
|
formattersCache = { $activeLanguage:activeLanguage };
|
||||||
|
}
|
||||||
|
// 先在当前作用域中查找,再在全局查找
|
||||||
|
const targets = [scope.formatters,scope.global.formatters];
|
||||||
|
for(const target of targets){
|
||||||
|
// 优先在当前语言查找
|
||||||
|
if(activeLanguage in target){
|
||||||
|
let formatters = target[activeLanguage] || {};
|
||||||
|
if((name in formatters) && typeof(formatters[name])==="function") return formattersCache[name] = formatters[name]
|
||||||
|
}
|
||||||
|
// 在所有语言的$types中查找
|
||||||
|
let formatters = target["*"] || {};
|
||||||
|
if((name in formatters) && typeof(formatters[name])==="function") return formattersCache[name] = formatters[name]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行格式化器并返回结果
|
||||||
|
* @param {*} value
|
||||||
|
* @param {*} formatters
|
||||||
|
*/
|
||||||
|
function executeFormatter(value,formatters){
|
||||||
|
if(formatters.length===0) return value
|
||||||
|
let result = value;
|
||||||
|
try{
|
||||||
|
for(let formatter of formatters){
|
||||||
|
if(typeof(formatter) === "function") {
|
||||||
|
result = formatter(result);
|
||||||
|
}else {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}catch(e){
|
||||||
|
console.error(`Error while execute i18n formatter for ${value}: ${e.message} ` );
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 将 [[格式化器名称,[参数,参数,...]],[格式化器名称,[参数,参数,...]]]格式化器转化为
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param {*} scope
|
||||||
|
* @param {*} activeLanguage
|
||||||
|
* @param {*} formatters
|
||||||
|
*/
|
||||||
|
function buildFormatters(scope,activeLanguage,formatters){
|
||||||
|
let results = [];
|
||||||
|
for(let formatter of formatters){
|
||||||
|
if(formatter[0]){
|
||||||
|
const func = getFormatter(scope,activeLanguage,formatter[0]);
|
||||||
|
if(typeof(func)==="function"){
|
||||||
|
results.push((v)=>{
|
||||||
|
return func(v,...formatter[1])
|
||||||
|
});
|
||||||
|
}else {// 格式化器无效或者没有定义时,查看当前值是否具有同名的方法,如果有则执行调用
|
||||||
|
results.push((v)=>{
|
||||||
|
if(typeof(v[formatter[0]])==="function"){
|
||||||
|
return v[formatter[0]].call(v,...formatter[1])
|
||||||
|
}else {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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("<模板字符串>",[变量值,变量值,...])
|
||||||
|
* replaceInterpolatedVars("<模板字符串>",变量值,变量值,...])
|
||||||
|
*
|
||||||
|
- 当只有两个参数并且第2个参数是{}时,将第2个参数视为命名变量的字典
|
||||||
|
replaceInterpolatedVars("this is {a}+{b},{a:1,b:2}) --> this is 1+2
|
||||||
|
- 当只有两个参数并且第2个参数是[]时,将第2个参数视为位置参数
|
||||||
|
replaceInterpolatedVars"this is {}+{}",[1,2]) --> this is 1+2
|
||||||
|
- 普通位置参数替换
|
||||||
|
replaceInterpolatedVars("this is {a}+{b}",1,2) --> this is 1+2
|
||||||
|
-
|
||||||
|
this == scope == { formatters: {}, ... }
|
||||||
|
* @param {*} template
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function replaceInterpolatedVars(template,...args) {
|
||||||
|
const scope = this;
|
||||||
|
// 当前激活语言
|
||||||
|
const activeLanguage = scope.global.activeLanguage;
|
||||||
|
|
||||||
|
// 没有变量插值则的返回原字符串
|
||||||
|
if(args.length===0 || !hasInterpolation(template)) return template
|
||||||
|
|
||||||
|
// ****************************变量插值****************************
|
||||||
|
if(args.length===1 && isPlainObject(args[0])){
|
||||||
|
// 读取模板字符串中的插值变量列表
|
||||||
|
// [[var1,[formatter,formatter,...],match],[var2,[formatter,formatter,...],match],...}
|
||||||
|
let varValues = args[0];
|
||||||
|
return forEachInterpolatedVars(template,(varname,formatters)=>{
|
||||||
|
let value = (varname in varValues) ? varValues[varname] : '';
|
||||||
|
return getFormattedValue(scope,activeLanguage,formatters,value)
|
||||||
|
})
|
||||||
|
}else {
|
||||||
|
// ****************************位置插值****************************
|
||||||
|
// 如果只有一个Array参数,则认为是位置变量列表,进行展开
|
||||||
|
const params=(args.length===1 && Array.isArray(args[0])) ? [...args[0]] : args;
|
||||||
|
if(params.length===0) return template // 没有变量则不需要进行插值处理,返回原字符串
|
||||||
|
let i = 0;
|
||||||
|
return forEachInterpolatedVars(template,(varname,formatters)=>{
|
||||||
|
if(params.length>i){
|
||||||
|
return getFormattedValue(scope,activeLanguage,formatters,params[i++])
|
||||||
|
}else {
|
||||||
|
throw new Error() // 抛出异常,停止插值处理
|
||||||
|
}
|
||||||
|
},{replaceAll:false})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认语言配置
|
||||||
|
const defaultLanguageSettings = {
|
||||||
|
defaultLanguage: "cn",
|
||||||
|
activeLanguage: "cn",
|
||||||
|
languages:{
|
||||||
|
cn:{name:"cn",title:"中文",default:true},
|
||||||
|
en:{name:"en",title:"英文"},
|
||||||
|
},
|
||||||
|
formatters
|
||||||
|
};
|
||||||
|
|
||||||
|
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("total {$count} items", {$count:1}) //复数形式
|
||||||
|
* translate("total {} {} {} items",a,b,c) // 位置变量插值
|
||||||
|
*
|
||||||
|
* this===scope 当前绑定的scope
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function translate(message) {
|
||||||
|
const scope = this;
|
||||||
|
const activeLanguage = scope.global.activeLanguage;
|
||||||
|
let content = message;
|
||||||
|
let vars=[]; // 插值变量列表
|
||||||
|
let pluralVars= []; // 复数变量
|
||||||
|
let pluraValue = null; // 复数值
|
||||||
|
try{
|
||||||
|
// 1. 预处理变量: 复数变量保存至pluralVars中 , 变量如果是Function则调用
|
||||||
|
if(arguments.length === 2 && isPlainObject(arguments[1])){
|
||||||
|
Object.entries(arguments[1]).forEach(([name,value])=>{
|
||||||
|
if(typeof(value)==="function"){
|
||||||
|
try{
|
||||||
|
vars[name] = value();
|
||||||
|
}catch(e){
|
||||||
|
vars[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 以$开头的视为复数变量
|
||||||
|
if(name.startsWith("$")) pluralVars.push(name);
|
||||||
|
});
|
||||||
|
vars = [arguments[1]];
|
||||||
|
}else if(arguments.length >= 2){
|
||||||
|
vars = [...arguments].splice(1).map((arg,index)=>{
|
||||||
|
try{
|
||||||
|
arg = typeof(arg)==="function" ? arg() : arg;
|
||||||
|
// 位置参数中以第一个数值变量为复数变量
|
||||||
|
if(isNumber(arg)) pluraValue = parseInt(arg);
|
||||||
|
}catch(e){ }
|
||||||
|
return arg
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 取得翻译文本模板字符串
|
||||||
|
if(activeLanguage === scope.defaultLanguage){
|
||||||
|
// 2.1 从默认语言中取得翻译文本模板字符串
|
||||||
|
// 如果当前语言就是默认语言,不需要查询加载,只需要做插值变换即可
|
||||||
|
// 当源文件运用了babel插件后会将原始文本内容转换为msgId
|
||||||
|
// 如果是msgId则从scope.default中读取,scope.default=默认语言包={<id>:<message>}
|
||||||
|
if(isMessageId(content)){
|
||||||
|
content = scope.default[content] || 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!==null){ // 启用的是位置插值,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 content // 出错则返回原始文本
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多语言管理类
|
||||||
|
*
|
||||||
|
* 当导入编译后的多语言文件时(import("./languages")),会自动生成全局实例VoerkaI18n
|
||||||
|
*
|
||||||
|
* VoerkaI18n.languages // 返回支持的语言列表
|
||||||
|
* VoerkaI18n.defaultLanguage // 默认语言
|
||||||
|
* VoerkaI18n.language // 当前语言
|
||||||
|
* VoerkaI18n.change(language) // 切换到新的语言
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* VoerkaI18n.on("change",(language)=>{}) // 注册语言切换事件
|
||||||
|
* VoerkaI18n.off("change",(language)=>{})
|
||||||
|
*
|
||||||
|
* */
|
||||||
|
class I18nManager{
|
||||||
|
static instance = null; // 单例引用
|
||||||
|
callbacks = [] // 当切换语言时的回调事件
|
||||||
|
constructor(settings={}){
|
||||||
|
if(I18nManager.instance!=null){
|
||||||
|
return I18nManager.instance;
|
||||||
|
}
|
||||||
|
I18nManager.instance = this;
|
||||||
|
this._settings = deepMerge(defaultLanguageSettings,settings);
|
||||||
|
this._scopes=[];
|
||||||
|
return I18nManager.instance;
|
||||||
|
}
|
||||||
|
get settings(){ return this._settings }
|
||||||
|
get scopes(){ return this._scopes }
|
||||||
|
// 当前激活语言
|
||||||
|
get activeLanguage(){ return this._settings.activeLanguage}
|
||||||
|
// 默认语言
|
||||||
|
get defaultLanguage(){ return this.this._settings.defaultLanguage}
|
||||||
|
// 支持的语言列表
|
||||||
|
get languages(){ return this._settings.languages}
|
||||||
|
// 订阅语言切换事件
|
||||||
|
on(callback){
|
||||||
|
this.callbacks.push(callback);
|
||||||
|
}
|
||||||
|
off(callback){
|
||||||
|
for(let i=0;i<this.callbacks.length;i++){
|
||||||
|
if(this.callbacks[i]===callback ){
|
||||||
|
this.callbacks.splice(i,1);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offAll(){
|
||||||
|
this.callbacks=[];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 切换语言时触发语言切换事件回调
|
||||||
|
*/
|
||||||
|
async _triggerChangeEvents(newLanguage){
|
||||||
|
try{
|
||||||
|
await this._updateScopes(newLanguage);
|
||||||
|
await (Promise.allSettled || Promise.all)(this.callbacks.map(async cb=>await cb(newLanguage)));
|
||||||
|
}catch(e){
|
||||||
|
console.warn("Error while executing language change events",e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 切换语言
|
||||||
|
*/
|
||||||
|
async change(value){
|
||||||
|
if(value in this.languages){
|
||||||
|
await this._triggerChangeEvents(value);
|
||||||
|
this._settings.activeLanguage = value;
|
||||||
|
}else {
|
||||||
|
throw new Error("Not supported language:"+value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取指定作用域的下的语言包加载器
|
||||||
|
*
|
||||||
|
* 同时会进行语言兼容性处理
|
||||||
|
*
|
||||||
|
* 如scope里面定义了一个cn的语言包,当切换到zh-cn时,会自动加载cn语言包
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param {*} scope
|
||||||
|
* @param {*} lang
|
||||||
|
*/
|
||||||
|
_getScopeLoader(scope,lang){
|
||||||
|
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 当切换语言时调用此方法来加载更新语言包
|
||||||
|
* @param {*} newLanguage
|
||||||
|
*/
|
||||||
|
async _updateScopes(newLanguage){
|
||||||
|
// 并发执行所有作用域语言包的加载
|
||||||
|
try{
|
||||||
|
await (Promise.allSettled || Promise.all)(this._scopes.map(scope=>{
|
||||||
|
return async ()=>{
|
||||||
|
// 默认语言,所有均默认语言均采用静态加载方式,只需要简单的替换即可
|
||||||
|
if(newLanguage === scope.defaultLanguage){
|
||||||
|
scope.messages = scope.default;
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 异步加载语言文件
|
||||||
|
const loader = scope.loaders[newLanguage];
|
||||||
|
if(typeof(loader) === "function"){
|
||||||
|
try{
|
||||||
|
scope.messages = await loader();
|
||||||
|
}catch(e){
|
||||||
|
console.warn(`Error loading language ${newLanguage} : ${e.message}`);
|
||||||
|
scope.messages = defaultMessages; // 出错时回退到默认语言
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
scope.messages = defaultMessages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}catch(e){
|
||||||
|
console.warn("Error while refreshing scope:",e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* 注册一个新的作用域
|
||||||
|
*
|
||||||
|
* 每一个库均对应一个作用域,每个作用域可以有多个语言包,且对应一个翻译函数
|
||||||
|
* scope={
|
||||||
|
* defaultLanguage:"cn",
|
||||||
|
default: defaultMessages, // 转换文本信息
|
||||||
|
messages : defaultMessages, // 当前语言的消息
|
||||||
|
idMap:messageIds,
|
||||||
|
formatters:{
|
||||||
|
...formatters,
|
||||||
|
...i18nSettings.formatters || {}
|
||||||
|
},
|
||||||
|
loaders:{}, // 异步加载语言文件的函数列表
|
||||||
|
settings:{} // 引用全局VoerkaI18n实例的配置
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* 除了默认语言外,其他语言采用动态加载的方式
|
||||||
|
*
|
||||||
|
* @param {*} scope
|
||||||
|
*/
|
||||||
|
register(scope){
|
||||||
|
scope.global = this._settings;
|
||||||
|
this._scopes.push(scope);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 注册全局格式化器
|
||||||
|
* @param {*} formatters
|
||||||
|
*/
|
||||||
|
registerFormatters(formatters){
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var runtime ={
|
||||||
|
getInterpolatedVars,
|
||||||
|
replaceInterpolatedVars,
|
||||||
|
I18nManager,
|
||||||
|
translate,
|
||||||
|
languages,
|
||||||
|
defaultLanguageSettings
|
||||||
|
};
|
||||||
|
|
||||||
|
export { runtime as default };
|
@ -1,6 +1,5 @@
|
|||||||
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
|
||||||
@ -31,7 +30,39 @@ 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;
|
||||||
|
};
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
通过正则表达式对原始文本内容进行解析匹配后得到的
|
通过正则表达式对原始文本内容进行解析匹配后得到的
|
||||||
@ -182,15 +213,14 @@ function transformToString(value){
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetScopeCache(scope,activeLanguage=null){
|
||||||
// 缓存数据类型的格式化器,避免每次都调用getDataTypeDefaultFormatter
|
scope.$cache = {activeLanguage,typedFormatters:{},formatters:{}}
|
||||||
let datatypeFormattersCache ={
|
|
||||||
$activeLanguage:null,
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 取得指定数据类型的默认格式化器
|
* 取得指定数据类型的默认格式化器
|
||||||
*
|
*
|
||||||
* 可以为每一个数据类型指定一个格式化器,当传入插值变量时,会自动调用该格式化器来对值进行格式化转换
|
* 可以为每一个数据类型指定一个默认的格式化器,当传入插值变量时,
|
||||||
|
* 会自动调用该格式化器来对值进行格式化转换
|
||||||
|
|
||||||
const formatters = {
|
const formatters = {
|
||||||
"*":{
|
"*":{
|
||||||
@ -211,33 +241,30 @@ let datatypeFormattersCache ={
|
|||||||
* @returns {Function} 格式化函数
|
* @returns {Function} 格式化函数
|
||||||
*/
|
*/
|
||||||
function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){
|
function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){
|
||||||
if(datatypeFormattersCache.$activeLanguage === activeLanguage) {
|
if(!scope.$cache) resetScopeCache(scope)
|
||||||
if(dataType in datatypeFormattersCache) return datatypeFormattersCache[dataType]
|
if(scope.$cache.activeLanguage === activeLanguage) {
|
||||||
}else{// 清空缓存
|
if(dataType in scope.$cache.typedFormatters) return scope.$cache.typedFormatters[dataType]
|
||||||
datatypeFormattersCache = { $activeLanguage:activeLanguage }
|
}else{// 当语言切换时清空缓存
|
||||||
|
resetScopeCache(scope,activeLanguage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 先在当前作用域中查找,再在全局查找
|
// 先在当前作用域中查找,再在全局查找
|
||||||
const targets = [scope.formatters,scope.global.formatters]
|
const targets = [scope.formatters,scope.global.formatters]
|
||||||
for(const target of targets){
|
for(const target of targets){
|
||||||
if(activeLanguage in target){
|
// 优先在当前语言的$types中查找
|
||||||
// 在当前语言的$types中查找
|
if((activeLanguage in target) && isPlainObject(target[activeLanguage].$types)){
|
||||||
let formatters = target[activeLanguage].$types || {}
|
let formatters = target[activeLanguage].$types
|
||||||
for(let [name,formatter] of Object.entries(formatters)){
|
if(dataType in formatters && typeof(formatters[dataType])==="function"){
|
||||||
if(name === dataType && typeof(formatter)==="function") {
|
return scope.$cache.typedFormatters[dataType] = formatters[dataType]
|
||||||
datatypeFormattersCache[dataType] = formatter
|
}
|
||||||
return formatter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// 在所有语言的$types中查找
|
// 在所有语言的$types中查找
|
||||||
let formatters = target["*"].$types || {}
|
if(("*" in target) && isPlainObject(target["*"].$types)){
|
||||||
for(let [name,formatter] of Object.entries(formatters)){
|
let formatters = target["*"].$types
|
||||||
if(name === dataType && typeof(formatter)==="function") {
|
if(dataType in formatters && typeof(formatters[dataType])==="function"){
|
||||||
datatypeFormattersCache[dataType] = formatter
|
return scope.$cache.typedFormatters[dataType] = formatters[dataType]
|
||||||
return formatter
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,13 +274,14 @@ function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){
|
|||||||
* @param {*} activeLanguage
|
* @param {*} activeLanguage
|
||||||
* @param {*} name 格式化器名称
|
* @param {*} name 格式化器名称
|
||||||
* @returns {Function} 格式化函数
|
* @returns {Function} 格式化函数
|
||||||
*/
|
*/
|
||||||
let formattersCache = { $activeLanguage:null}
|
|
||||||
function getFormatter(scope,activeLanguage,name){
|
function getFormatter(scope,activeLanguage,name){
|
||||||
if(formattersCache.$activeLanguage === activeLanguage) {
|
// 缓存格式化器引用,避免重复检索
|
||||||
if(name in formattersCache) return formattersCache[name]
|
if(!scope.$cache) resetScopeCache(scope)
|
||||||
}else{ // 当切换语言时需要清空缓存
|
if(scope.$cache.activeLanguage === activeLanguage) {
|
||||||
formattersCache = { $activeLanguage:activeLanguage }
|
if(dataType in scope.$cache.formatters) return scope.$cache.formatters[dataType]
|
||||||
|
}else{// 当语言切换时清空缓存
|
||||||
|
resetScopeCache(scope,activeLanguage)
|
||||||
}
|
}
|
||||||
// 先在当前作用域中查找,再在全局查找
|
// 先在当前作用域中查找,再在全局查找
|
||||||
const targets = [scope.formatters,scope.global.formatters]
|
const targets = [scope.formatters,scope.global.formatters]
|
||||||
@ -261,11 +289,11 @@ function getFormatter(scope,activeLanguage,name){
|
|||||||
// 优先在当前语言查找
|
// 优先在当前语言查找
|
||||||
if(activeLanguage in target){
|
if(activeLanguage in target){
|
||||||
let formatters = target[activeLanguage] || {}
|
let formatters = target[activeLanguage] || {}
|
||||||
if((name in formatters) && typeof(formatters[name])==="function") return formattersCache[name] = formatters[name]
|
if((name in formatters) && typeof(formatters[name])==="function") return scope.$cache.formatters[name] = formatters[name]
|
||||||
}
|
}
|
||||||
// 在所有语言的$types中查找
|
// 在所有语言的$types中查找
|
||||||
let formatters = target["*"] || {}
|
let formatters = target["*"] || {}
|
||||||
if((name in formatters) && typeof(formatters[name])==="function") return formattersCache[name] = formatters[name]
|
if((name in formatters) && typeof(formatters[name])==="function") return scope.$cache.formatters[name] = formatters[name]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,7 +336,9 @@ function buildFormatters(scope,activeLanguage,formatters){
|
|||||||
results.push((v)=>{
|
results.push((v)=>{
|
||||||
return func(v,...formatter[1])
|
return func(v,...formatter[1])
|
||||||
})
|
})
|
||||||
}else{// 格式化器无效或者没有定义时,查看当前值是否具有同名的方法,如果有则执行调用
|
}else{
|
||||||
|
// 格式化器无效或者没有定义时,查看当前值是否具有同名的原型方法,如果有则执行调用
|
||||||
|
// 比如padStart格式化器是String的原型方法,不需要配置就可以直接作为格式化器调用
|
||||||
results.push((v)=>{
|
results.push((v)=>{
|
||||||
if(typeof(v[formatter[0]])==="function"){
|
if(typeof(v[formatter[0]])==="function"){
|
||||||
return v[formatter[0]].call(v,...formatter[1])
|
return v[formatter[0]].call(v,...formatter[1])
|
||||||
@ -333,7 +363,7 @@ function buildFormatters(scope,activeLanguage,formatters){
|
|||||||
function getFormattedValue(scope,activeLanguage,formatters,value){
|
function getFormattedValue(scope,activeLanguage,formatters,value){
|
||||||
// 1. 取得格式化器函数列表
|
// 1. 取得格式化器函数列表
|
||||||
const formatterFuncs = buildFormatters(scope,activeLanguage,formatters)
|
const formatterFuncs = buildFormatters(scope,activeLanguage,formatters)
|
||||||
// 3. 查找每种数据类型默认格式化器,并添加到formatters最前面,默认数据类型格式化器优先级最高
|
// 2. 查找每种数据类型默认格式化器,并添加到formatters最前面,默认数据类型格式化器优先级最高
|
||||||
const defaultFormatter = getDataTypeDefaultFormatter(scope,activeLanguage,getDataTypeName(value))
|
const defaultFormatter = getDataTypeDefaultFormatter(scope,activeLanguage,getDataTypeName(value))
|
||||||
if(defaultFormatter){
|
if(defaultFormatter){
|
||||||
formatterFuncs.splice(0,0,defaultFormatter)
|
formatterFuncs.splice(0,0,defaultFormatter)
|
||||||
@ -534,13 +564,13 @@ function translate(message) {
|
|||||||
static instance = null; // 单例引用
|
static instance = null; // 单例引用
|
||||||
callbacks = [] // 当切换语言时的回调事件
|
callbacks = [] // 当切换语言时的回调事件
|
||||||
constructor(settings={}){
|
constructor(settings={}){
|
||||||
if(i18n.instance==null){
|
if(I18nManager.instance!=null){
|
||||||
this.reset()
|
return I18nManager.instance;
|
||||||
i18n.instance = this;
|
|
||||||
}
|
}
|
||||||
|
I18nManager.instance = this;
|
||||||
this._settings = deepMerge(defaultLanguageSettings,settings)
|
this._settings = deepMerge(defaultLanguageSettings,settings)
|
||||||
this._scopes=[]
|
this._scopes=[]
|
||||||
return i18n.instance;
|
return I18nManager.instance;
|
||||||
}
|
}
|
||||||
get settings(){ return this._settings }
|
get settings(){ return this._settings }
|
||||||
get scopes(){ return this._scopes }
|
get scopes(){ return this._scopes }
|
||||||
@ -571,16 +601,20 @@ function translate(message) {
|
|||||||
async _triggerChangeEvents(newLanguage){
|
async _triggerChangeEvents(newLanguage){
|
||||||
try{
|
try{
|
||||||
await this._updateScopes(newLanguage)
|
await this._updateScopes(newLanguage)
|
||||||
await (Promise.allSettled || Promise.all)(this.callbacks.map(async cb=>await cb(newLanguage)))
|
if(Promise.allSettled){
|
||||||
|
await Promise.allSettled(this.callbacks.map(cb=>cb(newLanguage)))
|
||||||
|
}else{
|
||||||
|
await Promise.all(this.callbacks.map(cb=>cb(newLanguage)))
|
||||||
|
}
|
||||||
}catch(e){
|
}catch(e){
|
||||||
console.warn("Error while executing language change events",e.message)
|
console.warn("Error while executing language change events:",e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 切换语言
|
* 切换语言
|
||||||
*/
|
*/
|
||||||
async change(value){
|
async change(value){
|
||||||
if(value in this.languages){
|
if(this.languages.findIndex(lang=>lang.name === value)!==-1){
|
||||||
await this._triggerChangeEvents(value)
|
await this._triggerChangeEvents(value)
|
||||||
this._settings.activeLanguage = value
|
this._settings.activeLanguage = value
|
||||||
}else{
|
}else{
|
||||||
@ -608,7 +642,7 @@ function translate(message) {
|
|||||||
async _updateScopes(newLanguage){
|
async _updateScopes(newLanguage){
|
||||||
// 并发执行所有作用域语言包的加载
|
// 并发执行所有作用域语言包的加载
|
||||||
try{
|
try{
|
||||||
await (Promise.allSettled || Promise.all)(this._scopes.map(scope=>{
|
let scopeLoders = this._scopes.map(scope=>{
|
||||||
return async ()=>{
|
return async ()=>{
|
||||||
// 默认语言,所有均默认语言均采用静态加载方式,只需要简单的替换即可
|
// 默认语言,所有均默认语言均采用静态加载方式,只需要简单的替换即可
|
||||||
if(newLanguage === scope.defaultLanguage){
|
if(newLanguage === scope.defaultLanguage){
|
||||||
@ -619,7 +653,7 @@ function translate(message) {
|
|||||||
const loader = scope.loaders[newLanguage]
|
const loader = scope.loaders[newLanguage]
|
||||||
if(typeof(loader) === "function"){
|
if(typeof(loader) === "function"){
|
||||||
try{
|
try{
|
||||||
scope.messages = await loader()
|
scope.messages = (await loader() ).default
|
||||||
}catch(e){
|
}catch(e){
|
||||||
console.warn(`Error loading language ${newLanguage} : ${e.message}`)
|
console.warn(`Error loading language ${newLanguage} : ${e.message}`)
|
||||||
scope.messages = defaultMessages // 出错时回退到默认语言
|
scope.messages = defaultMessages // 出错时回退到默认语言
|
||||||
@ -627,8 +661,13 @@ function translate(message) {
|
|||||||
}else{
|
}else{
|
||||||
scope.messages = defaultMessages
|
scope.messages = defaultMessages
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
|
if(Promise.allSettled){
|
||||||
|
await Promise.allSettled(scopeLoders.map((f)=>f()))
|
||||||
|
}else{
|
||||||
|
await Promise.all(scopeLoders.map((f)=>f()))
|
||||||
|
}
|
||||||
}catch(e){
|
}catch(e){
|
||||||
console.warn("Error while refreshing scope:",e.message)
|
console.warn("Error while refreshing scope:",e.message)
|
||||||
}
|
}
|
||||||
@ -674,5 +713,8 @@ module.exports ={
|
|||||||
I18nManager,
|
I18nManager,
|
||||||
translate,
|
translate,
|
||||||
languages,
|
languages,
|
||||||
defaultLanguageSettings
|
defaultLanguageSettings,
|
||||||
|
getDataTypeName,
|
||||||
|
isNumber,
|
||||||
|
isPlainObject
|
||||||
}
|
}
|
@ -3,12 +3,16 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Voerkai18n Runtime",
|
"description": "Voerkai18n Runtime",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
"module": "index.esm.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"build": "rollup -c"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"deepmerge": "^4.2.2"
|
"@rollup/plugin-node-resolve": "^13.1.3",
|
||||||
|
"deepmerge": "^4.2.2",
|
||||||
|
"rollup-plugin-terser": "^7.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,33 +1,30 @@
|
|||||||
|
|
||||||
import clear from 'rollup-plugin-clear'
|
import clear from 'rollup-plugin-clear'
|
||||||
import { uglify } from "rollup-plugin-uglify";
|
|
||||||
import { babel } from '@rollup/plugin-babel';
|
|
||||||
import commonjs from '@rollup/plugin-commonjs';
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
|
import resolve from "@rollup/plugin-node-resolve";
|
||||||
|
import { terser } from "rollup-plugin-terser";
|
||||||
|
// import { babel } from '@rollup/plugin-babel';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
input: './index.js',
|
input: './index.js',
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
file: 'dist/index.mjs',
|
file: 'index.esm.js',
|
||||||
format:"es"
|
format:"esm"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
file: 'dist/index.cjs',
|
file: 'index.cjs',
|
||||||
exports:"default",
|
exports:"default",
|
||||||
format:"cjs"
|
format:"cjs"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
//resolve(),
|
resolve(),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
babel({
|
|
||||||
babelHelpers:"runtime",
|
|
||||||
exclude: 'node_modules/**'
|
|
||||||
}),
|
|
||||||
clear({targets:["dist"]}),
|
clear({targets:["dist"]}),
|
||||||
uglify()
|
//terser()
|
||||||
],
|
],
|
||||||
external:["@babel/runtime"]
|
//external:["@babel/runtime"]
|
||||||
}
|
}
|
||||||
]
|
]
|
@ -1,41 +0,0 @@
|
|||||||
|
|
||||||
/**
|
|
||||||
* 获取指定变量类型名称
|
|
||||||
* 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;
|
|
||||||
};
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getDataTypeName,
|
|
||||||
isNumber,
|
|
||||||
isPlainObject
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -27,7 +27,7 @@ const readJson = require("readjson")
|
|||||||
const glob = require("glob")
|
const glob = require("glob")
|
||||||
const createLogger = require("logsets")
|
const createLogger = require("logsets")
|
||||||
const path = require("path")
|
const path = require("path")
|
||||||
const { importModule } = require("./utils")
|
const { importModule,findModuleType } = require("./utils")
|
||||||
const fs = require("fs")
|
const fs = require("fs")
|
||||||
const logger = createLogger()
|
const logger = createLogger()
|
||||||
const artTemplate = require("art-template")
|
const artTemplate = require("art-template")
|
||||||
@ -45,24 +45,7 @@ function normalizeCompileOptions(opts={}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从当前文件夹开始向上查找package.json文件,并解析出语言包的类型
|
|
||||||
* @param {*} folder
|
|
||||||
*/
|
|
||||||
function findModuleType(folder){
|
|
||||||
try{
|
|
||||||
let pkgPath = path.join(folder, "package.json")
|
|
||||||
if(fs.existsSync(pkgPath)){
|
|
||||||
let pkg = readJson.sync(pkgPath)
|
|
||||||
return pkg.type || "commonjs"
|
|
||||||
}
|
|
||||||
let parent = path.dirname(folder)
|
|
||||||
if(parent===folder) return null
|
|
||||||
return findModuleType(parent)
|
|
||||||
}catch{
|
|
||||||
return "esm"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports =async function compile(langFolder,opts={}){
|
module.exports =async function compile(langFolder,opts={}){
|
||||||
const options = normalizeCompileOptions(opts);
|
const options = normalizeCompileOptions(opts);
|
||||||
|
@ -11,7 +11,8 @@ const path = require('path')
|
|||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const readJson = require("readjson")
|
const readJson = require("readjson")
|
||||||
const createLogger = require("logsets")
|
const createLogger = require("logsets")
|
||||||
const { replaceInterpolateVars,getDataTypeName } = require("../runtime/utils")
|
const { replaceInterpolateVars,getDataTypeName } = require("@voerkai18n/runtime")
|
||||||
|
const { findModuleType } = require("./utils")
|
||||||
const logger = createLogger()
|
const logger = createLogger()
|
||||||
|
|
||||||
// 捕获翻译文本的默认正则表达式
|
// 捕获翻译文本的默认正则表达式
|
||||||
@ -415,7 +416,10 @@ module.exports = function(options={}){
|
|||||||
logger.log(" - Generate settings of language : {}",settingsFile)
|
logger.log(" - Generate settings of language : {}",settingsFile)
|
||||||
}else{
|
}else{
|
||||||
logger.log(" - Settings of language already exists : {}",settingsFile)
|
logger.log(" - Settings of language already exists : {}",settingsFile)
|
||||||
}
|
}
|
||||||
|
// 生成package.json
|
||||||
|
const packageJsonFile = path.join(outputPath,"package.json")
|
||||||
|
fs.writeFileSync(packageJsonFile,`${JSON.stringify({type:"commonjs"},null,4)}`)
|
||||||
callback()
|
callback()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
const { Command } = require('commander');
|
const { Command } = require('commander');
|
||||||
|
const createLogger = require("logsets")
|
||||||
|
|
||||||
|
const logger = createLogger()
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|
||||||
|
|
||||||
@ -6,14 +9,34 @@ program
|
|||||||
.option('-d, --debug', '输出调试信息')
|
.option('-d, --debug', '输出调试信息')
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('extract <source> [destination]')
|
.command('init')
|
||||||
.description('扫描指定的项目目录,提取文件中的国际化字符串')
|
.argument('[location]', '工程项目所在目录')
|
||||||
|
.description('初始化项目国际化配置')
|
||||||
|
.option('-r, --reset', '重新生成当前项目的语言配置')
|
||||||
|
.option('-p, --langPath [name]', '语言包保存路径,默认<location>/langauges',"languages")
|
||||||
|
.option('-m, --moduleType [type]', '生成的js模块类型,默认esm',"esm")
|
||||||
|
.option('-lngs, --languages <languages...>', '支持的语言列表', ['cn','en'])
|
||||||
|
.action((location,options) => {
|
||||||
|
if(!location) location = process.cwd()
|
||||||
|
const initializer = require("./initializer")
|
||||||
|
options.debug=true
|
||||||
|
initializer(location,options)
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('extract')
|
||||||
|
.description('扫描并提取所有待翻译的字符串到<languages/translates>文件夹中')
|
||||||
|
.argument('[location]', 'js项目所在目录')
|
||||||
.option('-d, --debug', '输出调试信息')
|
.option('-d, --debug', '输出调试信息')
|
||||||
.option('-l, --languages', '支持的语言', 'cn,en,de,fr,es,it,jp')
|
.option('-ls, --languages', '支持的语言', 'cn,en,de,fr,es,it,jp')
|
||||||
.option('-d, --default', '默认语言', 'cn')
|
.option('-d, --default', '默认语言', 'cn')
|
||||||
.option('-a, --active', '激活语言', 'cn')
|
.option('-a, --active', '激活语言', 'cn')
|
||||||
.action((source, destination) => {
|
.option('-o, --output', '输出目录', './languages')
|
||||||
console.log('clone command called');
|
.argument('<location...>', '工程所在目录')
|
||||||
|
.action((location) => {
|
||||||
|
if(!location) location = process.cwd()
|
||||||
|
console.log('location=',location);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
51
packages/tools/initializer.js
Normal file
51
packages/tools/initializer.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化指定项目的语言包
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
const { findModuleType } = require("./utils")
|
||||||
|
const path = require("path")
|
||||||
|
const fs = require("fs")
|
||||||
|
const createLogger = require("logsets")
|
||||||
|
const logger = createLogger()
|
||||||
|
|
||||||
|
module.exports = function(targetPath,{debug = true, langPath = "languages",languages=["cn","en"],defaultLanguage="cn",activeLanguage="cn",moduleType = "auto",reset=false}={}){
|
||||||
|
// 查找当前项目的语言包类型路径
|
||||||
|
if(moduleType==="auto"){
|
||||||
|
moduleType = findModuleType(targetPath)
|
||||||
|
}
|
||||||
|
if(moduleType==null) {
|
||||||
|
if(debug){
|
||||||
|
logger.log("找不到{}文件,{}只能在js项目工程中使用","package.json","voerkai18n")
|
||||||
|
}else{
|
||||||
|
throw new Error("找不到package.json文件,voerkai18n只能在js项目工程中使用")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lngPath = path.join(targetPath,langPath)
|
||||||
|
if(!fs.existsSync(lngPath)){
|
||||||
|
fs.mkdirSync(lngPath)
|
||||||
|
if(debug) logger.log("创建语言包文件夹: {}",lngPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建settings.js文件
|
||||||
|
const settingsFile = path.join(lngPath,"settings.js")
|
||||||
|
if(fs.existsSync(settingsFile) && !reset){
|
||||||
|
if(debug) logger.log("语言配置文件{}文件已存在,跳过创建。\n使用{}可以重新覆盖创建",settingsFile,"-r")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const settings = {
|
||||||
|
languages:languages.map(lng=>({name:lng,title:lng})),
|
||||||
|
defaultLanguage,
|
||||||
|
activeLanguage,
|
||||||
|
namespaces:{}
|
||||||
|
}
|
||||||
|
if(["esm","es"].includes(moduleType)){
|
||||||
|
fs.writeFileSync(settingsFile,`export default ${JSON.stringify(settings,null,4)}`)
|
||||||
|
}else{
|
||||||
|
fs.writeFileSync(settingsFile,`module.exports = ${JSON.stringify(settings,null,4)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(debug) logger.log("创建语言配置文件<{}>成功",settingsFile)
|
||||||
|
}
|
@ -7,20 +7,25 @@
|
|||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
|
"bin": {
|
||||||
|
"voerkai18n": "./index.js"
|
||||||
|
},
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/cli": "^7.17.6",
|
"@babel/cli": "^7.17.6",
|
||||||
"@babel/core": "^7.17.5",
|
"@babel/core": "^7.17.5",
|
||||||
|
"@voerkai18n/runtime": "workspace:^1.0.0",
|
||||||
"art-template": "^4.13.2",
|
"art-template": "^4.13.2",
|
||||||
"commander": "^9.0.0",
|
"commander": "^9.0.0",
|
||||||
"glob": "^7.2.0",
|
"glob": "^7.2.0",
|
||||||
"logsets": "^1.0.6",
|
"logsets": "^1.0.6",
|
||||||
"readjson": "^2.2.2",
|
"readjson": "^2.2.2",
|
||||||
"through2": "^4.0.2"
|
"through2": "^4.0.2",
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"deepmerge": "^4.2.2",
|
"deepmerge": "^4.2.2",
|
||||||
"gulp": "^4.0.2",
|
"gulp": "^4.0.2",
|
||||||
"vinyl": "^2.2.1"
|
"vinyl": "^2.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,25 +22,28 @@ let scope = {
|
|||||||
idMap:messageIds, // 消息id映射列表
|
idMap:messageIds, // 消息id映射列表
|
||||||
formatters:{}, // 当前作用域的格式化函数列表
|
formatters:{}, // 当前作用域的格式化函数列表
|
||||||
loaders:{}, // 异步加载语言文件的函数列表
|
loaders:{}, // 异步加载语言文件的函数列表
|
||||||
global:{} // 引用全局VoerkaI18n配置,注册后自动引用
|
global:{}, // 引用全局VoerkaI18n配置,注册后自动引用
|
||||||
|
// 主要用来缓存格式化器的引用,当使用格式化器时可以直接引用,避免检索
|
||||||
|
$cache:{
|
||||||
|
activeLanguage:null,
|
||||||
|
typedFormatters:{},
|
||||||
|
formatters:{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let supportedlanguages = {}
|
let supportedlanguages = {}
|
||||||
|
|
||||||
messages["{{defaultLanguage}}"]= defaultMessages
|
|
||||||
{{each languages}}{{if $value.name !== defaultLanguage}}
|
{{each languages}}{{if $value.name !== defaultLanguage}}
|
||||||
scope.loaders["{{$value.name}}"] = ()=>import("./{{$value.name}}.js")
|
scope.loaders["{{$value.name}}"] = ()=>import("./{{$value.name}}.js")
|
||||||
{{/if}}{{/each}}
|
{{/if}}{{/each}}
|
||||||
|
|
||||||
const t = ()=> translate.bind(scope)(...arguments)
|
const t = translate.bind(scope)
|
||||||
const languages = {{@ JSON.stringify(languages,null,4) }}
|
const languages = {{@ JSON.stringify(languages,null,4) }}
|
||||||
// 注册当前作用域到全局VoerkaI18n实例
|
// 注册当前作用域到全局VoerkaI18n实例
|
||||||
VoerkaI18n.register(scope)
|
VoerkaI18n.register(scope)
|
||||||
|
|
||||||
{{if moduleType === "esm"}}
|
{{if moduleType === "esm"}}
|
||||||
export languages
|
export { t, languages,scope }
|
||||||
export scope
|
|
||||||
export t
|
|
||||||
{{else}}
|
{{else}}
|
||||||
module.exports.languages = languages
|
module.exports.languages = languages
|
||||||
module.exports.scope = scope
|
module.exports.scope = scope
|
||||||
|
@ -7,9 +7,95 @@ async function importModule(url){
|
|||||||
return await import(url)
|
return await import(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 从当前文件夹开始向上查找package.json文件,并解析出语言包的类型
|
||||||
|
* @param {*} folder
|
||||||
|
*/
|
||||||
|
function findModuleType(folder){
|
||||||
|
try{
|
||||||
|
let pkgPath = path.join(folder, "package.json")
|
||||||
|
if(fs.existsSync(pkgPath)){
|
||||||
|
let pkg = readJson.sync(pkgPath)
|
||||||
|
return pkg.type || "commonjs"
|
||||||
|
}
|
||||||
|
let parent = path.dirname(folder)
|
||||||
|
if(parent===folder) return null
|
||||||
|
return findModuleType(parent)
|
||||||
|
}catch{
|
||||||
|
return "esm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* getExportContent({a:1}) == export let a = 1
|
||||||
|
*
|
||||||
|
* @param {*} values
|
||||||
|
* @param {*} moduleType
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function generateExportContents(values,{moduleType="esm",varExportDeclare="let"}={}){
|
||||||
|
if(!isPlainObject(values)) throw new TypeError("export value must be a function or plain object")
|
||||||
|
let results = []
|
||||||
|
let varExports = []
|
||||||
|
let varExportSyntax = moduleType === "esm" ? `export ${varExportDeclare} ` : "module.exports."
|
||||||
|
let funcExportSyntax = moduleType === "esm" ? `export ` : "module.exports."
|
||||||
|
|
||||||
|
Object.entries(values).forEach(([name,value])=>{
|
||||||
|
if(Array.isArray(value) || isPlainObject(value)){
|
||||||
|
results.push(`${varExportDeclare} ${name} = ${JSON.stringify(value,null,4)}`)
|
||||||
|
}else if(typeof(value)==="function"){
|
||||||
|
if(value.prototype){
|
||||||
|
results.push(value.toString())
|
||||||
|
}else{// 箭头函数
|
||||||
|
results.push(`const ${name} = ${value.toString()}`)
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
results.push(`${varExportDeclare} ${name} = ${JSON.stringify(value)}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if(moduleType === "esm"){
|
||||||
|
results.push(`export {\n\t${Object.keys(values).join(",\n\t")}\n}`)
|
||||||
|
}else{ fu
|
||||||
|
results.push(`module.exports = {\n\t${Object.keys(values).join(",\n\t")}\n}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建js文件
|
||||||
|
* @param {*} filename
|
||||||
|
* @param {*} defaultExports
|
||||||
|
* @param {*} namedExports {name:value}
|
||||||
|
*
|
||||||
|
* @param {*} moduleType
|
||||||
|
*/
|
||||||
|
function createJsModuleFile(filename,defaultExports={},namedExports={},moduleType="esm"){
|
||||||
|
let jsContents = []
|
||||||
|
if(moduleType === "esm"){
|
||||||
|
Object.entries(namedExports).forEach(([name,value])=>{
|
||||||
|
|
||||||
|
})
|
||||||
|
jsContents.push
|
||||||
|
}else{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
module.exports = {
|
module.exports = {
|
||||||
importModule
|
importModule,
|
||||||
|
findModuleType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
539
pnpm-lock.yaml
generated
539
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user