update
This commit is contained in:
parent
50446a1614
commit
b22d2ddaf7
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,7 +1,7 @@
|
|||||||
/.vscode
|
/.vscode
|
||||||
/node_modules
|
/node_modules
|
||||||
node_modules
|
node_modules
|
||||||
/demo/apps/app/languages
|
/packages/**/node_modules
|
||||||
/demo/apps/*/languages
|
|
||||||
/demo/*/node_modules
|
|
||||||
/coverage
|
/coverage
|
||||||
|
/packages/apps/vueapp/src/languages
|
||||||
|
/packages/apps/app/languages
|
@ -10,8 +10,7 @@
|
|||||||
"test:extract": "jest extract",
|
"test:extract": "jest extract",
|
||||||
"test:translate": "jest translate",
|
"test:translate": "jest translate",
|
||||||
"demo:extract": "node ./packages/demo/extract.demo.js",
|
"demo:extract": "node ./packages/demo/extract.demo.js",
|
||||||
"demo:compile": "node ./packages/demo/compile.demo.js",
|
"demo:compile": "node ./packages/demo/compile.demo.js"
|
||||||
"publish":""
|
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
|
|
||||||
const messageIds = require("./idMap")
|
const messageIds = require("./idMap")
|
||||||
const { translate,I18nManager,i18nScope } = require("@voerkai18n/runtime")
|
const { translate,i18nScope } = require("./runtime.js")
|
||||||
|
|
||||||
const formatters = require("./formatters.js")
|
const formatters = require("./formatters.js")
|
||||||
const defaultMessages = require("./cn.js")
|
const defaultMessages = require("./cn.js")
|
||||||
const activeMessages = defaultMessages
|
const activeMessages = defaultMessages
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 语言配置文件
|
// 语言配置文件
|
||||||
const scopeSettings = {
|
const scopeSettings = {
|
||||||
"languages": [
|
"languages": [
|
||||||
@ -40,6 +40,5 @@ const scope = new i18nScope({
|
|||||||
const t = translate.bind(scope)
|
const t = translate.bind(scope)
|
||||||
|
|
||||||
module.exports.t = t
|
module.exports.t = t
|
||||||
module.exports.scope = scope
|
module.exports.i18nScope = scope
|
||||||
module.exports.i18nManager = VoerkaI18n
|
|
||||||
|
|
||||||
|
@ -1 +1,6 @@
|
|||||||
{"type":"module","dependencies":{"@voerkai18n/cli":"workspace:^1.0.6","@voerkai18n/runtime":"^1.0.0"}}
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@voerkai18n/cli": "workspace:^1.0.6",
|
||||||
|
"@voerkai18n/runtime": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,8 @@
|
|||||||
// This starter template is using Vue 3 <script setup> SFCs
|
// This starter template is using Vue 3 <script setup> SFCs
|
||||||
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
|
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
|
||||||
import HelloWorld from './components/HelloWorld.vue'
|
import HelloWorld from './components/HelloWorld.vue'
|
||||||
|
import { t } from './languages'
|
||||||
|
|
||||||
console.log(t("Hello world!"))
|
console.log(t("Hello world!"))
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
|
||||||
import messageIds from "./idMap.js"
|
import messageIds from "./idMap.js"
|
||||||
import { translate,I18nManager,i18nScope } from "@voerkai18n/runtime"
|
import runtime from "./runtime.js"
|
||||||
|
const { translate,i18nScope } = runtime
|
||||||
|
|
||||||
import formatters from "./formatters.js"
|
import formatters from "./formatters.js"
|
||||||
import defaultMessages from "./cn.js"
|
import defaultMessages from "./cn.js"
|
||||||
const activeMessages = defaultMessages
|
const activeMessages = defaultMessages
|
||||||
@ -40,7 +42,6 @@ const t = translate.bind(scope)
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
t,
|
t,
|
||||||
i18nScope:scope,
|
i18nScope as scope
|
||||||
i18nManager:VoerkaI18n,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import { t, i18nScope } from './languages'
|
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
createApp(App).mount('#app')
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ function normalizeCompileOptions(opts={}) {
|
|||||||
|
|
||||||
module.exports =async function compile(langFolder,opts={}){
|
module.exports =async function compile(langFolder,opts={}){
|
||||||
const options = normalizeCompileOptions(opts);
|
const options = normalizeCompileOptions(opts);
|
||||||
let { moduleType } = options;
|
let { moduleType,inlineRuntime } = options;
|
||||||
// 如果自动则会从当前项目读取,如果没有指定则会是esm
|
// 如果自动则会从当前项目读取,如果没有指定则会是esm
|
||||||
if(moduleType==="auto"){
|
if(moduleType==="auto"){
|
||||||
moduleType = findModuleType(langFolder)
|
moduleType = findModuleType(langFolder)
|
||||||
@ -51,7 +51,11 @@ module.exports =async function compile(langFolder,opts={}){
|
|||||||
const projectPackageJson = getCurrentPackageJson(langFolder)
|
const projectPackageJson = getCurrentPackageJson(langFolder)
|
||||||
// 加载多语言配置文件
|
// 加载多语言配置文件
|
||||||
const settingsFile = path.join(langFolder,"settings.json")
|
const settingsFile = path.join(langFolder,"settings.json")
|
||||||
|
|
||||||
|
|
||||||
try{
|
try{
|
||||||
|
|
||||||
|
|
||||||
// 读取多语言配置文件
|
// 读取多语言配置文件
|
||||||
const langSettings = fs.readJSONSync(settingsFile)
|
const langSettings = fs.readJSONSync(settingsFile)
|
||||||
let { languages,defaultLanguage,activeLanguage,namespaces } = langSettings
|
let { languages,defaultLanguage,activeLanguage,namespaces } = langSettings
|
||||||
@ -80,7 +84,7 @@ module.exports =async function compile(langFolder,opts={}){
|
|||||||
logger.log(t("读取语言文件{}失败:{}"),file,e.message)
|
logger.log(t("读取语言文件{}失败:{}"),file,e.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
logger.log(t(" - 共合成{}条语言包文本"),Object.keys(messages).length)
|
logger.log(t(" - 共合成{}条文本"),Object.keys(messages).length)
|
||||||
|
|
||||||
// 2. 为每一个文本内容生成一个唯一的id
|
// 2. 为每一个文本内容生成一个唯一的id
|
||||||
let messageIds = {}
|
let messageIds = {}
|
||||||
@ -113,8 +117,19 @@ module.exports =async function compile(langFolder,opts={}){
|
|||||||
}
|
}
|
||||||
logger.log(t(" - idMap文件: {}"),path.basename(idMapFile))
|
logger.log(t(" - idMap文件: {}"),path.basename(idMapFile))
|
||||||
|
|
||||||
|
// 嵌入运行时源码
|
||||||
|
if(inlineRuntime){
|
||||||
|
const runtimeSourceFolder = path.join(require.resolve("@voerkai18n/runtime"),"../..")
|
||||||
|
fs.copyFileSync(
|
||||||
|
path.join(runtimeSourceFolder,"dist",`runtime.${moduleType === 'esm' ? 'mjs' : 'cjs'}`),
|
||||||
|
path.join(langFolder,"runtime.js")
|
||||||
|
)
|
||||||
|
logger.log(t(" - 运行时: {}"),"runtime.js")
|
||||||
|
}
|
||||||
|
|
||||||
const templateContext = {
|
const templateContext = {
|
||||||
scopeId:projectPackageJson.name,
|
scopeId:projectPackageJson.name,
|
||||||
|
inlineRuntime,
|
||||||
languages,
|
languages,
|
||||||
defaultLanguage,
|
defaultLanguage,
|
||||||
activeLanguage,
|
activeLanguage,
|
||||||
|
@ -10,7 +10,6 @@ const deepmerge = require("deepmerge")
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
const createLogger = require("logsets")
|
const createLogger = require("logsets")
|
||||||
const { replaceInterpolateVars,getDataTypeName } = require("@voerkai18n/runtime")
|
|
||||||
const { findModuleType,createPackageJsonFile,t } = require("./utils")
|
const { findModuleType,createPackageJsonFile,t } = require("./utils")
|
||||||
const logger = createLogger()
|
const logger = createLogger()
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ program
|
|||||||
.option('-r, --reset', t('重新生成当前项目的语言配置'))
|
.option('-r, --reset', t('重新生成当前项目的语言配置'))
|
||||||
.option('-lngs, --languages <languages...>', t('支持的语言列表'), ['cn','en'])
|
.option('-lngs, --languages <languages...>', t('支持的语言列表'), ['cn','en'])
|
||||||
.option('-d, --defaultLanguage <name>', t('默认语言'), 'cn')
|
.option('-d, --defaultLanguage <name>', t('默认语言'), 'cn')
|
||||||
.option('-i, --installRuntime', t('自动安装默认语言'),true)
|
// .option('-i, --installRuntime', t('自动安装默认语言'),true)
|
||||||
.option('-a, --activeLanguage <name>', t('激活语言'), 'cn')
|
.option('-a, --activeLanguage <name>', t('激活语言'), 'cn')
|
||||||
.hook("preAction",async function(location){
|
.hook("preAction",async function(location){
|
||||||
const lang= process.env.LANGUAGE || "cn"
|
const lang= process.env.LANGUAGE || "cn"
|
||||||
@ -104,6 +104,7 @@ program
|
|||||||
.command('compile')
|
.command('compile')
|
||||||
.description(t('编译指定项目的语言包'))
|
.description(t('编译指定项目的语言包'))
|
||||||
.option('-d, --debug', t('输出调试信息'))
|
.option('-d, --debug', t('输出调试信息'))
|
||||||
|
.option('--no-inline-runtime', t('不嵌入运行时源码'))
|
||||||
.option('-m, --moduleType [types]', t('输出模块类型,取值auto,esm,cjs'), 'esm')
|
.option('-m, --moduleType [types]', t('输出模块类型,取值auto,esm,cjs'), 'esm')
|
||||||
.argument('[location]', t('工程项目所在目录'),"./")
|
.argument('[location]', t('工程项目所在目录'),"./")
|
||||||
.hook("preAction",async function(location){
|
.hook("preAction",async function(location){
|
||||||
|
@ -56,10 +56,10 @@ module.exports = function(srcPath,{debug = true,languages=["cn","en"],defaultLan
|
|||||||
fs.writeFileSync(settingsFile,JSON.stringify(settings,null,4))
|
fs.writeFileSync(settingsFile,JSON.stringify(settings,null,4))
|
||||||
|
|
||||||
// 自动安装运行时@voerkai18n/runtime
|
// 自动安装运行时@voerkai18n/runtime
|
||||||
if(installRuntime){
|
// if(installRuntime){
|
||||||
logger.log(t("正在安装多语言运行时:{}"),"@voerkai18n/runtime")
|
// logger.log(t("正在安装多语言运行时:{}"),"@voerkai18n/runtime")
|
||||||
installVoerkai18nRuntim(srcPath)
|
// installVoerkai18nRuntim(srcPath)
|
||||||
}
|
// }
|
||||||
|
|
||||||
if(debug) {
|
if(debug) {
|
||||||
logger.log(t("生成语言配置文件:{}"),"./languages/settings.json")
|
logger.log(t("生成语言配置文件:{}"),"./languages/settings.json")
|
||||||
|
@ -38,9 +38,12 @@ module.exports = {
|
|||||||
"37": "模块类型\\t: {}",
|
"37": "模块类型\\t: {}",
|
||||||
"38": "编译结果输出至:{}",
|
"38": "编译结果输出至:{}",
|
||||||
"39": "读取语言文件{}失败:{}",
|
"39": "读取语言文件{}失败:{}",
|
||||||
"40": " - 共合成{}条语言包文本",
|
"40": " - 语言包文件: {}",
|
||||||
"41": " - 语言包文件: {}",
|
"41": " - idMap文件: {}",
|
||||||
"42": " - idMap文件: {}",
|
"42": " - 格式化器:{}",
|
||||||
"43": " - 格式化器:{}",
|
"43": "Now is { value | date | bjTime }",
|
||||||
"44": "Now is { value | date | bjTime }"
|
"44": " - 共合成{}条文本",
|
||||||
|
"45": " - 运行时: {}",
|
||||||
|
"46": "自动安装默认语言",
|
||||||
|
"47": "不嵌入运行时源码"
|
||||||
}
|
}
|
@ -38,9 +38,12 @@ module.exports = {
|
|||||||
"37": "Type of module\\t\\t: {}",
|
"37": "Type of module\\t\\t: {}",
|
||||||
"38": "Compile to:{}",
|
"38": "Compile to:{}",
|
||||||
"39": "Error while read language file{}: {}",
|
"39": "Error while read language file{}: {}",
|
||||||
"40": " - Total {} messages",
|
"40": " - Language file: {}",
|
||||||
"41": " - Language file: {}",
|
"41": " - idMap file: {}",
|
||||||
"42": " - idMap file: {}",
|
"42": " - Formatters: {}",
|
||||||
"43": " - Formatters: {}",
|
"43": "Now is { value | date | bjTime }",
|
||||||
"44": "Now is { value | date | bjTime }"
|
"44": " - Total{} messages",
|
||||||
|
"45": " - Runtime: {}",
|
||||||
|
"46": "Auto install default language",
|
||||||
|
"47": "Not inline runtime source"
|
||||||
}
|
}
|
@ -38,9 +38,12 @@ module.exports = {
|
|||||||
"模块类型\\t: {}": 37,
|
"模块类型\\t: {}": 37,
|
||||||
"编译结果输出至:{}": 38,
|
"编译结果输出至:{}": 38,
|
||||||
"读取语言文件{}失败:{}": 39,
|
"读取语言文件{}失败:{}": 39,
|
||||||
" - 共合成{}条语言包文本": 40,
|
" - 语言包文件: {}": 40,
|
||||||
" - 语言包文件: {}": 41,
|
" - idMap文件: {}": 41,
|
||||||
" - idMap文件: {}": 42,
|
" - 格式化器:{}": 42,
|
||||||
" - 格式化器:{}": 43,
|
"Now is { value | date | bjTime }": 43,
|
||||||
"Now is { value | date | bjTime }": 44
|
" - 共合成{}条文本": 44,
|
||||||
|
" - 运行时: {}": 45,
|
||||||
|
"自动安装默认语言": 46,
|
||||||
|
"不嵌入运行时源码": 47
|
||||||
}
|
}
|
@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
const messageIds = require("./idMap")
|
const messageIds = require("./idMap")
|
||||||
const { translate,I18nManager,i18nScope } = require("@voerkai18n/runtime")
|
const { translate,i18nScope } = require("./runtime.js")
|
||||||
|
|
||||||
const formatters = require("./formatters.js")
|
const formatters = require("./formatters.js")
|
||||||
const defaultMessages = require("./cn.js")
|
const defaultMessages = require("./cn.js")
|
||||||
const activeMessages = defaultMessages
|
const activeMessages = defaultMessages
|
||||||
|
924
packages/cli/languages/runtime.js
Normal file
924
packages/cli/languages/runtime.js
Normal file
@ -0,0 +1,924 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* 简单的事件触发器
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
var eventemitter = class EventEmitter{
|
||||||
|
constructor(){
|
||||||
|
this._callbacks = [];
|
||||||
|
}
|
||||||
|
on(callback){
|
||||||
|
if(this._callbacks.includes(callback)) return
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offAll(){
|
||||||
|
this._callbacks = [];
|
||||||
|
}
|
||||||
|
async emit(...args){
|
||||||
|
if(Promise.allSettled){
|
||||||
|
await Promise.allSettled(this._callbacks.map(cb=>cb(...args)));
|
||||||
|
}else {
|
||||||
|
await Promise.all(this._callbacks.map(cb=>cb(...args)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var scope = class i18nScope {
|
||||||
|
constructor(options={},callback){
|
||||||
|
// 每个作用域都有一个唯一的id
|
||||||
|
this._id = options.id || (new Date().getTime().toString()+parseInt(Math.random()*1000));
|
||||||
|
this._languages = options.languages; // 当前作用域的语言列表
|
||||||
|
this._defaultLanguage = options.defaultLanguage || "cn"; // 默认语言名称
|
||||||
|
this._activeLanguage = options.activeLanguage; // 当前语言名称
|
||||||
|
this._default = options.default; // 默认语言包
|
||||||
|
this._messages = options.messages; // 当前语言包
|
||||||
|
this._idMap = options.idMap; // 消息id映射列表
|
||||||
|
this._formatters = options.formatters; // 当前作用域的格式化函数列表
|
||||||
|
this._loaders = options.loaders; // 异步加载语言文件的函数列表
|
||||||
|
this._global = null; // 引用全局VoerkaI18n配置,注册后自动引用
|
||||||
|
// 主要用来缓存格式化器的引用,当使用格式化器时可以直接引用,避免检索
|
||||||
|
this.$cache={
|
||||||
|
activeLanguage : null,
|
||||||
|
typedFormatters: {},
|
||||||
|
formatters : {},
|
||||||
|
};
|
||||||
|
// 如果不存在全局VoerkaI18n实例,说明当前Scope是唯一或第一个加载的作用域,
|
||||||
|
// 则使用当前作用域来初始化全局VoerkaI18n实例
|
||||||
|
if(!globalThis.VoerkaI18n){
|
||||||
|
const { I18nManager } = runtime;
|
||||||
|
globalThis.VoerkaI18n = new I18nManager({
|
||||||
|
defaultLanguage: this.defaultLanguage,
|
||||||
|
activeLanguage : this.activeLanguage,
|
||||||
|
languages: options.languages,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.global = globalThis.VoerkaI18n;
|
||||||
|
// 正在加载语言包标识
|
||||||
|
this._loading=false;
|
||||||
|
// 在全局注册作用域
|
||||||
|
this.register(callback);
|
||||||
|
}
|
||||||
|
// 作用域
|
||||||
|
get id(){return this._id}
|
||||||
|
// 默认语言名称
|
||||||
|
get defaultLanguage(){return this._defaultLanguage}
|
||||||
|
// 默认语言名称
|
||||||
|
get activeLanguage(){return this._activeLanguage}
|
||||||
|
// 默认语言包
|
||||||
|
get default(){return this._default}
|
||||||
|
// 当前语言包
|
||||||
|
get messages(){return this._messages}
|
||||||
|
// 消息id映射列表
|
||||||
|
get idMap(){return this._idMap}
|
||||||
|
// 当前作用域的格式化函数列表
|
||||||
|
get formatters(){return this._formatters}
|
||||||
|
// 异步加载语言文件的函数列表
|
||||||
|
get loaders(){return this._loaders}
|
||||||
|
// 引用全局VoerkaI18n配置,注册后自动引用
|
||||||
|
get global(){return this._global}
|
||||||
|
set global(value){this._global = value;}
|
||||||
|
/**
|
||||||
|
* 在全局注册作用域
|
||||||
|
* @param {*} callback 当注册
|
||||||
|
*/
|
||||||
|
register(callback){
|
||||||
|
if(!typeof(callback)==="function") callback = ()=>{};
|
||||||
|
this.global.register(this).then(callback).catch(callback);
|
||||||
|
}
|
||||||
|
registerFormatter(name,formatter,{language="*"}={}){
|
||||||
|
if(!typeof(formatter)==="function" || typeof(name)!=="string"){
|
||||||
|
throw new TypeError("Formatter must be a function")
|
||||||
|
}
|
||||||
|
if(DataTypes.includes(name)){
|
||||||
|
this.formatters[language].$types[name] = formatter;
|
||||||
|
}else {
|
||||||
|
this.formatters[language][name] = formatter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 回退到默认语言
|
||||||
|
*/
|
||||||
|
_fallback(){
|
||||||
|
this._messages = this._default;
|
||||||
|
this._activeLanguage = this.defaultLanguage;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 刷新当前语言包
|
||||||
|
* @param {*} newLanguage
|
||||||
|
*/
|
||||||
|
async refresh(newLanguage){
|
||||||
|
this._loading = Promise.resolve();
|
||||||
|
if(!newLanguage) newLanguage = this.activeLanguage;
|
||||||
|
// 默认语言,默认语言采用静态加载方式,只需要简单的替换即可
|
||||||
|
if(newLanguage === this.defaultLanguage){
|
||||||
|
this._messages = this._default;
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 非默认语言需要异步加载语言包文件,加载器是一个异步函数
|
||||||
|
// 如果没有加载器,则无法加载语言包,因此回退到默认语言
|
||||||
|
const loader = this.loaders[newLanguage];
|
||||||
|
if(typeof(loader) === "function"){
|
||||||
|
try{
|
||||||
|
this._messages = (await loader()).default;
|
||||||
|
this._activeLanguage = newLanguage;
|
||||||
|
}catch(e){
|
||||||
|
console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`);
|
||||||
|
this._fallback();
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
this._fallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 以下方法引用全局VoerkaI18n实例的方法
|
||||||
|
get on(){return this.global.on.bind(this.global)}
|
||||||
|
get off(){return this.global.off.bind(this.global)}
|
||||||
|
get offAll(){return this.global.offAll.bind(this.global)}
|
||||||
|
get change(){
|
||||||
|
return this.global.change.bind(this.global)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内置的格式化器
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典格式化器
|
||||||
|
* 根据输入data的值,返回后续参数匹配的结果
|
||||||
|
* dict(data,<value1>,<result1>,<value2>,<result1>,<value3>,<result1>,...)
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* dict(1,1,"one",2,"two",3,"three",4,"four") == "one"
|
||||||
|
* dict(2,1,"one",2,"two",3,"three",4,"four") == "two"
|
||||||
|
* dict(3,1,"one",2,"two",3,"three",4,"four") == "three"
|
||||||
|
* dict(4,1,"one",2,"two",3,"three",4,"four") == "four"
|
||||||
|
* // 无匹配时返回原始值
|
||||||
|
* dict(5,1,"one",2,"two",3,"three",4,"four") == 5
|
||||||
|
* // 无匹配时并且后续参数个数是奇数,则返回最后一个参数
|
||||||
|
* dict(5,1,"one",2,"two",3,"three",4,"four","more") == "more"
|
||||||
|
*
|
||||||
|
* 在翻译中使用
|
||||||
|
* I have { value | dict(1,"one",2,"two",3,"three",4,"four")} apples
|
||||||
|
*
|
||||||
|
* @param {*} value
|
||||||
|
* @param {...any} args
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function dict(value,...args){
|
||||||
|
for(let i=0;i<args.length;i+=2){
|
||||||
|
if(args[i]===value){
|
||||||
|
return args[i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(args.length >0 && (args.length % 2!==0)) return args[args.length-1]
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
var formatters$1 = {
|
||||||
|
"*":{
|
||||||
|
$types:{
|
||||||
|
Date:(value)=>value.toLocaleString()
|
||||||
|
},
|
||||||
|
time:(value)=> value.toLocaleTimeString(),
|
||||||
|
shorttime:(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()}秒`
|
||||||
|
},
|
||||||
|
shortime:(value)=> value.toLocaleTimeString(),
|
||||||
|
time:(value)=>`${value.getHours()}点${value.getMinutes()}分${value.getSeconds()}秒`,
|
||||||
|
date: (value)=> `${value.getFullYear()}年${value.getMonth()+1}月${value.getDate()}日`,
|
||||||
|
shortdate: (value)=> `${value.getFullYear()}-${value.getMonth()+1}-${value.getDate()}`,
|
||||||
|
currency:(value)=>`${value}元`,
|
||||||
|
},
|
||||||
|
en:{
|
||||||
|
currency:(value)=>{
|
||||||
|
return `$${value}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const EventEmitter = eventemitter;
|
||||||
|
const i18nScope = scope;
|
||||||
|
let formatters = formatters$1;
|
||||||
|
|
||||||
|
|
||||||
|
// 用来提取字符里面的插值变量参数 , 支持管道符 { 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("}")
|
||||||
|
}
|
||||||
|
const DataTypes$1 = ["String","Number","Boolean","Object","Array","Function","Error","Symbol","RegExp","Date","Null","Undefined","Set","Map","WeakSet","WeakMap"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定变量类型名称
|
||||||
|
* 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单进行对象合并
|
||||||
|
*
|
||||||
|
* options={
|
||||||
|
* array:0 , // 数组合并策略,0-替换,1-合并,2-去重合并
|
||||||
|
* object:0, // 对象合并策略,0-替换,1-合并,2-去重合并
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @param {*} toObj
|
||||||
|
* @param {*} formObj
|
||||||
|
* @returns 合并后的对象
|
||||||
|
*/
|
||||||
|
function deepMerge(toObj,formObj,options={}){
|
||||||
|
let results = Object.assign({},toObj);
|
||||||
|
Object.entries(formObj).forEach(([key,value])=>{
|
||||||
|
if(key in results){
|
||||||
|
if(typeof value === "object" && value !== null){
|
||||||
|
if(Array.isArray(value)){
|
||||||
|
if(options.array === 0){
|
||||||
|
results[key] = value;
|
||||||
|
}else if(options.array === 1){
|
||||||
|
results[key] = [...results[key],...value];
|
||||||
|
}else if(options.array === 2){
|
||||||
|
results[key] = [...new Set([...results[key],...value])];
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
results[key] = deepMerge(results[key],value,options);
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
results[key] = value;
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
results[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
通过正则表达式对原始文本内容进行解析匹配后得到的
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetScopeCache(scope,activeLanguage=null){
|
||||||
|
scope.$cache = {activeLanguage,typedFormatters:{},formatters:{}};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 取得指定数据类型的默认格式化器
|
||||||
|
*
|
||||||
|
* 可以为每一个数据类型指定一个默认的格式化器,当传入插值变量时,
|
||||||
|
* 会自动调用该格式化器来对值进行格式化转换
|
||||||
|
|
||||||
|
const formatters = {
|
||||||
|
"*":{
|
||||||
|
$types:{...} // 在所有语言下只作用于特定数据类型的格式化器
|
||||||
|
}, // 在所有语言下生效的格式化器
|
||||||
|
cn:{
|
||||||
|
$types:{
|
||||||
|
[数据类型]:(value)=>{...},
|
||||||
|
},
|
||||||
|
[格式化器名称]:(value)=>{...},
|
||||||
|
[格式化器名称]:(value)=>{...},
|
||||||
|
[格式化器名称]:(value)=>{...},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
* @param {*} scope
|
||||||
|
* @param {*} activeLanguage
|
||||||
|
* @param {*} dataType 数字类型
|
||||||
|
* @returns {Function} 格式化函数
|
||||||
|
*/
|
||||||
|
function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){
|
||||||
|
if(!scope.$cache) resetScopeCache(scope);
|
||||||
|
if(scope.$cache.activeLanguage === activeLanguage) {
|
||||||
|
if(dataType in scope.$cache.typedFormatters) return scope.$cache.typedFormatters[dataType]
|
||||||
|
}else {// 当语言切换时清空缓存
|
||||||
|
resetScopeCache(scope,activeLanguage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先在当前作用域中查找,再在全局查找
|
||||||
|
const targets = [scope.formatters,scope.global.formatters];
|
||||||
|
for(const target of targets){
|
||||||
|
if(!target) continue
|
||||||
|
// 优先在当前语言的$types中查找
|
||||||
|
if((activeLanguage in target) && isPlainObject(target[activeLanguage].$types)){
|
||||||
|
let formatters = target[activeLanguage].$types;
|
||||||
|
if(dataType in formatters && typeof(formatters[dataType])==="function"){
|
||||||
|
return scope.$cache.typedFormatters[dataType] = formatters[dataType]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 在所有语言的$types中查找
|
||||||
|
if(("*" in target) && isPlainObject(target["*"].$types)){
|
||||||
|
let formatters = target["*"].$types;
|
||||||
|
if(dataType in formatters && typeof(formatters[dataType])==="function"){
|
||||||
|
return scope.$cache.typedFormatters[dataType] = formatters[dataType]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定名称的格式化器函数
|
||||||
|
* @param {*} scope
|
||||||
|
* @param {*} activeLanguage
|
||||||
|
* @param {*} name 格式化器名称
|
||||||
|
* @returns {Function} 格式化函数
|
||||||
|
*/
|
||||||
|
function getFormatter(scope,activeLanguage,name){
|
||||||
|
// 缓存格式化器引用,避免重复检索
|
||||||
|
if(!scope.$cache) resetScopeCache(scope);
|
||||||
|
if(scope.$cache.activeLanguage === activeLanguage) {
|
||||||
|
if(name in scope.$cache.formatters) return scope.$cache.formatters[name]
|
||||||
|
}else {// 当语言切换时清空缓存
|
||||||
|
resetScopeCache(scope,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 scope.$cache.formatters[name] = formatters[name]
|
||||||
|
}
|
||||||
|
// 在所有语言的$types中查找
|
||||||
|
let formatters = target["*"] || {};
|
||||||
|
if((name in formatters) && typeof(formatters[name])==="function") return scope.$cache.formatters[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 {
|
||||||
|
// 格式化器无效或者没有定义时,查看当前值是否具有同名的原型方法,如果有则执行调用
|
||||||
|
// 比如padStart格式化器是String的原型方法,不需要配置就可以直接作为格式化器调用
|
||||||
|
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);
|
||||||
|
// 2. 查找每种数据类型默认格式化器,并添加到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:[
|
||||||
|
{name:"cn",title:"中文",default:true},
|
||||||
|
{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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function escape(str){
|
||||||
|
return str.replaceAll(/\\(?![trnbvf'"]{1})/g,"\\\\")
|
||||||
|
.replaceAll("\t","\\t")
|
||||||
|
.replaceAll("\n","\\n")
|
||||||
|
.replaceAll("\b","\\b")
|
||||||
|
.replaceAll("\r","\\r")
|
||||||
|
.replaceAll("\f","\\f")
|
||||||
|
.replaceAll("\'","\\'")
|
||||||
|
.replaceAll('\"','\\"')
|
||||||
|
.replaceAll('\v','\\v')
|
||||||
|
}
|
||||||
|
function unescape(str){
|
||||||
|
return str
|
||||||
|
.replaceAll("\\t","\t")
|
||||||
|
.replaceAll("\\n","\n")
|
||||||
|
.replaceAll("\\b","\b")
|
||||||
|
.replaceAll("\\r","\r")
|
||||||
|
.replaceAll("\\f","\f")
|
||||||
|
.replaceAll("\\'","\'")
|
||||||
|
.replaceAll('\\"','\"')
|
||||||
|
.replaceAll('\\v','\v')
|
||||||
|
.replaceAll(/\\\\(?![trnbvf'"]{1})/g,"\\")
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 翻译函数
|
||||||
|
*
|
||||||
|
* 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; // 复数值
|
||||||
|
if(!typeof(message)==="string") return message
|
||||||
|
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("$") && typeof(vars[name])==="number") 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
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 3. 取得翻译文本模板字符串
|
||||||
|
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
|
||||||
|
// JSON.stringify在进行转换时会将\t\n\r转换为\\t\\n\\r,这样在进行匹配时就出错
|
||||||
|
let msgId = isMessageId(content) ? content : scope.idMap[escape(content)];
|
||||||
|
content = scope.messages[msgId] || content;
|
||||||
|
content = Array.isArray(content) ? content.map(v=>unescape(v)) : unescape(content);
|
||||||
|
}
|
||||||
|
// 2. 处理复数
|
||||||
|
// 经过上面的处理,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 extends EventEmitter{
|
||||||
|
constructor(settings={}){
|
||||||
|
super();
|
||||||
|
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}
|
||||||
|
// 全局格式化器
|
||||||
|
get formatters(){ return formatters }
|
||||||
|
/**
|
||||||
|
* 切换语言
|
||||||
|
*/
|
||||||
|
async change(value){
|
||||||
|
value=value.trim();
|
||||||
|
if(this.languages.findIndex(lang=>lang.name === value)!==-1){
|
||||||
|
// 通知所有作用域刷新到对应的语言包
|
||||||
|
await this._refreshScopes(value);
|
||||||
|
this._settings.activeLanguage = value;
|
||||||
|
/// 触发语言切换事件
|
||||||
|
await this.emit(value);
|
||||||
|
}else {
|
||||||
|
throw new Error("Not supported language:"+value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 当切换语言时调用此方法来加载更新语言包
|
||||||
|
* @param {*} newLanguage
|
||||||
|
*/
|
||||||
|
async _refreshScopes(newLanguage){
|
||||||
|
// 并发执行所有作用域语言包的加载
|
||||||
|
try{
|
||||||
|
const scopeRefreshers = this._scopes.map(scope=>{
|
||||||
|
return scope.refresh(newLanguage)
|
||||||
|
});
|
||||||
|
if(Promise.allSettled){
|
||||||
|
await Promise.allSettled(scopeRefreshers);
|
||||||
|
}else {
|
||||||
|
await Promise.all(scopeRefreshers);
|
||||||
|
}
|
||||||
|
}catch(e){
|
||||||
|
console.warn("Error while refreshing i18n scopes:",e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* 注册一个新的作用域
|
||||||
|
*
|
||||||
|
* 每一个库均对应一个作用域,每个作用域可以有多个语言包,且对应一个翻译函数
|
||||||
|
* 除了默认语言外,其他语言采用动态加载的方式
|
||||||
|
*
|
||||||
|
* @param {*} scope
|
||||||
|
*/
|
||||||
|
async register(scope){
|
||||||
|
if(!(scope instanceof i18nScope)){
|
||||||
|
throw new TypeError("Scope must be an instance of I18nScope")
|
||||||
|
}
|
||||||
|
this._scopes.push(scope);
|
||||||
|
await scope.refresh(this.activeLanguage);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 注册全局格式化器
|
||||||
|
* 格式化器是一个简单的同步函数value=>{...},用来对输入进行格式化后返回结果
|
||||||
|
*
|
||||||
|
* registerFormatters(name,value=>{...}) // 适用于所有语言
|
||||||
|
* registerFormatters(name,value=>{...},{langauge:"cn"}) // 适用于cn语言
|
||||||
|
* registerFormatters(name,value=>{...},{langauge:"en"}) // 适用于en语言
|
||||||
|
|
||||||
|
* @param {*} formatters
|
||||||
|
*/
|
||||||
|
registerFormatter(name,formatter,{language="*"}={}){
|
||||||
|
if(!typeof(formatter)==="function" || typeof(name)!=="string"){
|
||||||
|
throw new TypeError("Formatter must be a function")
|
||||||
|
}
|
||||||
|
if(DataTypes$1.includes(name)){
|
||||||
|
this.formatters[language].$types[name] = formatter;
|
||||||
|
}else {
|
||||||
|
this.formatters[language][name] = formatter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var runtime ={
|
||||||
|
getInterpolatedVars,
|
||||||
|
replaceInterpolatedVars,
|
||||||
|
I18nManager,
|
||||||
|
translate,
|
||||||
|
languages,
|
||||||
|
i18nScope,
|
||||||
|
defaultLanguageSettings,
|
||||||
|
getDataTypeName,
|
||||||
|
isNumber,
|
||||||
|
isPlainObject
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = runtime;
|
@ -237,12 +237,6 @@
|
|||||||
"compile.command.js"
|
"compile.command.js"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
" - 共合成{}条语言包文本": {
|
|
||||||
"en": " - Total {} messages",
|
|
||||||
"$file": [
|
|
||||||
"compile.command.js"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
" - 语言包文件: {}": {
|
" - 语言包文件: {}": {
|
||||||
"en": " - Language file: {}",
|
"en": " - Language file: {}",
|
||||||
"$file": [
|
"$file": [
|
||||||
@ -266,5 +260,29 @@
|
|||||||
"$file": [
|
"$file": [
|
||||||
"templates\\formatters.js"
|
"templates\\formatters.js"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
" - 共合成{}条文本": {
|
||||||
|
"en": " - Total{} messages",
|
||||||
|
"$file": [
|
||||||
|
"compile.command.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
" - 运行时: {}": {
|
||||||
|
"en": " - Runtime: {}",
|
||||||
|
"$file": [
|
||||||
|
"compile.command.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"自动安装默认语言": {
|
||||||
|
"en": "Auto install default language",
|
||||||
|
"$file": [
|
||||||
|
"index.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"不嵌入运行时源码": {
|
||||||
|
"en": "Not inline runtime source",
|
||||||
|
"$file": [
|
||||||
|
"index.js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@voerkai18n/cli",
|
"name": "@voerkai18n/cli",
|
||||||
"version": "1.0.7",
|
"version": "1.0.10",
|
||||||
"description": "VoerkaI18n command line interactive tools",
|
"description": "VoerkaI18n command line interactive tools",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"homepage": "https://gitee.com/zhangfisher/voerka-i18n",
|
"homepage": "https://gitee.com/zhangfisher/voerka-i18n",
|
||||||
@ -18,13 +18,13 @@
|
|||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"extract": "node ./index.js extract -d -e babel-plugin-voerkai18n.js,templates/**",
|
"extract": "node ./index.js extract -d -e babel-plugin-voerkai18n.js,templates/**",
|
||||||
"compile": "node ./index.js compile -d",
|
"compile": "node ./index.js compile -d",
|
||||||
"compile:en": "cross-env LANGUAGE=en node ./index.js compile -d"
|
"compile:en": "cross-env LANGUAGE=en node ./index.js compile -d",
|
||||||
|
"release": "npm version patch && pnpm publish --no-git-checks --access public"
|
||||||
},
|
},
|
||||||
"author": "wxzhang",
|
"author": "wxzhang",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"voerkai18n": "./index.js",
|
"voerkai18n": "./index.js"
|
||||||
"publish": "npm publish -access public"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/cli": "^7.17.6",
|
"@babel/cli": "^7.17.6",
|
||||||
|
@ -11,12 +11,11 @@
|
|||||||
Arguments:
|
Arguments:
|
||||||
location 工程项目所在目录
|
location 工程项目所在目录
|
||||||
Options:
|
Options:
|
||||||
-d, --debug 输出调试信息
|
-D, --debug 输出调试信息
|
||||||
-r, --reset 重新生成当前项目的语言配置
|
-r, --reset 重新生成当前项目的语言配置
|
||||||
-m, --moduleType [type] 生成的js模块类型,取值auto,esm,cjs (default: "auto")
|
|
||||||
-lngs, --languages <languages...> 支持的语言列表 (default: ["cn","en"])
|
-lngs, --languages <languages...> 支持的语言列表 (default: ["cn","en"])
|
||||||
-default, --defaultLanguage 默认语言
|
-d, --defaultLanguage 默认语言
|
||||||
-active, --activeLanguage 激活语言
|
-a, --activeLanguage 激活语言
|
||||||
-h, --help display help for command
|
-h, --help display help for command
|
||||||
|
|
||||||
|
|
||||||
@ -33,10 +32,10 @@ Arguments:
|
|||||||
location 工程项目所在目录 (default: "./")
|
location 工程项目所在目录 (default: "./")
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-d, --debug 输出调试信息
|
-D, --debug 输出调试信息
|
||||||
-lngs, --languages 支持的语言
|
-lngs, --languages 支持的语言
|
||||||
-default, --defaultLanguage 默认语言
|
-d, --defaultLanguage 默认语言
|
||||||
-active, --activeLanguage 激活语言
|
-a, --activeLanguage 激活语言
|
||||||
-ns, --namespaces 翻译名称空间
|
-ns, --namespaces 翻译名称空间
|
||||||
-e, --exclude <folders> 排除要扫描的文件夹,多个用逗号分隔
|
-e, --exclude <folders> 排除要扫描的文件夹,多个用逗号分隔
|
||||||
-u, --updateMode 本次提取内容与已存在内容的数据合并策略,默认取值sync=同步,overwrite=覆盖,merge=合并
|
-u, --updateMode 本次提取内容与已存在内容的数据合并策略,默认取值sync=同步,overwrite=覆盖,merge=合并
|
||||||
@ -58,7 +57,7 @@ Arguments:
|
|||||||
|
|
||||||
Options:
|
Options:
|
||||||
-d, --debug 输出调试信息
|
-d, --debug 输出调试信息
|
||||||
-m, --moduleType [types] 输出模块类型,取值auto,esm,cjs (default: "auto")
|
-m, --moduleType [types] 输出模块类型,取值auto,esm,cjs (default: "esm")
|
||||||
-h, --help display help for command
|
-h, --help display help for command
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
{{if moduleType === "esm"}}
|
{{if moduleType === "esm"}}
|
||||||
import messageIds from "./idMap.js"
|
import messageIds from "./idMap.js"
|
||||||
import { translate,I18nManager,i18nScope } from "@voerkai18n/runtime"
|
{{if inlineRuntime }}import runtime from "./runtime.js"
|
||||||
|
const { translate,i18nScope } = runtime
|
||||||
|
{{else}}import { translate,i18nScope } from "@voerkai18n/runtime"{{/if}}
|
||||||
import formatters from "./formatters.js"
|
import formatters from "./formatters.js"
|
||||||
import defaultMessages from "./{{defaultLanguage}}.js"
|
import defaultMessages from "./{{defaultLanguage}}.js"
|
||||||
{{if defaultLanguage === activeLanguage}}const activeMessages = defaultMessages
|
{{if defaultLanguage === activeLanguage}}const activeMessages = defaultMessages
|
||||||
{{else}}import activeMessages from "./{{activeLanguage}}.js"{{/if}}
|
{{else}}import activeMessages from "./{{activeLanguage}}.js"{{/if}}
|
||||||
{{else}}
|
{{else}}
|
||||||
const messageIds = require("./idMap")
|
const messageIds = require("./idMap")
|
||||||
const { translate,I18nManager,i18nScope } = require("@voerkai18n/runtime")
|
{{if inlineRuntime }}const { translate,i18nScope } = require("./runtime.js")
|
||||||
|
{{else}}const { translate,i18nScope } = require("@voerkai18n/runtime"){{/if}}
|
||||||
const formatters = require("./formatters.js")
|
const formatters = require("./formatters.js")
|
||||||
const defaultMessages = require("./{{defaultLanguage}}.js")
|
const defaultMessages = require("./{{defaultLanguage}}.js")
|
||||||
{{if defaultLanguage === activeLanguage}}const activeMessages = defaultMessages
|
{{if defaultLanguage === activeLanguage}}const activeMessages = defaultMessages
|
||||||
@ -33,11 +36,9 @@ const t = translate.bind(scope)
|
|||||||
{{if moduleType === "esm"}}
|
{{if moduleType === "esm"}}
|
||||||
export {
|
export {
|
||||||
t,
|
t,
|
||||||
i18nScope:scope,
|
i18nScope as scope
|
||||||
i18nManager:VoerkaI18n,
|
|
||||||
}
|
}
|
||||||
{{else}}
|
{{else}}
|
||||||
module.exports.t = t
|
module.exports.t = t
|
||||||
module.exports.i18nScope = scope
|
module.exports.i18nScope = scope
|
||||||
module.exports.i18nManager = VoerkaI18n
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
25
packages/runtime/babel.config.js
Normal file
25
packages/runtime/babel.config.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
module.exports = {
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"@babel/preset-env",
|
||||||
|
{
|
||||||
|
"useBuiltIns": "usage",
|
||||||
|
"debug": false,
|
||||||
|
"modules": false,
|
||||||
|
"corejs":{
|
||||||
|
"version":"3.21",
|
||||||
|
"proposals": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"@babel/plugin-transform-runtime",
|
||||||
|
{
|
||||||
|
"corejs":3,
|
||||||
|
"proposals": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
3
packages/runtime/dist/index.cjs
vendored
3
packages/runtime/dist/index.cjs
vendored
File diff suppressed because one or more lines are too long
1
packages/runtime/dist/index.cjs.map
vendored
Normal file
1
packages/runtime/dist/index.cjs.map
vendored
Normal file
File diff suppressed because one or more lines are too long
3
packages/runtime/dist/index.esm.js
vendored
3
packages/runtime/dist/index.esm.js
vendored
File diff suppressed because one or more lines are too long
1
packages/runtime/dist/index.esm.js.map
vendored
Normal file
1
packages/runtime/dist/index.esm.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
924
packages/runtime/dist/runtime.cjs
vendored
Normal file
924
packages/runtime/dist/runtime.cjs
vendored
Normal file
@ -0,0 +1,924 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* 简单的事件触发器
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
var eventemitter = class EventEmitter{
|
||||||
|
constructor(){
|
||||||
|
this._callbacks = [];
|
||||||
|
}
|
||||||
|
on(callback){
|
||||||
|
if(this._callbacks.includes(callback)) return
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offAll(){
|
||||||
|
this._callbacks = [];
|
||||||
|
}
|
||||||
|
async emit(...args){
|
||||||
|
if(Promise.allSettled){
|
||||||
|
await Promise.allSettled(this._callbacks.map(cb=>cb(...args)));
|
||||||
|
}else {
|
||||||
|
await Promise.all(this._callbacks.map(cb=>cb(...args)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var scope = class i18nScope {
|
||||||
|
constructor(options={},callback){
|
||||||
|
// 每个作用域都有一个唯一的id
|
||||||
|
this._id = options.id || (new Date().getTime().toString()+parseInt(Math.random()*1000));
|
||||||
|
this._languages = options.languages; // 当前作用域的语言列表
|
||||||
|
this._defaultLanguage = options.defaultLanguage || "cn"; // 默认语言名称
|
||||||
|
this._activeLanguage = options.activeLanguage; // 当前语言名称
|
||||||
|
this._default = options.default; // 默认语言包
|
||||||
|
this._messages = options.messages; // 当前语言包
|
||||||
|
this._idMap = options.idMap; // 消息id映射列表
|
||||||
|
this._formatters = options.formatters; // 当前作用域的格式化函数列表
|
||||||
|
this._loaders = options.loaders; // 异步加载语言文件的函数列表
|
||||||
|
this._global = null; // 引用全局VoerkaI18n配置,注册后自动引用
|
||||||
|
// 主要用来缓存格式化器的引用,当使用格式化器时可以直接引用,避免检索
|
||||||
|
this.$cache={
|
||||||
|
activeLanguage : null,
|
||||||
|
typedFormatters: {},
|
||||||
|
formatters : {},
|
||||||
|
};
|
||||||
|
// 如果不存在全局VoerkaI18n实例,说明当前Scope是唯一或第一个加载的作用域,
|
||||||
|
// 则使用当前作用域来初始化全局VoerkaI18n实例
|
||||||
|
if(!globalThis.VoerkaI18n){
|
||||||
|
const { I18nManager } = runtime;
|
||||||
|
globalThis.VoerkaI18n = new I18nManager({
|
||||||
|
defaultLanguage: this.defaultLanguage,
|
||||||
|
activeLanguage : this.activeLanguage,
|
||||||
|
languages: options.languages,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.global = globalThis.VoerkaI18n;
|
||||||
|
// 正在加载语言包标识
|
||||||
|
this._loading=false;
|
||||||
|
// 在全局注册作用域
|
||||||
|
this.register(callback);
|
||||||
|
}
|
||||||
|
// 作用域
|
||||||
|
get id(){return this._id}
|
||||||
|
// 默认语言名称
|
||||||
|
get defaultLanguage(){return this._defaultLanguage}
|
||||||
|
// 默认语言名称
|
||||||
|
get activeLanguage(){return this._activeLanguage}
|
||||||
|
// 默认语言包
|
||||||
|
get default(){return this._default}
|
||||||
|
// 当前语言包
|
||||||
|
get messages(){return this._messages}
|
||||||
|
// 消息id映射列表
|
||||||
|
get idMap(){return this._idMap}
|
||||||
|
// 当前作用域的格式化函数列表
|
||||||
|
get formatters(){return this._formatters}
|
||||||
|
// 异步加载语言文件的函数列表
|
||||||
|
get loaders(){return this._loaders}
|
||||||
|
// 引用全局VoerkaI18n配置,注册后自动引用
|
||||||
|
get global(){return this._global}
|
||||||
|
set global(value){this._global = value;}
|
||||||
|
/**
|
||||||
|
* 在全局注册作用域
|
||||||
|
* @param {*} callback 当注册
|
||||||
|
*/
|
||||||
|
register(callback){
|
||||||
|
if(!typeof(callback)==="function") callback = ()=>{};
|
||||||
|
this.global.register(this).then(callback).catch(callback);
|
||||||
|
}
|
||||||
|
registerFormatter(name,formatter,{language="*"}={}){
|
||||||
|
if(!typeof(formatter)==="function" || typeof(name)!=="string"){
|
||||||
|
throw new TypeError("Formatter must be a function")
|
||||||
|
}
|
||||||
|
if(DataTypes.includes(name)){
|
||||||
|
this.formatters[language].$types[name] = formatter;
|
||||||
|
}else {
|
||||||
|
this.formatters[language][name] = formatter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 回退到默认语言
|
||||||
|
*/
|
||||||
|
_fallback(){
|
||||||
|
this._messages = this._default;
|
||||||
|
this._activeLanguage = this.defaultLanguage;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 刷新当前语言包
|
||||||
|
* @param {*} newLanguage
|
||||||
|
*/
|
||||||
|
async refresh(newLanguage){
|
||||||
|
this._loading = Promise.resolve();
|
||||||
|
if(!newLanguage) newLanguage = this.activeLanguage;
|
||||||
|
// 默认语言,默认语言采用静态加载方式,只需要简单的替换即可
|
||||||
|
if(newLanguage === this.defaultLanguage){
|
||||||
|
this._messages = this._default;
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 非默认语言需要异步加载语言包文件,加载器是一个异步函数
|
||||||
|
// 如果没有加载器,则无法加载语言包,因此回退到默认语言
|
||||||
|
const loader = this.loaders[newLanguage];
|
||||||
|
if(typeof(loader) === "function"){
|
||||||
|
try{
|
||||||
|
this._messages = (await loader()).default;
|
||||||
|
this._activeLanguage = newLanguage;
|
||||||
|
}catch(e){
|
||||||
|
console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`);
|
||||||
|
this._fallback();
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
this._fallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 以下方法引用全局VoerkaI18n实例的方法
|
||||||
|
get on(){return this.global.on.bind(this.global)}
|
||||||
|
get off(){return this.global.off.bind(this.global)}
|
||||||
|
get offAll(){return this.global.offAll.bind(this.global)}
|
||||||
|
get change(){
|
||||||
|
return this.global.change.bind(this.global)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内置的格式化器
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典格式化器
|
||||||
|
* 根据输入data的值,返回后续参数匹配的结果
|
||||||
|
* dict(data,<value1>,<result1>,<value2>,<result1>,<value3>,<result1>,...)
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* dict(1,1,"one",2,"two",3,"three",4,"four") == "one"
|
||||||
|
* dict(2,1,"one",2,"two",3,"three",4,"four") == "two"
|
||||||
|
* dict(3,1,"one",2,"two",3,"three",4,"four") == "three"
|
||||||
|
* dict(4,1,"one",2,"two",3,"three",4,"four") == "four"
|
||||||
|
* // 无匹配时返回原始值
|
||||||
|
* dict(5,1,"one",2,"two",3,"three",4,"four") == 5
|
||||||
|
* // 无匹配时并且后续参数个数是奇数,则返回最后一个参数
|
||||||
|
* dict(5,1,"one",2,"two",3,"three",4,"four","more") == "more"
|
||||||
|
*
|
||||||
|
* 在翻译中使用
|
||||||
|
* I have { value | dict(1,"one",2,"two",3,"three",4,"four")} apples
|
||||||
|
*
|
||||||
|
* @param {*} value
|
||||||
|
* @param {...any} args
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function dict(value,...args){
|
||||||
|
for(let i=0;i<args.length;i+=2){
|
||||||
|
if(args[i]===value){
|
||||||
|
return args[i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(args.length >0 && (args.length % 2!==0)) return args[args.length-1]
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
var formatters$1 = {
|
||||||
|
"*":{
|
||||||
|
$types:{
|
||||||
|
Date:(value)=>value.toLocaleString()
|
||||||
|
},
|
||||||
|
time:(value)=> value.toLocaleTimeString(),
|
||||||
|
shorttime:(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()}秒`
|
||||||
|
},
|
||||||
|
shortime:(value)=> value.toLocaleTimeString(),
|
||||||
|
time:(value)=>`${value.getHours()}点${value.getMinutes()}分${value.getSeconds()}秒`,
|
||||||
|
date: (value)=> `${value.getFullYear()}年${value.getMonth()+1}月${value.getDate()}日`,
|
||||||
|
shortdate: (value)=> `${value.getFullYear()}-${value.getMonth()+1}-${value.getDate()}`,
|
||||||
|
currency:(value)=>`${value}元`,
|
||||||
|
},
|
||||||
|
en:{
|
||||||
|
currency:(value)=>{
|
||||||
|
return `$${value}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const EventEmitter = eventemitter;
|
||||||
|
const i18nScope = scope;
|
||||||
|
let formatters = formatters$1;
|
||||||
|
|
||||||
|
|
||||||
|
// 用来提取字符里面的插值变量参数 , 支持管道符 { 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("}")
|
||||||
|
}
|
||||||
|
const DataTypes$1 = ["String","Number","Boolean","Object","Array","Function","Error","Symbol","RegExp","Date","Null","Undefined","Set","Map","WeakSet","WeakMap"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定变量类型名称
|
||||||
|
* 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单进行对象合并
|
||||||
|
*
|
||||||
|
* options={
|
||||||
|
* array:0 , // 数组合并策略,0-替换,1-合并,2-去重合并
|
||||||
|
* object:0, // 对象合并策略,0-替换,1-合并,2-去重合并
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @param {*} toObj
|
||||||
|
* @param {*} formObj
|
||||||
|
* @returns 合并后的对象
|
||||||
|
*/
|
||||||
|
function deepMerge(toObj,formObj,options={}){
|
||||||
|
let results = Object.assign({},toObj);
|
||||||
|
Object.entries(formObj).forEach(([key,value])=>{
|
||||||
|
if(key in results){
|
||||||
|
if(typeof value === "object" && value !== null){
|
||||||
|
if(Array.isArray(value)){
|
||||||
|
if(options.array === 0){
|
||||||
|
results[key] = value;
|
||||||
|
}else if(options.array === 1){
|
||||||
|
results[key] = [...results[key],...value];
|
||||||
|
}else if(options.array === 2){
|
||||||
|
results[key] = [...new Set([...results[key],...value])];
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
results[key] = deepMerge(results[key],value,options);
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
results[key] = value;
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
results[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
通过正则表达式对原始文本内容进行解析匹配后得到的
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetScopeCache(scope,activeLanguage=null){
|
||||||
|
scope.$cache = {activeLanguage,typedFormatters:{},formatters:{}};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 取得指定数据类型的默认格式化器
|
||||||
|
*
|
||||||
|
* 可以为每一个数据类型指定一个默认的格式化器,当传入插值变量时,
|
||||||
|
* 会自动调用该格式化器来对值进行格式化转换
|
||||||
|
|
||||||
|
const formatters = {
|
||||||
|
"*":{
|
||||||
|
$types:{...} // 在所有语言下只作用于特定数据类型的格式化器
|
||||||
|
}, // 在所有语言下生效的格式化器
|
||||||
|
cn:{
|
||||||
|
$types:{
|
||||||
|
[数据类型]:(value)=>{...},
|
||||||
|
},
|
||||||
|
[格式化器名称]:(value)=>{...},
|
||||||
|
[格式化器名称]:(value)=>{...},
|
||||||
|
[格式化器名称]:(value)=>{...},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
* @param {*} scope
|
||||||
|
* @param {*} activeLanguage
|
||||||
|
* @param {*} dataType 数字类型
|
||||||
|
* @returns {Function} 格式化函数
|
||||||
|
*/
|
||||||
|
function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){
|
||||||
|
if(!scope.$cache) resetScopeCache(scope);
|
||||||
|
if(scope.$cache.activeLanguage === activeLanguage) {
|
||||||
|
if(dataType in scope.$cache.typedFormatters) return scope.$cache.typedFormatters[dataType]
|
||||||
|
}else {// 当语言切换时清空缓存
|
||||||
|
resetScopeCache(scope,activeLanguage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先在当前作用域中查找,再在全局查找
|
||||||
|
const targets = [scope.formatters,scope.global.formatters];
|
||||||
|
for(const target of targets){
|
||||||
|
if(!target) continue
|
||||||
|
// 优先在当前语言的$types中查找
|
||||||
|
if((activeLanguage in target) && isPlainObject(target[activeLanguage].$types)){
|
||||||
|
let formatters = target[activeLanguage].$types;
|
||||||
|
if(dataType in formatters && typeof(formatters[dataType])==="function"){
|
||||||
|
return scope.$cache.typedFormatters[dataType] = formatters[dataType]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 在所有语言的$types中查找
|
||||||
|
if(("*" in target) && isPlainObject(target["*"].$types)){
|
||||||
|
let formatters = target["*"].$types;
|
||||||
|
if(dataType in formatters && typeof(formatters[dataType])==="function"){
|
||||||
|
return scope.$cache.typedFormatters[dataType] = formatters[dataType]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定名称的格式化器函数
|
||||||
|
* @param {*} scope
|
||||||
|
* @param {*} activeLanguage
|
||||||
|
* @param {*} name 格式化器名称
|
||||||
|
* @returns {Function} 格式化函数
|
||||||
|
*/
|
||||||
|
function getFormatter(scope,activeLanguage,name){
|
||||||
|
// 缓存格式化器引用,避免重复检索
|
||||||
|
if(!scope.$cache) resetScopeCache(scope);
|
||||||
|
if(scope.$cache.activeLanguage === activeLanguage) {
|
||||||
|
if(name in scope.$cache.formatters) return scope.$cache.formatters[name]
|
||||||
|
}else {// 当语言切换时清空缓存
|
||||||
|
resetScopeCache(scope,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 scope.$cache.formatters[name] = formatters[name]
|
||||||
|
}
|
||||||
|
// 在所有语言的$types中查找
|
||||||
|
let formatters = target["*"] || {};
|
||||||
|
if((name in formatters) && typeof(formatters[name])==="function") return scope.$cache.formatters[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 {
|
||||||
|
// 格式化器无效或者没有定义时,查看当前值是否具有同名的原型方法,如果有则执行调用
|
||||||
|
// 比如padStart格式化器是String的原型方法,不需要配置就可以直接作为格式化器调用
|
||||||
|
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);
|
||||||
|
// 2. 查找每种数据类型默认格式化器,并添加到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:[
|
||||||
|
{name:"cn",title:"中文",default:true},
|
||||||
|
{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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function escape(str){
|
||||||
|
return str.replaceAll(/\\(?![trnbvf'"]{1})/g,"\\\\")
|
||||||
|
.replaceAll("\t","\\t")
|
||||||
|
.replaceAll("\n","\\n")
|
||||||
|
.replaceAll("\b","\\b")
|
||||||
|
.replaceAll("\r","\\r")
|
||||||
|
.replaceAll("\f","\\f")
|
||||||
|
.replaceAll("\'","\\'")
|
||||||
|
.replaceAll('\"','\\"')
|
||||||
|
.replaceAll('\v','\\v')
|
||||||
|
}
|
||||||
|
function unescape(str){
|
||||||
|
return str
|
||||||
|
.replaceAll("\\t","\t")
|
||||||
|
.replaceAll("\\n","\n")
|
||||||
|
.replaceAll("\\b","\b")
|
||||||
|
.replaceAll("\\r","\r")
|
||||||
|
.replaceAll("\\f","\f")
|
||||||
|
.replaceAll("\\'","\'")
|
||||||
|
.replaceAll('\\"','\"')
|
||||||
|
.replaceAll('\\v','\v')
|
||||||
|
.replaceAll(/\\\\(?![trnbvf'"]{1})/g,"\\")
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 翻译函数
|
||||||
|
*
|
||||||
|
* 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; // 复数值
|
||||||
|
if(!typeof(message)==="string") return message
|
||||||
|
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("$") && typeof(vars[name])==="number") 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
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 3. 取得翻译文本模板字符串
|
||||||
|
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
|
||||||
|
// JSON.stringify在进行转换时会将\t\n\r转换为\\t\\n\\r,这样在进行匹配时就出错
|
||||||
|
let msgId = isMessageId(content) ? content : scope.idMap[escape(content)];
|
||||||
|
content = scope.messages[msgId] || content;
|
||||||
|
content = Array.isArray(content) ? content.map(v=>unescape(v)) : unescape(content);
|
||||||
|
}
|
||||||
|
// 2. 处理复数
|
||||||
|
// 经过上面的处理,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 extends EventEmitter{
|
||||||
|
constructor(settings={}){
|
||||||
|
super();
|
||||||
|
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}
|
||||||
|
// 全局格式化器
|
||||||
|
get formatters(){ return formatters }
|
||||||
|
/**
|
||||||
|
* 切换语言
|
||||||
|
*/
|
||||||
|
async change(value){
|
||||||
|
value=value.trim();
|
||||||
|
if(this.languages.findIndex(lang=>lang.name === value)!==-1){
|
||||||
|
// 通知所有作用域刷新到对应的语言包
|
||||||
|
await this._refreshScopes(value);
|
||||||
|
this._settings.activeLanguage = value;
|
||||||
|
/// 触发语言切换事件
|
||||||
|
await this.emit(value);
|
||||||
|
}else {
|
||||||
|
throw new Error("Not supported language:"+value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 当切换语言时调用此方法来加载更新语言包
|
||||||
|
* @param {*} newLanguage
|
||||||
|
*/
|
||||||
|
async _refreshScopes(newLanguage){
|
||||||
|
// 并发执行所有作用域语言包的加载
|
||||||
|
try{
|
||||||
|
const scopeRefreshers = this._scopes.map(scope=>{
|
||||||
|
return scope.refresh(newLanguage)
|
||||||
|
});
|
||||||
|
if(Promise.allSettled){
|
||||||
|
await Promise.allSettled(scopeRefreshers);
|
||||||
|
}else {
|
||||||
|
await Promise.all(scopeRefreshers);
|
||||||
|
}
|
||||||
|
}catch(e){
|
||||||
|
console.warn("Error while refreshing i18n scopes:",e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* 注册一个新的作用域
|
||||||
|
*
|
||||||
|
* 每一个库均对应一个作用域,每个作用域可以有多个语言包,且对应一个翻译函数
|
||||||
|
* 除了默认语言外,其他语言采用动态加载的方式
|
||||||
|
*
|
||||||
|
* @param {*} scope
|
||||||
|
*/
|
||||||
|
async register(scope){
|
||||||
|
if(!(scope instanceof i18nScope)){
|
||||||
|
throw new TypeError("Scope must be an instance of I18nScope")
|
||||||
|
}
|
||||||
|
this._scopes.push(scope);
|
||||||
|
await scope.refresh(this.activeLanguage);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 注册全局格式化器
|
||||||
|
* 格式化器是一个简单的同步函数value=>{...},用来对输入进行格式化后返回结果
|
||||||
|
*
|
||||||
|
* registerFormatters(name,value=>{...}) // 适用于所有语言
|
||||||
|
* registerFormatters(name,value=>{...},{langauge:"cn"}) // 适用于cn语言
|
||||||
|
* registerFormatters(name,value=>{...},{langauge:"en"}) // 适用于en语言
|
||||||
|
|
||||||
|
* @param {*} formatters
|
||||||
|
*/
|
||||||
|
registerFormatter(name,formatter,{language="*"}={}){
|
||||||
|
if(!typeof(formatter)==="function" || typeof(name)!=="string"){
|
||||||
|
throw new TypeError("Formatter must be a function")
|
||||||
|
}
|
||||||
|
if(DataTypes$1.includes(name)){
|
||||||
|
this.formatters[language].$types[name] = formatter;
|
||||||
|
}else {
|
||||||
|
this.formatters[language][name] = formatter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var runtime ={
|
||||||
|
getInterpolatedVars,
|
||||||
|
replaceInterpolatedVars,
|
||||||
|
I18nManager,
|
||||||
|
translate,
|
||||||
|
languages,
|
||||||
|
i18nScope,
|
||||||
|
defaultLanguageSettings,
|
||||||
|
getDataTypeName,
|
||||||
|
isNumber,
|
||||||
|
isPlainObject
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = runtime;
|
922
packages/runtime/dist/runtime.mjs
vendored
Normal file
922
packages/runtime/dist/runtime.mjs
vendored
Normal file
@ -0,0 +1,922 @@
|
|||||||
|
/**
|
||||||
|
*
|
||||||
|
* 简单的事件触发器
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
var eventemitter = class EventEmitter{
|
||||||
|
constructor(){
|
||||||
|
this._callbacks = [];
|
||||||
|
}
|
||||||
|
on(callback){
|
||||||
|
if(this._callbacks.includes(callback)) return
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offAll(){
|
||||||
|
this._callbacks = [];
|
||||||
|
}
|
||||||
|
async emit(...args){
|
||||||
|
if(Promise.allSettled){
|
||||||
|
await Promise.allSettled(this._callbacks.map(cb=>cb(...args)));
|
||||||
|
}else {
|
||||||
|
await Promise.all(this._callbacks.map(cb=>cb(...args)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var scope = class i18nScope {
|
||||||
|
constructor(options={},callback){
|
||||||
|
// 每个作用域都有一个唯一的id
|
||||||
|
this._id = options.id || (new Date().getTime().toString()+parseInt(Math.random()*1000));
|
||||||
|
this._languages = options.languages; // 当前作用域的语言列表
|
||||||
|
this._defaultLanguage = options.defaultLanguage || "cn"; // 默认语言名称
|
||||||
|
this._activeLanguage = options.activeLanguage; // 当前语言名称
|
||||||
|
this._default = options.default; // 默认语言包
|
||||||
|
this._messages = options.messages; // 当前语言包
|
||||||
|
this._idMap = options.idMap; // 消息id映射列表
|
||||||
|
this._formatters = options.formatters; // 当前作用域的格式化函数列表
|
||||||
|
this._loaders = options.loaders; // 异步加载语言文件的函数列表
|
||||||
|
this._global = null; // 引用全局VoerkaI18n配置,注册后自动引用
|
||||||
|
// 主要用来缓存格式化器的引用,当使用格式化器时可以直接引用,避免检索
|
||||||
|
this.$cache={
|
||||||
|
activeLanguage : null,
|
||||||
|
typedFormatters: {},
|
||||||
|
formatters : {},
|
||||||
|
};
|
||||||
|
// 如果不存在全局VoerkaI18n实例,说明当前Scope是唯一或第一个加载的作用域,
|
||||||
|
// 则使用当前作用域来初始化全局VoerkaI18n实例
|
||||||
|
if(!globalThis.VoerkaI18n){
|
||||||
|
const { I18nManager } = runtime;
|
||||||
|
globalThis.VoerkaI18n = new I18nManager({
|
||||||
|
defaultLanguage: this.defaultLanguage,
|
||||||
|
activeLanguage : this.activeLanguage,
|
||||||
|
languages: options.languages,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.global = globalThis.VoerkaI18n;
|
||||||
|
// 正在加载语言包标识
|
||||||
|
this._loading=false;
|
||||||
|
// 在全局注册作用域
|
||||||
|
this.register(callback);
|
||||||
|
}
|
||||||
|
// 作用域
|
||||||
|
get id(){return this._id}
|
||||||
|
// 默认语言名称
|
||||||
|
get defaultLanguage(){return this._defaultLanguage}
|
||||||
|
// 默认语言名称
|
||||||
|
get activeLanguage(){return this._activeLanguage}
|
||||||
|
// 默认语言包
|
||||||
|
get default(){return this._default}
|
||||||
|
// 当前语言包
|
||||||
|
get messages(){return this._messages}
|
||||||
|
// 消息id映射列表
|
||||||
|
get idMap(){return this._idMap}
|
||||||
|
// 当前作用域的格式化函数列表
|
||||||
|
get formatters(){return this._formatters}
|
||||||
|
// 异步加载语言文件的函数列表
|
||||||
|
get loaders(){return this._loaders}
|
||||||
|
// 引用全局VoerkaI18n配置,注册后自动引用
|
||||||
|
get global(){return this._global}
|
||||||
|
set global(value){this._global = value;}
|
||||||
|
/**
|
||||||
|
* 在全局注册作用域
|
||||||
|
* @param {*} callback 当注册
|
||||||
|
*/
|
||||||
|
register(callback){
|
||||||
|
if(!typeof(callback)==="function") callback = ()=>{};
|
||||||
|
this.global.register(this).then(callback).catch(callback);
|
||||||
|
}
|
||||||
|
registerFormatter(name,formatter,{language="*"}={}){
|
||||||
|
if(!typeof(formatter)==="function" || typeof(name)!=="string"){
|
||||||
|
throw new TypeError("Formatter must be a function")
|
||||||
|
}
|
||||||
|
if(DataTypes.includes(name)){
|
||||||
|
this.formatters[language].$types[name] = formatter;
|
||||||
|
}else {
|
||||||
|
this.formatters[language][name] = formatter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 回退到默认语言
|
||||||
|
*/
|
||||||
|
_fallback(){
|
||||||
|
this._messages = this._default;
|
||||||
|
this._activeLanguage = this.defaultLanguage;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 刷新当前语言包
|
||||||
|
* @param {*} newLanguage
|
||||||
|
*/
|
||||||
|
async refresh(newLanguage){
|
||||||
|
this._loading = Promise.resolve();
|
||||||
|
if(!newLanguage) newLanguage = this.activeLanguage;
|
||||||
|
// 默认语言,默认语言采用静态加载方式,只需要简单的替换即可
|
||||||
|
if(newLanguage === this.defaultLanguage){
|
||||||
|
this._messages = this._default;
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 非默认语言需要异步加载语言包文件,加载器是一个异步函数
|
||||||
|
// 如果没有加载器,则无法加载语言包,因此回退到默认语言
|
||||||
|
const loader = this.loaders[newLanguage];
|
||||||
|
if(typeof(loader) === "function"){
|
||||||
|
try{
|
||||||
|
this._messages = (await loader()).default;
|
||||||
|
this._activeLanguage = newLanguage;
|
||||||
|
}catch(e){
|
||||||
|
console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`);
|
||||||
|
this._fallback();
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
this._fallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 以下方法引用全局VoerkaI18n实例的方法
|
||||||
|
get on(){return this.global.on.bind(this.global)}
|
||||||
|
get off(){return this.global.off.bind(this.global)}
|
||||||
|
get offAll(){return this.global.offAll.bind(this.global)}
|
||||||
|
get change(){
|
||||||
|
return this.global.change.bind(this.global)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内置的格式化器
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典格式化器
|
||||||
|
* 根据输入data的值,返回后续参数匹配的结果
|
||||||
|
* dict(data,<value1>,<result1>,<value2>,<result1>,<value3>,<result1>,...)
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* dict(1,1,"one",2,"two",3,"three",4,"four") == "one"
|
||||||
|
* dict(2,1,"one",2,"two",3,"three",4,"four") == "two"
|
||||||
|
* dict(3,1,"one",2,"two",3,"three",4,"four") == "three"
|
||||||
|
* dict(4,1,"one",2,"two",3,"three",4,"four") == "four"
|
||||||
|
* // 无匹配时返回原始值
|
||||||
|
* dict(5,1,"one",2,"two",3,"three",4,"four") == 5
|
||||||
|
* // 无匹配时并且后续参数个数是奇数,则返回最后一个参数
|
||||||
|
* dict(5,1,"one",2,"two",3,"three",4,"four","more") == "more"
|
||||||
|
*
|
||||||
|
* 在翻译中使用
|
||||||
|
* I have { value | dict(1,"one",2,"two",3,"three",4,"four")} apples
|
||||||
|
*
|
||||||
|
* @param {*} value
|
||||||
|
* @param {...any} args
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function dict(value,...args){
|
||||||
|
for(let i=0;i<args.length;i+=2){
|
||||||
|
if(args[i]===value){
|
||||||
|
return args[i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(args.length >0 && (args.length % 2!==0)) return args[args.length-1]
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
var formatters$1 = {
|
||||||
|
"*":{
|
||||||
|
$types:{
|
||||||
|
Date:(value)=>value.toLocaleString()
|
||||||
|
},
|
||||||
|
time:(value)=> value.toLocaleTimeString(),
|
||||||
|
shorttime:(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()}秒`
|
||||||
|
},
|
||||||
|
shortime:(value)=> value.toLocaleTimeString(),
|
||||||
|
time:(value)=>`${value.getHours()}点${value.getMinutes()}分${value.getSeconds()}秒`,
|
||||||
|
date: (value)=> `${value.getFullYear()}年${value.getMonth()+1}月${value.getDate()}日`,
|
||||||
|
shortdate: (value)=> `${value.getFullYear()}-${value.getMonth()+1}-${value.getDate()}`,
|
||||||
|
currency:(value)=>`${value}元`,
|
||||||
|
},
|
||||||
|
en:{
|
||||||
|
currency:(value)=>{
|
||||||
|
return `$${value}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const EventEmitter = eventemitter;
|
||||||
|
const i18nScope = scope;
|
||||||
|
let formatters = formatters$1;
|
||||||
|
|
||||||
|
|
||||||
|
// 用来提取字符里面的插值变量参数 , 支持管道符 { 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("}")
|
||||||
|
}
|
||||||
|
const DataTypes$1 = ["String","Number","Boolean","Object","Array","Function","Error","Symbol","RegExp","Date","Null","Undefined","Set","Map","WeakSet","WeakMap"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定变量类型名称
|
||||||
|
* 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单进行对象合并
|
||||||
|
*
|
||||||
|
* options={
|
||||||
|
* array:0 , // 数组合并策略,0-替换,1-合并,2-去重合并
|
||||||
|
* object:0, // 对象合并策略,0-替换,1-合并,2-去重合并
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @param {*} toObj
|
||||||
|
* @param {*} formObj
|
||||||
|
* @returns 合并后的对象
|
||||||
|
*/
|
||||||
|
function deepMerge(toObj,formObj,options={}){
|
||||||
|
let results = Object.assign({},toObj);
|
||||||
|
Object.entries(formObj).forEach(([key,value])=>{
|
||||||
|
if(key in results){
|
||||||
|
if(typeof value === "object" && value !== null){
|
||||||
|
if(Array.isArray(value)){
|
||||||
|
if(options.array === 0){
|
||||||
|
results[key] = value;
|
||||||
|
}else if(options.array === 1){
|
||||||
|
results[key] = [...results[key],...value];
|
||||||
|
}else if(options.array === 2){
|
||||||
|
results[key] = [...new Set([...results[key],...value])];
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
results[key] = deepMerge(results[key],value,options);
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
results[key] = value;
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
results[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
通过正则表达式对原始文本内容进行解析匹配后得到的
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetScopeCache(scope,activeLanguage=null){
|
||||||
|
scope.$cache = {activeLanguage,typedFormatters:{},formatters:{}};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 取得指定数据类型的默认格式化器
|
||||||
|
*
|
||||||
|
* 可以为每一个数据类型指定一个默认的格式化器,当传入插值变量时,
|
||||||
|
* 会自动调用该格式化器来对值进行格式化转换
|
||||||
|
|
||||||
|
const formatters = {
|
||||||
|
"*":{
|
||||||
|
$types:{...} // 在所有语言下只作用于特定数据类型的格式化器
|
||||||
|
}, // 在所有语言下生效的格式化器
|
||||||
|
cn:{
|
||||||
|
$types:{
|
||||||
|
[数据类型]:(value)=>{...},
|
||||||
|
},
|
||||||
|
[格式化器名称]:(value)=>{...},
|
||||||
|
[格式化器名称]:(value)=>{...},
|
||||||
|
[格式化器名称]:(value)=>{...},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
* @param {*} scope
|
||||||
|
* @param {*} activeLanguage
|
||||||
|
* @param {*} dataType 数字类型
|
||||||
|
* @returns {Function} 格式化函数
|
||||||
|
*/
|
||||||
|
function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){
|
||||||
|
if(!scope.$cache) resetScopeCache(scope);
|
||||||
|
if(scope.$cache.activeLanguage === activeLanguage) {
|
||||||
|
if(dataType in scope.$cache.typedFormatters) return scope.$cache.typedFormatters[dataType]
|
||||||
|
}else {// 当语言切换时清空缓存
|
||||||
|
resetScopeCache(scope,activeLanguage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先在当前作用域中查找,再在全局查找
|
||||||
|
const targets = [scope.formatters,scope.global.formatters];
|
||||||
|
for(const target of targets){
|
||||||
|
if(!target) continue
|
||||||
|
// 优先在当前语言的$types中查找
|
||||||
|
if((activeLanguage in target) && isPlainObject(target[activeLanguage].$types)){
|
||||||
|
let formatters = target[activeLanguage].$types;
|
||||||
|
if(dataType in formatters && typeof(formatters[dataType])==="function"){
|
||||||
|
return scope.$cache.typedFormatters[dataType] = formatters[dataType]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 在所有语言的$types中查找
|
||||||
|
if(("*" in target) && isPlainObject(target["*"].$types)){
|
||||||
|
let formatters = target["*"].$types;
|
||||||
|
if(dataType in formatters && typeof(formatters[dataType])==="function"){
|
||||||
|
return scope.$cache.typedFormatters[dataType] = formatters[dataType]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定名称的格式化器函数
|
||||||
|
* @param {*} scope
|
||||||
|
* @param {*} activeLanguage
|
||||||
|
* @param {*} name 格式化器名称
|
||||||
|
* @returns {Function} 格式化函数
|
||||||
|
*/
|
||||||
|
function getFormatter(scope,activeLanguage,name){
|
||||||
|
// 缓存格式化器引用,避免重复检索
|
||||||
|
if(!scope.$cache) resetScopeCache(scope);
|
||||||
|
if(scope.$cache.activeLanguage === activeLanguage) {
|
||||||
|
if(name in scope.$cache.formatters) return scope.$cache.formatters[name]
|
||||||
|
}else {// 当语言切换时清空缓存
|
||||||
|
resetScopeCache(scope,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 scope.$cache.formatters[name] = formatters[name]
|
||||||
|
}
|
||||||
|
// 在所有语言的$types中查找
|
||||||
|
let formatters = target["*"] || {};
|
||||||
|
if((name in formatters) && typeof(formatters[name])==="function") return scope.$cache.formatters[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 {
|
||||||
|
// 格式化器无效或者没有定义时,查看当前值是否具有同名的原型方法,如果有则执行调用
|
||||||
|
// 比如padStart格式化器是String的原型方法,不需要配置就可以直接作为格式化器调用
|
||||||
|
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);
|
||||||
|
// 2. 查找每种数据类型默认格式化器,并添加到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:[
|
||||||
|
{name:"cn",title:"中文",default:true},
|
||||||
|
{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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function escape(str){
|
||||||
|
return str.replaceAll(/\\(?![trnbvf'"]{1})/g,"\\\\")
|
||||||
|
.replaceAll("\t","\\t")
|
||||||
|
.replaceAll("\n","\\n")
|
||||||
|
.replaceAll("\b","\\b")
|
||||||
|
.replaceAll("\r","\\r")
|
||||||
|
.replaceAll("\f","\\f")
|
||||||
|
.replaceAll("\'","\\'")
|
||||||
|
.replaceAll('\"','\\"')
|
||||||
|
.replaceAll('\v','\\v')
|
||||||
|
}
|
||||||
|
function unescape(str){
|
||||||
|
return str
|
||||||
|
.replaceAll("\\t","\t")
|
||||||
|
.replaceAll("\\n","\n")
|
||||||
|
.replaceAll("\\b","\b")
|
||||||
|
.replaceAll("\\r","\r")
|
||||||
|
.replaceAll("\\f","\f")
|
||||||
|
.replaceAll("\\'","\'")
|
||||||
|
.replaceAll('\\"','\"')
|
||||||
|
.replaceAll('\\v','\v')
|
||||||
|
.replaceAll(/\\\\(?![trnbvf'"]{1})/g,"\\")
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 翻译函数
|
||||||
|
*
|
||||||
|
* 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; // 复数值
|
||||||
|
if(!typeof(message)==="string") return message
|
||||||
|
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("$") && typeof(vars[name])==="number") 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
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 3. 取得翻译文本模板字符串
|
||||||
|
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
|
||||||
|
// JSON.stringify在进行转换时会将\t\n\r转换为\\t\\n\\r,这样在进行匹配时就出错
|
||||||
|
let msgId = isMessageId(content) ? content : scope.idMap[escape(content)];
|
||||||
|
content = scope.messages[msgId] || content;
|
||||||
|
content = Array.isArray(content) ? content.map(v=>unescape(v)) : unescape(content);
|
||||||
|
}
|
||||||
|
// 2. 处理复数
|
||||||
|
// 经过上面的处理,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 extends EventEmitter{
|
||||||
|
constructor(settings={}){
|
||||||
|
super();
|
||||||
|
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}
|
||||||
|
// 全局格式化器
|
||||||
|
get formatters(){ return formatters }
|
||||||
|
/**
|
||||||
|
* 切换语言
|
||||||
|
*/
|
||||||
|
async change(value){
|
||||||
|
value=value.trim();
|
||||||
|
if(this.languages.findIndex(lang=>lang.name === value)!==-1){
|
||||||
|
// 通知所有作用域刷新到对应的语言包
|
||||||
|
await this._refreshScopes(value);
|
||||||
|
this._settings.activeLanguage = value;
|
||||||
|
/// 触发语言切换事件
|
||||||
|
await this.emit(value);
|
||||||
|
}else {
|
||||||
|
throw new Error("Not supported language:"+value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 当切换语言时调用此方法来加载更新语言包
|
||||||
|
* @param {*} newLanguage
|
||||||
|
*/
|
||||||
|
async _refreshScopes(newLanguage){
|
||||||
|
// 并发执行所有作用域语言包的加载
|
||||||
|
try{
|
||||||
|
const scopeRefreshers = this._scopes.map(scope=>{
|
||||||
|
return scope.refresh(newLanguage)
|
||||||
|
});
|
||||||
|
if(Promise.allSettled){
|
||||||
|
await Promise.allSettled(scopeRefreshers);
|
||||||
|
}else {
|
||||||
|
await Promise.all(scopeRefreshers);
|
||||||
|
}
|
||||||
|
}catch(e){
|
||||||
|
console.warn("Error while refreshing i18n scopes:",e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* 注册一个新的作用域
|
||||||
|
*
|
||||||
|
* 每一个库均对应一个作用域,每个作用域可以有多个语言包,且对应一个翻译函数
|
||||||
|
* 除了默认语言外,其他语言采用动态加载的方式
|
||||||
|
*
|
||||||
|
* @param {*} scope
|
||||||
|
*/
|
||||||
|
async register(scope){
|
||||||
|
if(!(scope instanceof i18nScope)){
|
||||||
|
throw new TypeError("Scope must be an instance of I18nScope")
|
||||||
|
}
|
||||||
|
this._scopes.push(scope);
|
||||||
|
await scope.refresh(this.activeLanguage);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 注册全局格式化器
|
||||||
|
* 格式化器是一个简单的同步函数value=>{...},用来对输入进行格式化后返回结果
|
||||||
|
*
|
||||||
|
* registerFormatters(name,value=>{...}) // 适用于所有语言
|
||||||
|
* registerFormatters(name,value=>{...},{langauge:"cn"}) // 适用于cn语言
|
||||||
|
* registerFormatters(name,value=>{...},{langauge:"en"}) // 适用于en语言
|
||||||
|
|
||||||
|
* @param {*} formatters
|
||||||
|
*/
|
||||||
|
registerFormatter(name,formatter,{language="*"}={}){
|
||||||
|
if(!typeof(formatter)==="function" || typeof(name)!=="string"){
|
||||||
|
throw new TypeError("Formatter must be a function")
|
||||||
|
}
|
||||||
|
if(DataTypes$1.includes(name)){
|
||||||
|
this.formatters[language].$types[name] = formatter;
|
||||||
|
}else {
|
||||||
|
this.formatters[language][name] = formatter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var runtime ={
|
||||||
|
getInterpolatedVars,
|
||||||
|
replaceInterpolatedVars,
|
||||||
|
I18nManager,
|
||||||
|
translate,
|
||||||
|
languages,
|
||||||
|
i18nScope,
|
||||||
|
defaultLanguageSettings,
|
||||||
|
getDataTypeName,
|
||||||
|
isNumber,
|
||||||
|
isPlainObject
|
||||||
|
};
|
||||||
|
|
||||||
|
export { runtime as default };
|
@ -1,4 +1,3 @@
|
|||||||
const deepMerge = require("deepmerge")
|
|
||||||
const EventEmitter = require("./eventemitter")
|
const EventEmitter = require("./eventemitter")
|
||||||
const i18nScope = require("./scope.js")
|
const i18nScope = require("./scope.js")
|
||||||
let formatters = require("./formatters")
|
let formatters = require("./formatters")
|
||||||
@ -69,6 +68,46 @@ function isNumber(value){
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单进行对象合并
|
||||||
|
*
|
||||||
|
* options={
|
||||||
|
* array:0 , // 数组合并策略,0-替换,1-合并,2-去重合并
|
||||||
|
* object:0, // 对象合并策略,0-替换,1-合并,2-去重合并
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @param {*} toObj
|
||||||
|
* @param {*} formObj
|
||||||
|
* @returns 合并后的对象
|
||||||
|
*/
|
||||||
|
function deepMerge(toObj,formObj,options={}){
|
||||||
|
let results = Object.assign({},toObj)
|
||||||
|
Object.entries(formObj).forEach(([key,value])=>{
|
||||||
|
if(key in results){
|
||||||
|
if(typeof value === "object" && value !== null){
|
||||||
|
if(Array.isArray(value)){
|
||||||
|
if(options.array === 0){
|
||||||
|
results[key] = value
|
||||||
|
}else if(options.array === 1){
|
||||||
|
results[key] = [...results[key],...value]
|
||||||
|
}else if(options.array === 2){
|
||||||
|
results[key] = [...new Set([...results[key],...value])]
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
results[key] = deepMerge(results[key],value,options)
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
results[key] = value
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
results[key] = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
通过正则表达式对原始文本内容进行解析匹配后得到的
|
通过正则表达式对原始文本内容进行解析匹配后得到的
|
||||||
formatters="| aaa(1,1) | bbb "
|
formatters="| aaa(1,1) | bbb "
|
||||||
@ -436,10 +475,10 @@ function replaceInterpolatedVars(template,...args) {
|
|||||||
const defaultLanguageSettings = {
|
const defaultLanguageSettings = {
|
||||||
defaultLanguage: "cn",
|
defaultLanguage: "cn",
|
||||||
activeLanguage: "cn",
|
activeLanguage: "cn",
|
||||||
languages:{
|
languages:[
|
||||||
cn:{name:"cn",title:"中文",default:true},
|
{name:"cn",title:"中文",default:true},
|
||||||
en:{name:"en",title:"英文"},
|
{name:"en",title:"英文"}
|
||||||
},
|
],
|
||||||
formatters
|
formatters
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -595,8 +634,6 @@ function translate(message) {
|
|||||||
*
|
*
|
||||||
* */
|
* */
|
||||||
class I18nManager extends EventEmitter{
|
class I18nManager extends EventEmitter{
|
||||||
static instance = null; // 单例引用
|
|
||||||
callbacks = [] // 当切换语言时的回调事件
|
|
||||||
constructor(settings={}){
|
constructor(settings={}){
|
||||||
super()
|
super()
|
||||||
if(I18nManager.instance!=null){
|
if(I18nManager.instance!=null){
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@voerkai18n/runtime",
|
"name": "@voerkai18n/runtime",
|
||||||
"version": "1.0.3",
|
"version": "1.0.7",
|
||||||
"description": "Voerkai18n Runtime",
|
"description": "Voerkai18n Runtime",
|
||||||
"main": "./dist/index.cjs",
|
"main": "./dist/index.cjs",
|
||||||
"module": "dist/index.esm.js",
|
"module": "dist/index.esm.js",
|
||||||
@ -12,18 +12,26 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"build": "rollup -c",
|
"build": "rollup -c",
|
||||||
"publish": "npm run build && npm publish -access public"
|
"release": "npm version patch && npm run build && npm publish -access public"
|
||||||
},
|
},
|
||||||
"exports":{
|
"exports": {
|
||||||
"import":"./dist/index.esm.js" ,
|
"import": "./dist/index.esm.js",
|
||||||
"require":"./dist/index.cjs"
|
"require": "./dist/index.cjs"
|
||||||
},
|
},
|
||||||
"author": "wxzhang",
|
"author": "wxzhang",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/cli": "^7.17.6",
|
||||||
|
"@babel/core": "^7.17.5",
|
||||||
|
"@babel/plugin-transform-runtime": "^7.17.0",
|
||||||
|
"@babel/preset-env": "^7.16.11",
|
||||||
|
"@babel/runtime": "^7.17.8",
|
||||||
|
"@rollup/plugin-babel": "^5.3.1",
|
||||||
|
"@rollup/plugin-commonjs": "^21.0.2",
|
||||||
"@rollup/plugin-node-resolve": "^13.1.3",
|
"@rollup/plugin-node-resolve": "^13.1.3",
|
||||||
"deepmerge": "^4.2.2",
|
"deepmerge": "^4.2.2",
|
||||||
"rollup": "^2.69.0",
|
"rollup": "^2.69.0",
|
||||||
|
"rollup-plugin-clear": "^2.0.7",
|
||||||
"rollup-plugin-terser": "^7.0.2"
|
"rollup-plugin-terser": "^7.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
# @voerkai18n/runtime
|
# @voerkai18n/runtime
|
||||||
|
|
||||||
`voerkai18n`运行时依赖
|
`voerkai18n`运行时核心代码
|
@ -3,7 +3,7 @@ import clear from 'rollup-plugin-clear'
|
|||||||
import commonjs from '@rollup/plugin-commonjs';
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
import resolve from "@rollup/plugin-node-resolve";
|
import resolve from "@rollup/plugin-node-resolve";
|
||||||
import { terser } from "rollup-plugin-terser";
|
import { terser } from "rollup-plugin-terser";
|
||||||
// import { babel } from '@rollup/plugin-babel';
|
import { babel } from '@rollup/plugin-babel';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
@ -11,20 +11,44 @@ export default [
|
|||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
file: 'dist/index.esm.js',
|
file: 'dist/index.esm.js',
|
||||||
format:"esm"
|
format:"esm",
|
||||||
|
sourcemap:true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
file: 'dist/index.cjs',
|
file: 'dist/index.cjs',
|
||||||
exports:"default",
|
exports:"default",
|
||||||
format:"cjs"
|
format:"cjs",
|
||||||
|
sourcemap:true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
resolve(),
|
resolve(),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
|
babel({
|
||||||
|
babelHelpers:"runtime",
|
||||||
|
exclude: 'node_modules/**'
|
||||||
|
}),
|
||||||
clear({targets:["dist"]}),
|
clear({targets:["dist"]}),
|
||||||
terser()
|
terser()
|
||||||
],
|
],
|
||||||
external:["@babel/runtime"]
|
external:["@babel/runtime"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: './index.js',
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
file: 'dist/runtime.cjs',
|
||||||
|
exports:"auto",
|
||||||
|
format:"cjs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: 'dist/runtime.mjs',
|
||||||
|
exports:"default",
|
||||||
|
format:"esm"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
plugins:[
|
||||||
|
commonjs(),
|
||||||
|
],
|
||||||
}
|
}
|
||||||
]
|
]
|
@ -16,8 +16,15 @@ export default {
|
|||||||
install: (app, opts={}) => {
|
install: (app, opts={}) => {
|
||||||
let options = Object.assign({
|
let options = Object.assign({
|
||||||
t:message=>message,
|
t:message=>message,
|
||||||
|
i18nScope:null,
|
||||||
}, opts)
|
}, opts)
|
||||||
|
|
||||||
|
let translate = options.t
|
||||||
|
if(typeof(translate)!=="function"){
|
||||||
|
console.warn("@voerkai18n/vue: t function is not provided, use default t function")
|
||||||
|
translate = message=>message
|
||||||
|
}
|
||||||
|
|
||||||
// 全局翻译函数
|
// 全局翻译函数
|
||||||
app.config.globalProperties.t = function(){
|
app.config.globalProperties.t = function(){
|
||||||
return options.t(...arguments)
|
return options.t(...arguments)
|
||||||
|
38
pnpm-lock.yaml
generated
38
pnpm-lock.yaml
generated
@ -90,9 +90,6 @@ importers:
|
|||||||
through2: 4.0.2
|
through2: 4.0.2
|
||||||
vinyl: 2.2.1
|
vinyl: 2.2.1
|
||||||
|
|
||||||
packages/cli/languages:
|
|
||||||
specifiers: {}
|
|
||||||
|
|
||||||
packages/formatters:
|
packages/formatters:
|
||||||
specifiers:
|
specifiers:
|
||||||
deepmerge: ^4.2.2
|
deepmerge: ^4.2.2
|
||||||
@ -115,14 +112,30 @@ importers:
|
|||||||
|
|
||||||
packages/runtime:
|
packages/runtime:
|
||||||
specifiers:
|
specifiers:
|
||||||
|
'@babel/cli': ^7.17.6
|
||||||
|
'@babel/core': ^7.17.5
|
||||||
|
'@babel/plugin-transform-runtime': ^7.17.0
|
||||||
|
'@babel/preset-env': ^7.16.11
|
||||||
|
'@babel/runtime': ^7.17.8
|
||||||
|
'@rollup/plugin-babel': ^5.3.1
|
||||||
|
'@rollup/plugin-commonjs': ^21.0.2
|
||||||
'@rollup/plugin-node-resolve': ^13.1.3
|
'@rollup/plugin-node-resolve': ^13.1.3
|
||||||
deepmerge: ^4.2.2
|
deepmerge: ^4.2.2
|
||||||
rollup: ^2.69.0
|
rollup: ^2.69.0
|
||||||
|
rollup-plugin-clear: ^2.0.7
|
||||||
rollup-plugin-terser: ^7.0.2
|
rollup-plugin-terser: ^7.0.2
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@babel/cli': 7.17.6_@babel+core@7.17.5
|
||||||
|
'@babel/core': 7.17.5
|
||||||
|
'@babel/plugin-transform-runtime': 7.17.0_@babel+core@7.17.5
|
||||||
|
'@babel/preset-env': 7.16.11_@babel+core@7.17.5
|
||||||
|
'@babel/runtime': 7.17.8
|
||||||
|
'@rollup/plugin-babel': 5.3.1_@babel+core@7.17.5+rollup@2.69.0
|
||||||
|
'@rollup/plugin-commonjs': 21.0.2_rollup@2.69.0
|
||||||
'@rollup/plugin-node-resolve': 13.1.3_rollup@2.69.0
|
'@rollup/plugin-node-resolve': 13.1.3_rollup@2.69.0
|
||||||
deepmerge: 4.2.2
|
deepmerge: 4.2.2
|
||||||
rollup: 2.69.0
|
rollup: 2.69.0
|
||||||
|
rollup-plugin-clear: 2.0.7
|
||||||
rollup-plugin-terser: 7.0.2_rollup@2.69.0
|
rollup-plugin-terser: 7.0.2_rollup@2.69.0
|
||||||
|
|
||||||
packages/vue:
|
packages/vue:
|
||||||
@ -162,7 +175,6 @@ packages:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@nicolo-ribaudo/chokidar-2': 2.1.8-no-fsevents.3
|
'@nicolo-ribaudo/chokidar-2': 2.1.8-no-fsevents.3
|
||||||
chokidar: 3.5.3
|
chokidar: 3.5.3
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@babel/code-frame/7.16.7:
|
/@babel/code-frame/7.16.7:
|
||||||
resolution: {integrity: sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==}
|
resolution: {integrity: sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==}
|
||||||
@ -1289,6 +1301,13 @@ packages:
|
|||||||
regenerator-runtime: 0.13.9
|
regenerator-runtime: 0.13.9
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@babel/runtime/7.17.8:
|
||||||
|
resolution: {integrity: sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime: 0.13.9
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@babel/template/7.16.7:
|
/@babel/template/7.16.7:
|
||||||
resolution: {integrity: sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==}
|
resolution: {integrity: sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@ -1548,7 +1567,6 @@ packages:
|
|||||||
/@nicolo-ribaudo/chokidar-2/2.1.8-no-fsevents.3:
|
/@nicolo-ribaudo/chokidar-2/2.1.8-no-fsevents.3:
|
||||||
resolution: {integrity: sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==}
|
resolution: {integrity: sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@rollup/plugin-babel/5.3.1_@babel+core@7.17.5+rollup@2.69.0:
|
/@rollup/plugin-babel/5.3.1_@babel+core@7.17.5+rollup@2.69.0:
|
||||||
@ -2187,7 +2205,6 @@ packages:
|
|||||||
/binary-extensions/2.2.0:
|
/binary-extensions/2.2.0:
|
||||||
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/bindings/1.5.0:
|
/bindings/1.5.0:
|
||||||
@ -2359,7 +2376,6 @@ packages:
|
|||||||
readdirp: 3.6.0
|
readdirp: 3.6.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.2
|
fsevents: 2.3.2
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/ci-info/3.3.0:
|
/ci-info/3.3.0:
|
||||||
@ -2492,7 +2508,6 @@ packages:
|
|||||||
/commander/4.1.1:
|
/commander/4.1.1:
|
||||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/commander/9.0.0:
|
/commander/9.0.0:
|
||||||
resolution: {integrity: sha512-JJfP2saEKbQqvW+FI93OYUB4ByV5cizMpFMiiJI8xDbBvQvSkIk0VvQdn1CZ8mqAO8Loq2h0gYTYtDFUZUeERw==}
|
resolution: {integrity: sha512-JJfP2saEKbQqvW+FI93OYUB4ByV5cizMpFMiiJI8xDbBvQvSkIk0VvQdn1CZ8mqAO8Loq2h0gYTYtDFUZUeERw==}
|
||||||
@ -3266,7 +3281,6 @@ packages:
|
|||||||
|
|
||||||
/fs-readdir-recursive/1.1.0:
|
/fs-readdir-recursive/1.1.0:
|
||||||
resolution: {integrity: sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==}
|
resolution: {integrity: sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/fs.realpath/1.0.0:
|
/fs.realpath/1.0.0:
|
||||||
resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=}
|
resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=}
|
||||||
@ -3336,7 +3350,6 @@ packages:
|
|||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
dependencies:
|
dependencies:
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/glob-stream/6.1.0:
|
/glob-stream/6.1.0:
|
||||||
@ -3632,7 +3645,6 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dependencies:
|
dependencies:
|
||||||
binary-extensions: 2.2.0
|
binary-extensions: 2.2.0
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/is-buffer/1.1.6:
|
/is-buffer/1.1.6:
|
||||||
@ -4555,7 +4567,6 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
pify: 4.0.1
|
pify: 4.0.1
|
||||||
semver: 5.7.1
|
semver: 5.7.1
|
||||||
dev: false
|
|
||||||
|
|
||||||
/make-dir/3.1.0:
|
/make-dir/3.1.0:
|
||||||
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
|
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
|
||||||
@ -4970,7 +4981,6 @@ packages:
|
|||||||
/pify/4.0.1:
|
/pify/4.0.1:
|
||||||
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
|
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/pinkie-promise/2.0.1:
|
/pinkie-promise/2.0.1:
|
||||||
resolution: {integrity: sha1-ITXW36ejWMBprJsXh3YogihFD/o=}
|
resolution: {integrity: sha1-ITXW36ejWMBprJsXh3YogihFD/o=}
|
||||||
@ -5114,7 +5124,6 @@ packages:
|
|||||||
engines: {node: '>=8.10.0'}
|
engines: {node: '>=8.10.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/rechoir/0.6.2:
|
/rechoir/0.6.2:
|
||||||
@ -5406,7 +5415,6 @@ packages:
|
|||||||
/slash/2.0.0:
|
/slash/2.0.0:
|
||||||
resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==}
|
resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/slash/3.0.0:
|
/slash/3.0.0:
|
||||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||||
|
67
readme.md
67
readme.md
@ -1,4 +1,7 @@
|
|||||||
|
|
||||||
|
# ** 测试阶段,有问题请issues **
|
||||||
|
|
||||||
|
[](https://gitee.com/zhangfisher/voerka-i18n/stargazers) [](https://gitee.com/zhangfisher/voerka-i18n/stargazers)
|
||||||
|
|
||||||
# 前言
|
# 前言
|
||||||
|
|
||||||
@ -26,7 +29,7 @@
|
|||||||
|
|
||||||
- 支持`nodejs`、浏览器(`vue`/`react`)前端环境。
|
- 支持`nodejs`、浏览器(`vue`/`react`)前端环境。
|
||||||
|
|
||||||
- 采用`工程工具链`与`运行时`分开设计,发布时只需要集成很小的运行时(12K)。
|
- 采用`工具链`与`运行时`分开设计,发布时只需要集成很小的运行时。
|
||||||
|
|
||||||
- 高度可扩展的`复数`、`货币`、`数字`等常用的多语言处理机制。
|
- 高度可扩展的`复数`、`货币`、`数字`等常用的多语言处理机制。
|
||||||
|
|
||||||
@ -42,9 +45,18 @@
|
|||||||
|
|
||||||
`VoerkaI18n`国际化框架是一个开源多包工程,主要由以下几个包组成:
|
`VoerkaI18n`国际化框架是一个开源多包工程,主要由以下几个包组成:
|
||||||
|
|
||||||
|
- **@voerkai18/cli**
|
||||||
|
|
||||||
|
包含文本提取/编译等命令行工具,一般应该安装到全局。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
npm install --g @voerkai18/cli
|
||||||
|
yarn global add @voerkai18/cli
|
||||||
|
pnpm add -g @voerkai18/cli
|
||||||
|
```
|
||||||
- **@voerkai18/runtime**
|
- **@voerkai18/runtime**
|
||||||
|
|
||||||
必须的运行时,安装到运行依赖`dependencies`中
|
**可选的**,运行时,`@voerkai18/cli`的依赖。大部分情况下不需要手动安装。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
npm install --save @voerkai18/runtime
|
npm install --save @voerkai18/runtime
|
||||||
@ -52,19 +64,9 @@
|
|||||||
pnpm add @voerkai18/runtime
|
pnpm add @voerkai18/runtime
|
||||||
```
|
```
|
||||||
|
|
||||||
- **@voerkai18/cli**
|
|
||||||
|
|
||||||
包含文本提取/编译等命令行工具,应该安装到开发依赖`devDependencies`中
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
npm install --save-dev @voerkai18/cli
|
|
||||||
yarn add -D @voerkai18/cli
|
|
||||||
pnpm add -D @voerkai18/cli
|
|
||||||
```
|
|
||||||
|
|
||||||
- **@voerkai18/formatters**
|
- **@voerkai18/formatters**
|
||||||
|
|
||||||
可选的,一些额外的格式化器,可以按需进行安装到`dependencies`中,用来扩展翻译时对插值变量的额外处理。
|
**可选的**,一些额外的格式化器,可以按需进行安装到`dependencies`中,用来扩展翻译时对插值变量的额外处理。
|
||||||
|
|
||||||
- **@voerkai18/babel**
|
- **@voerkai18/babel**
|
||||||
|
|
||||||
@ -91,7 +93,15 @@ console.log(t("中华人民共和国成立于{}",1949))
|
|||||||
|
|
||||||
`t`翻译函数是从`myapp/languages/index.js`文件导出的翻译函数,但是现在`myapp/languages`还不存在,后续会使用工具自动生成。`voerkai18n`后续会使用正则表达式对提取要翻译的文本。
|
`t`翻译函数是从`myapp/languages/index.js`文件导出的翻译函数,但是现在`myapp/languages`还不存在,后续会使用工具自动生成。`voerkai18n`后续会使用正则表达式对提取要翻译的文本。
|
||||||
|
|
||||||
## 第一步:初始化工程
|
## 第一步:安装命令行工具
|
||||||
|
|
||||||
|
```shell
|
||||||
|
> npm install -g @voerkai18n/cli
|
||||||
|
> yarn global add @voerkai18n/cli
|
||||||
|
>pnpm add -g @voerkai18/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第二步:初始化工程
|
||||||
|
|
||||||
在工程目录中运行`voerkai18n init`命令进行初始化。
|
在工程目录中运行`voerkai18n init`命令进行初始化。
|
||||||
|
|
||||||
@ -127,14 +137,12 @@ console.log(t("中华人民共和国成立于{}",1949))
|
|||||||
- 默认语言是`中文`(即在源代码中直接使用中文)
|
- 默认语言是`中文`(即在源代码中直接使用中文)
|
||||||
- 激活语言是`中文`
|
- 激活语言是`中文`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**注意:**
|
**注意:**
|
||||||
|
|
||||||
- `voerkai18n init`是可选的,`voerkai18n extract`也可以实现相同的功能。
|
- `voerkai18n init`是可选的,`voerkai18n extract`也可以实现相同的功能。
|
||||||
- 一般情况下,您可以手工修改`settings.json`,如定义名称空间。
|
- 一般情况下,您可以手工修改`settings.json`,如定义名称空间。
|
||||||
|
|
||||||
## 第二步:提取文本
|
## 第三步:提取文本
|
||||||
|
|
||||||
接下来我们使用`voerkai18n extract`命令来自动扫描工程源码文件中的需要的翻译的文本信息。
|
接下来我们使用`voerkai18n extract`命令来自动扫描工程源码文件中的需要的翻译的文本信息。
|
||||||
|
|
||||||
@ -175,7 +183,7 @@ myapp>voerkai18n extract -D -lngs cn en de jp -d cn -a cn
|
|||||||
- 激活语言是中文(即默认切换到中文)
|
- 激活语言是中文(即默认切换到中文)
|
||||||
- `-D`代表显示扫描调试信息
|
- `-D`代表显示扫描调试信息
|
||||||
|
|
||||||
## 第三步:翻译文本
|
## 第四步:翻译文本
|
||||||
|
|
||||||
接下来就可以分别对`language/translates`文件夹下的所有`JSON`文件进行翻译了。每个`JSON`文件大概如下:
|
接下来就可以分别对`language/translates`文件夹下的所有`JSON`文件进行翻译了。每个`JSON`文件大概如下:
|
||||||
|
|
||||||
@ -206,7 +214,7 @@ myapp>voerkai18n extract -D -lngs cn en de jp -d cn -a cn
|
|||||||
|
|
||||||
因此,反复执行`voerkai18n extract`命令是安全的,不会导致进行了一半的翻译内容丢失,可以放心执行。
|
因此,反复执行`voerkai18n extract`命令是安全的,不会导致进行了一半的翻译内容丢失,可以放心执行。
|
||||||
|
|
||||||
## 第四步:编译语言包
|
## 第五步:编译语言包
|
||||||
|
|
||||||
当我们完成`myapp/languages/translates`下的所有`JSON语言文件`的翻译后(如果配置了名称空间后,每一个名称空间会对应生成一个文件,详见后续`名称空间`介绍),接下来需要对翻译后的文件进行编译。
|
当我们完成`myapp/languages/translates`下的所有`JSON语言文件`的翻译后(如果配置了名称空间后,每一个名称空间会对应生成一个文件,详见后续`名称空间`介绍),接下来需要对翻译后的文件进行编译。
|
||||||
|
|
||||||
@ -220,6 +228,7 @@ myapp> voerkai18n compile
|
|||||||
|-- languages
|
|-- languages
|
||||||
|-- settings.json // 语言配置文件
|
|-- settings.json // 语言配置文件
|
||||||
|-- idMap.js // 文本信息id映射表
|
|-- idMap.js // 文本信息id映射表
|
||||||
|
|-- runtime.js // 运行时源码
|
||||||
|-- index.js // 包含该应用作用域下的翻译函数等
|
|-- index.js // 包含该应用作用域下的翻译函数等
|
||||||
|-- cn.js // 语言包
|
|-- cn.js // 语言包
|
||||||
|-- en.js
|
|-- en.js
|
||||||
@ -232,7 +241,7 @@ myapp> voerkai18n compile
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 第五步:导入翻译函数
|
## 第六步:导入翻译函数
|
||||||
|
|
||||||
第一步中我们在源文件中直接使用了`t`翻译函数包装要翻译的文本信息,该`t`翻译函数就是在编译环节自动生成并声明在`myapp/languages/index.js`中的。
|
第一步中我们在源文件中直接使用了`t`翻译函数包装要翻译的文本信息,该`t`翻译函数就是在编译环节自动生成并声明在`myapp/languages/index.js`中的。
|
||||||
|
|
||||||
@ -307,8 +316,6 @@ t("中华人民共和国成立于{birthday | year}年",{birthday:new Date()})
|
|||||||
|
|
||||||
- `voerkai18n`使用正则表达式来提取要翻译的内容,因此`t()`可以使用在任意地方。
|
- `voerkai18n`使用正则表达式来提取要翻译的内容,因此`t()`可以使用在任意地方。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 插值变量
|
## 插值变量
|
||||||
|
|
||||||
`voerkai18n`的`t`函数支持使用**插值变量**,用来传入一个可变内容。
|
`voerkai18n`的`t`函数支持使用**插值变量**,用来传入一个可变内容。
|
||||||
@ -969,6 +976,22 @@ const scope = new i18nScope({
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 运行时
|
||||||
|
|
||||||
|
运行`voerkai18n compile`时会在`languages`文件下生成运行时文件`runtime.js`,该文件被`languages/index.js`引入,里面是核心运行时`ES6`源代码(`@voerkai18n/runtime`代码),也就是在您的工程中是直接引入的运行时代码,因此就不需要额外`@voerkai18n/runtime`了。
|
||||||
|
|
||||||
|
每次运行`voerkai18n compile`时就会自动生成`runtime.js`,请及时升级`@voerkai18n/cli`工程以更新运行时代码。
|
||||||
|
|
||||||
|
**重点:默认情况下是不需要额外安装`@voerkai18n/runtime`的。**
|
||||||
|
|
||||||
|
由于`runtime.js`是`ES6`代码,在某些情况下,您需要兼容性更好的代码时,就需要进行`babel`转码。比如在普通的`nodejs`应用中。`@voerkai18n/runtime`也提供转码后的发布版本。
|
||||||
|
|
||||||
|
当运行`voerkai18n compile --no-inline-runtime`时就不会生成`runtime.js`,而是直接引用的`@voerkai18n/runtime`,而`@voerkai18n/runtime`发布了`commonjs`和`esm`两个经过`babel/rollup`转码后的版本。
|
||||||
|
|
||||||
|
- 当运行`voerkai18n compile`并启用了`--no-inline-runtime`时,在您在工程中就需要额外安装`@voerkai18n/runtime`到运行依赖中。
|
||||||
|
|
||||||
|
- 当运行`voerkai18n compile --no-inline-runtime`时,不需要安装`@voerkai18n/runtime`。但是,您的运行环境需要支持`ES6`或者自行转码。大部分`vue/react`等应用均能支持转码。
|
||||||
|
|
||||||
## babel插件
|
## babel插件
|
||||||
|
|
||||||
全局安装`@voerkai18n/babel`插件用来进行自动导入t函数和自动文本映射。
|
全局安装`@voerkai18n/babel`插件用来进行自动导入t函数和自动文本映射。
|
||||||
|
Loading…
x
Reference in New Issue
Block a user