update
This commit is contained in:
parent
ca11e8f288
commit
fbde8540c8
7
demo/apps/app/index.js
Normal file
7
demo/apps/app/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
t("a")
|
||||
t("b")
|
||||
t("c")
|
||||
t("d")
|
||||
t("e")
|
||||
|
1
demo/apps/app/package.json
Normal file
1
demo/apps/app/package.json
Normal file
@ -0,0 +1 @@
|
||||
{"type":"module"}
|
7
demo/apps/lib1/languages/idMap.js
Normal file
7
demo/apps/lib1/languages/idMap.js
Normal file
@ -0,0 +1,7 @@
|
||||
export default {
|
||||
"a":1,
|
||||
"b":2,
|
||||
"c{}{}":3,
|
||||
"d{a}{b}":4,
|
||||
"e":5
|
||||
}
|
7
demo/apps/lib2/languages/idMap.js
Normal file
7
demo/apps/lib2/languages/idMap.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
"a":1,
|
||||
"b":2,
|
||||
"c{}{}":3,
|
||||
"d{a}{b}":4,
|
||||
"e":5
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
|
||||
|
||||
const compile = require('@voerkai18n/tools/compile.command');
|
||||
const compile = require('@voerkai18n/cli/compile.command');
|
||||
const path = require("path")
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
const gulp = require('gulp');
|
||||
const extract = require('@voerkai18n/tools/extract.plugin');
|
||||
const extract = require('@voerkai18n/cli/extract.plugin');
|
||||
const path = require('path');
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@voerkai18n/runtime": "workspace:^1.0.0",
|
||||
"@voerkai18n/tools": "workspace:^1.0.0"
|
||||
"@voerkai18n/cli": "workspace:^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"deepmerge": "^4.2.2",
|
@ -1,4 +1,4 @@
|
||||
const { objectStringify } = require("../tools/stringify")
|
||||
const { objectStringify } = require("@voerkai18n/cli/stringify")
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
@ -4,14 +4,13 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build:runtime":"pnpm build --filter \"@voerkai18n/runtime\"",
|
||||
"test": "jest",
|
||||
"build:runtime": "pnpm build --filter \"@voerkai18n/runtime\"",
|
||||
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules node node_modules/jest/bin/jest.js ",
|
||||
"test:babelplugin": "jest babel",
|
||||
"test:extract": "jest extract",
|
||||
"test:translate": "jest translate",
|
||||
"demo:extract": "node ./packages/demo/extract.demo.js",
|
||||
"demo:compile": "node ./packages/demo/compile.demo.js",
|
||||
"demo:babel": "babel ./demodata/a/a1.js --plugins=./src/babel-plugin-voerkai18n.js"
|
||||
"demo:compile": "node ./packages/demo/compile.demo.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
@ -23,10 +22,12 @@
|
||||
"@rollup/plugin-commonjs": "^21.0.2",
|
||||
"dayjs": "^1.10.8",
|
||||
"deepmerge": "^4.2.2",
|
||||
"fs-extra": "^10.0.1",
|
||||
"gulp": "^4.0.2",
|
||||
"jest": "^27.5.1",
|
||||
"rollup": "^2.69.0",
|
||||
"rollup-plugin-clear": "^2.0.7",
|
||||
"shelljs": "^0.8.5",
|
||||
"vinyl": "^2.2.1"
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,10 @@
|
||||
*
|
||||
* - 将源文件中的t("xxxxx")转码成t("id")
|
||||
* - 自动导入languages/index.js中的翻译函数t
|
||||
*
|
||||
*
|
||||
* 查看AST: https://astexplorer.net/
|
||||
* Babel插件手册: https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md
|
||||
*
|
||||
* 使用方法:
|
||||
*
|
||||
* {
|
16
packages/babel/package.json
Normal file
16
packages/babel/package.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@voerkai18n/babel",
|
||||
"version": "1.0.0",
|
||||
"description": "VoerkaI18n babel plugin",
|
||||
"main": "index.js",
|
||||
"homepage": "https://gitee.com/zhangfisher/voerka-i18n",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://gitee.com/zhangfisher/voerka-i18n.git"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
3
packages/babel/readme.md
Normal file
3
packages/babel/readme.md
Normal file
@ -0,0 +1,3 @@
|
||||
# @voerkai18n/babel
|
||||
|
||||
`Babel`转码插件,用来对翻译文本进行自动转码
|
18
packages/babel/utils.js
Normal file
18
packages/babel/utils.js
Normal file
@ -0,0 +1,18 @@
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports = {
|
||||
isPlainObject
|
||||
}
|
@ -16,7 +16,9 @@ program
|
||||
.option('-d, --debug', t('输出调试信息'))
|
||||
.option('-r, --reset', t('重新生成当前项目的语言配置'))
|
||||
.option('-m, --moduleType [type]', t('生成的js模块类型,取值auto,esm,cjs'),"auto")
|
||||
.option('-lngs, --languages <languages...>', t('支持的语言列表'), ['cn','en'])
|
||||
.option('-lngs, --languages <languages...>', t('支持的语言列表'), ['cn','en'])
|
||||
.option('-default, --defaultLanguage <name>', t('默认语言'), 'cn')
|
||||
.option('-active, --activeLanguage <name>', t('激活语言'), 'cn')
|
||||
.hook("preAction",async function(location){
|
||||
const lang= process.env.LANGUAGE
|
||||
if(lang){
|
||||
|
@ -47,7 +47,7 @@ module.exports = function(srcPath,{debug = true,languages=["cn","en"],defaultLan
|
||||
namespaces:{}
|
||||
}
|
||||
// 写入配置文件
|
||||
if(["esm","es"].includes(moduleType)){
|
||||
if(["esm","es","module"].includes(moduleType)){
|
||||
fs.writeFileSync(settingsFile,`export default ${JSON.stringify(settings,null,4)}`)
|
||||
}else{
|
||||
fs.writeFileSync(settingsFile,`module.exports = ${JSON.stringify(settings,null,4)}`)
|
||||
|
@ -10,7 +10,7 @@ const activeMessages = defaultMessages
|
||||
// 语言作用域
|
||||
const scope = new i18nScope({
|
||||
...scopeSettings, // languages,defaultLanguage,activeLanguage,namespaces,formatters
|
||||
id: "@voerkai18n/tools", // 当前作用域的id,自动取当前工程的package.json的name
|
||||
id: "@voerkai18n/cli", // 当前作用域的id,自动取当前工程的package.json的name
|
||||
default: defaultMessages, // 默认语言包
|
||||
messages : activeMessages, // 当前语言包
|
||||
idMap:messageIds, // 消息id映射列表
|
||||
|
@ -1,19 +1,30 @@
|
||||
{
|
||||
"name": "@voerkai18n/tools",
|
||||
"name": "@voerkai18n/cli",
|
||||
"version": "1.0.0",
|
||||
"description": "VoerkaI18n Tools",
|
||||
"description": "VoerkaI18n command line interactive tools",
|
||||
"main": "index.js",
|
||||
"homepage": "https://gitee.com/zhangfisher/voerka-i18n",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://gitee.com/zhangfisher/voerka-i18n.git"
|
||||
},
|
||||
"keywords": [
|
||||
"i18n",
|
||||
"language",
|
||||
"translation",
|
||||
"internationalize"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"extract": "node ./index.js extract -d -e babel-plugin-voerkai18n.js,templates/**",
|
||||
"compile": "node ./index.js compile -d",
|
||||
"compile:en": "cross-env LANGUAGE=en node ./index.js compile -d"
|
||||
},
|
||||
"author": "",
|
||||
"author": "wxzhang",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"voerkai18n": "./index.js"
|
||||
},
|
||||
"license": "ISC",
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/cli": "^7.17.6",
|
||||
"@babel/core": "^7.17.5",
|
||||
@ -28,8 +39,5 @@
|
||||
"through2": "^4.0.2",
|
||||
"vinyl": "^2.2.1",
|
||||
"cross-env": "^7.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
}
|
||||
}
|
||||
|
63
packages/cli/readme.md
Normal file
63
packages/cli/readme.md
Normal file
@ -0,0 +1,63 @@
|
||||
# VoerkaI18n命令行工具
|
||||
|
||||
`@VoerkaI18n/cli`实现初始化、文本提取和编译等命令
|
||||
|
||||
## 初始化项目 - init
|
||||
|
||||
```shell
|
||||
初始化项目国际化配置
|
||||
Arguments:
|
||||
location 工程项目所在目录
|
||||
Options:
|
||||
-d, --debug 输出调试信息
|
||||
-r, --reset 重新生成当前项目的语言配置
|
||||
-m, --moduleType [type] 生成的js模块类型,取值auto,esm,cjs (default: "auto")
|
||||
-lngs, --languages <languages...> 支持的语言列表 (default: ["cn","en"])
|
||||
-default, --defaultLanguage 默认语言
|
||||
-active, --activeLanguage 激活语言
|
||||
-h, --help display help for command
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
## 提取文本 - extract
|
||||
|
||||
```shell
|
||||
|
||||
扫描并提取所有待翻译的字符串到<languages/translates>文件夹中
|
||||
|
||||
Arguments:
|
||||
location 工程项目所在目录 (default: "./")
|
||||
|
||||
Options:
|
||||
-d, --debug 输出调试信息
|
||||
-lngs, --languages 支持的语言
|
||||
-default, --defaultLanguage 默认语言
|
||||
-active, --activeLanguage 激活语言
|
||||
-ns, --namespaces 翻译名称空间
|
||||
-e, --exclude <folders> 排除要扫描的文件夹,多个用逗号分隔
|
||||
-u, --updateMode 本次提取内容与已存在内容的数据合并策略,默认取值sync=同步,overwrite=覆盖,merge=合并
|
||||
-f, --filetypes 要扫描的文件类型
|
||||
-h, --help display help for command
|
||||
|
||||
```
|
||||
|
||||
## 编译文本 - compile
|
||||
|
||||
```shell
|
||||
|
||||
Usage: voerkai18n compile [options] [location]
|
||||
|
||||
编译指定项目的语言包
|
||||
|
||||
Arguments:
|
||||
location 工程项目所在目录 (default: "./")
|
||||
|
||||
Options:
|
||||
-d, --debug 输出调试信息
|
||||
-m, --moduleType [types] 输出模块类型,取值auto,esm,cjs (default: "auto")
|
||||
-h, --help display help for command
|
||||
|
||||
```
|
||||
|
@ -2,13 +2,15 @@ const path = require("path")
|
||||
const fs = require("fs")
|
||||
const readJson = require("readjson")
|
||||
|
||||
async function importModule(url){
|
||||
async function importModule(url,onlyDefault=true) {
|
||||
try{
|
||||
return require(url)
|
||||
}catch{
|
||||
return await import(url)
|
||||
}
|
||||
}catch(e){
|
||||
const result = await import(`file:///${url}`)
|
||||
return onlyDefault ? result.default : result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从当前文件夹开始向上查找package.json文件,并解析出语言包的类型
|
||||
* @param {*} folder
|
||||
@ -58,8 +60,11 @@ function createPackageJsonFile(targetPath,moduleType="auto"){
|
||||
moduleType = findModuleType(targetPath)
|
||||
}
|
||||
const packageJsonFile = path.join(targetPath, "package.json")
|
||||
if(["esm","es"].includes(moduleType)){
|
||||
if(["esm","es","module"].includes(moduleType)){
|
||||
fs.writeFileSync(packageJsonFile,JSON.stringify({type:"module",license:"MIT"},null,4))
|
||||
if(moduleType==="module"){
|
||||
moduleType = "esm"
|
||||
}
|
||||
}else{
|
||||
fs.writeFileSync(packageJsonFile,JSON.stringify({license:"MIT"},null,4))
|
||||
}
|
||||
@ -150,7 +155,7 @@ function escape(str){
|
||||
}
|
||||
|
||||
// 翻译函数
|
||||
// @voerkai18n/tools工程本身使用了voerkai18n,即@voerkai18n/tools的extract和compile依赖于其自己生成的languages运行时
|
||||
// @voerkai18n/cli工程本身使用了voerkai18n,即@voerkai18n/cli的extract和compile依赖于其自己生成的languages运行时
|
||||
// 这样产生了鸡蛋问题,因此在extract与compile调试阶段如果t函数无法使用(即编译的languages无法正常使用),则需要提供t函数
|
||||
// 此函数的目的是提供一种容错方式
|
||||
let t
|
||||
|
@ -1,51 +0,0 @@
|
||||
|
||||
import messageIds from "./idMap.js"
|
||||
import { translate,I18nManager } from "@voerkai18n/runtime"
|
||||
import defaultMessages from "./cn.js"
|
||||
import scopeSettings from "./settings.js"
|
||||
|
||||
|
||||
// 自动创建全局VoerkaI18n实例
|
||||
if(!globalThis.VoerkaI18n){
|
||||
globalThis.VoerkaI18n = new I18nManager(scopeSettings)
|
||||
}
|
||||
|
||||
let scope = {
|
||||
defaultLanguage: "cn", // 默认语言名称
|
||||
default: defaultMessages, // 默认语言包
|
||||
messages : defaultMessages, // 当前语言包
|
||||
idMap:messageIds, // 消息id映射列表
|
||||
formatters:{}, // 当前作用域的格式化函数列表
|
||||
loaders:{}, // 异步加载语言文件的函数列表
|
||||
global:{}, // 引用全局VoerkaI18n配置,注册后自动引用
|
||||
// 主要用来缓存格式化器的引用,当使用格式化器时可以直接引用,避免检索
|
||||
$cache:{
|
||||
activeLanguage:null,
|
||||
typedFormatters:{},
|
||||
formatters:{},
|
||||
}
|
||||
}
|
||||
|
||||
let supportedlanguages = {}
|
||||
|
||||
|
||||
scope.loaders["en"] = ()=>import("./en.js")
|
||||
|
||||
|
||||
const t = translate.bind(scope)
|
||||
const languages = [
|
||||
{
|
||||
"name": "cn",
|
||||
"title": "cn"
|
||||
},
|
||||
{
|
||||
"name": "en",
|
||||
"title": "en"
|
||||
}
|
||||
]
|
||||
// 注册当前作用域到全局VoerkaI18n实例
|
||||
VoerkaI18n.register(scope)
|
||||
|
||||
|
||||
export { t, languages,scope }
|
||||
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"type": "commonjs",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@voerkai18n/tools": "workspace:^1.0.0"
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
const babel = require("@babel/core");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const i18nPlugin = require("@voerkai18n/tools/babel-plugin-voerkai18n");
|
||||
|
||||
|
||||
const code = fs.readFileSync(path.join(__dirname, "./apps/app/index.js"), "utf-8");
|
||||
babel.transform(code, {
|
||||
plugins: [
|
||||
[
|
||||
i18nPlugin,
|
||||
{
|
||||
location:"./languages" // 指定语言文件存放的目录,即保存编译后的语言文件的文件夹
|
||||
}
|
||||
]
|
||||
]
|
||||
}, function(err, result) {
|
||||
console.log(result.code)
|
||||
});
|
@ -3,7 +3,7 @@
|
||||
* 提供日期时间格式化的功能
|
||||
*
|
||||
* import './languages';
|
||||
* import "voerka-i18n/formatters/datetime"; // 货币格式化
|
||||
* import "voerka-i18n/formatters/datetime"; // 货币格式化器
|
||||
*
|
||||
*
|
||||
*/
|
||||
@ -11,17 +11,7 @@
|
||||
|
||||
if(globalThis.VoerkaI18n){
|
||||
|
||||
VoerkaI18n.registerFormatters({
|
||||
"*":{
|
||||
|
||||
},
|
||||
cn:{
|
||||
|
||||
},
|
||||
en:{
|
||||
|
||||
}
|
||||
})
|
||||
VoerkaI18n.registerFormatter()
|
||||
}
|
||||
|
||||
|
||||
|
2
packages/formatters/index.js
Normal file
2
packages/formatters/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
import './datetime.formatters.js';
|
||||
import './currency.formatters.js';
|
@ -6,8 +6,11 @@
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"exports":{
|
||||
|
||||
},
|
||||
"author": "wxzhang",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"deepmerge": "^4.2.2",
|
||||
"gulp": "^4.0.2",
|
||||
|
3
packages/formatters/readme.md
Normal file
3
packages/formatters/readme.md
Normal file
@ -0,0 +1,3 @@
|
||||
# @voerkai18n/formatters
|
||||
|
||||
|
1
packages/runtime/dist/index.cjs
vendored
Normal file
1
packages/runtime/dist/index.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
1
packages/runtime/dist/index.esm.js
vendored
Normal file
1
packages/runtime/dist/index.esm.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,883 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var isMergeableObject = function isMergeableObject(value) {
|
||||
return isNonNullObject(value)
|
||||
&& !isSpecial(value)
|
||||
};
|
||||
|
||||
function isNonNullObject(value) {
|
||||
return !!value && typeof value === 'object'
|
||||
}
|
||||
|
||||
function isSpecial(value) {
|
||||
var stringValue = Object.prototype.toString.call(value);
|
||||
|
||||
return stringValue === '[object RegExp]'
|
||||
|| stringValue === '[object Date]'
|
||||
|| isReactElement(value)
|
||||
}
|
||||
|
||||
// see https://github.com/facebook/react/blob/b5ac963fb791d1298e7f396236383bc955f916c1/src/isomorphic/classic/element/ReactElement.js#L21-L25
|
||||
var canUseSymbol = typeof Symbol === 'function' && Symbol.for;
|
||||
var REACT_ELEMENT_TYPE = canUseSymbol ? Symbol.for('react.element') : 0xeac7;
|
||||
|
||||
function isReactElement(value) {
|
||||
return value.$$typeof === REACT_ELEMENT_TYPE
|
||||
}
|
||||
|
||||
function emptyTarget(val) {
|
||||
return Array.isArray(val) ? [] : {}
|
||||
}
|
||||
|
||||
function cloneUnlessOtherwiseSpecified(value, options) {
|
||||
return (options.clone !== false && options.isMergeableObject(value))
|
||||
? deepmerge(emptyTarget(value), value, options)
|
||||
: value
|
||||
}
|
||||
|
||||
function defaultArrayMerge(target, source, options) {
|
||||
return target.concat(source).map(function(element) {
|
||||
return cloneUnlessOtherwiseSpecified(element, options)
|
||||
})
|
||||
}
|
||||
|
||||
function getMergeFunction(key, options) {
|
||||
if (!options.customMerge) {
|
||||
return deepmerge
|
||||
}
|
||||
var customMerge = options.customMerge(key);
|
||||
return typeof customMerge === 'function' ? customMerge : deepmerge
|
||||
}
|
||||
|
||||
function getEnumerableOwnPropertySymbols(target) {
|
||||
return Object.getOwnPropertySymbols
|
||||
? Object.getOwnPropertySymbols(target).filter(function(symbol) {
|
||||
return target.propertyIsEnumerable(symbol)
|
||||
})
|
||||
: []
|
||||
}
|
||||
|
||||
function getKeys(target) {
|
||||
return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target))
|
||||
}
|
||||
|
||||
function propertyIsOnObject(object, property) {
|
||||
try {
|
||||
return property in object
|
||||
} catch(_) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Protects from prototype poisoning and unexpected merging up the prototype chain.
|
||||
function propertyIsUnsafe(target, key) {
|
||||
return propertyIsOnObject(target, key) // Properties are safe to merge if they don't exist in the target yet,
|
||||
&& !(Object.hasOwnProperty.call(target, key) // unsafe if they exist up the prototype chain,
|
||||
&& Object.propertyIsEnumerable.call(target, key)) // and also unsafe if they're nonenumerable.
|
||||
}
|
||||
|
||||
function mergeObject(target, source, options) {
|
||||
var destination = {};
|
||||
if (options.isMergeableObject(target)) {
|
||||
getKeys(target).forEach(function(key) {
|
||||
destination[key] = cloneUnlessOtherwiseSpecified(target[key], options);
|
||||
});
|
||||
}
|
||||
getKeys(source).forEach(function(key) {
|
||||
if (propertyIsUnsafe(target, key)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (propertyIsOnObject(target, key) && options.isMergeableObject(source[key])) {
|
||||
destination[key] = getMergeFunction(key, options)(target[key], source[key], options);
|
||||
} else {
|
||||
destination[key] = cloneUnlessOtherwiseSpecified(source[key], options);
|
||||
}
|
||||
});
|
||||
return destination
|
||||
}
|
||||
|
||||
function deepmerge(target, source, options) {
|
||||
options = options || {};
|
||||
options.arrayMerge = options.arrayMerge || defaultArrayMerge;
|
||||
options.isMergeableObject = options.isMergeableObject || isMergeableObject;
|
||||
// cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge()
|
||||
// implementations can use it. The caller may not replace it.
|
||||
options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified;
|
||||
|
||||
var sourceIsArray = Array.isArray(source);
|
||||
var targetIsArray = Array.isArray(target);
|
||||
var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray;
|
||||
|
||||
if (!sourceAndTargetTypesMatch) {
|
||||
return cloneUnlessOtherwiseSpecified(source, options)
|
||||
} else if (sourceIsArray) {
|
||||
return options.arrayMerge(target, source, options)
|
||||
} else {
|
||||
return mergeObject(target, source, options)
|
||||
}
|
||||
}
|
||||
|
||||
deepmerge.all = function deepmergeAll(array, options) {
|
||||
if (!Array.isArray(array)) {
|
||||
throw new Error('first argument should be an array')
|
||||
}
|
||||
|
||||
return array.reduce(function(prev, next) {
|
||||
return deepmerge(prev, next, options)
|
||||
}, {})
|
||||
};
|
||||
|
||||
var deepmerge_1 = deepmerge;
|
||||
|
||||
var cjs = deepmerge_1;
|
||||
|
||||
/**
|
||||
* 内置的格式化器
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 字典格式化器
|
||||
* 根据输入data的值,返回后续参数匹配的结果
|
||||
* dict(data,<value1>,<result1>,<value2>,<result1>,<value3>,<result1>,...)
|
||||
*
|
||||
*
|
||||
* dict(1,1,"one",2,"two",3,"three",4,"four") == "one"
|
||||
* dict(2,1,"one",2,"two",3,"three",4,"four") == "two"
|
||||
* dict(3,1,"one",2,"two",3,"three",4,"four") == "three"
|
||||
* dict(4,1,"one",2,"two",3,"three",4,"four") == "four"
|
||||
* // 无匹配时返回原始值
|
||||
* dict(5,1,"one",2,"two",3,"three",4,"four") == 5
|
||||
* // 无匹配时并且后续参数个数是奇数,则返回最后一个参数
|
||||
* dict(5,1,"one",2,"two",3,"three",4,"four","more") == "more"
|
||||
*
|
||||
* 在翻译中使用
|
||||
* I have { value | dict(1,"one",2,"two",3,"three",4,"four")} apples
|
||||
*
|
||||
* @param {*} value
|
||||
* @param {...any} args
|
||||
* @returns
|
||||
*/
|
||||
function dict(value,...args){
|
||||
try{
|
||||
for(let i=0;i<args.length;i+=2){
|
||||
if(args[i]===value){
|
||||
return args[i+1]
|
||||
}
|
||||
}
|
||||
if(args.length >0 && (args.length % 2!==0)) return args[args.length-1]
|
||||
}catch{}
|
||||
return value
|
||||
}
|
||||
|
||||
|
||||
var formatters$1 = {
|
||||
"*":{
|
||||
$types:{
|
||||
Date:(value)=>value.toLocaleString()
|
||||
},
|
||||
time:(value)=> value.toLocaleTimeString(),
|
||||
date: (value)=> value.toLocaleDateString(),
|
||||
dict, //字典格式化器
|
||||
},
|
||||
cn:{
|
||||
$types:{
|
||||
Date:(value)=> `${value.getFullYear()}年${value.getMonth()+1}月${value.getDate()}日 ${value.getHours()}点${value.getMinutes()}分${value.getSeconds()}秒`
|
||||
},
|
||||
time:(value)=>`${value.getHours()}点${value.getMinutes()}分${value.getSeconds()}秒`,
|
||||
date: (value)=> `${value.getFullYear()}年${value.getMonth()+1}月${value.getDate()}日`,
|
||||
currency:(value)=>`${value}元`,
|
||||
},
|
||||
en:{
|
||||
currency:(value)=>`$${value}`,
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取指定变量类型名称
|
||||
* getDataTypeName(1) == Number
|
||||
* getDataTypeName("") == String
|
||||
* getDataTypeName(null) == Null
|
||||
* getDataTypeName(undefined) == Undefined
|
||||
* getDataTypeName(new Date()) == Date
|
||||
* getDataTypeName(new Error()) == Error
|
||||
*
|
||||
* @param {*} v
|
||||
* @returns
|
||||
*/
|
||||
function getDataTypeName$1(v){
|
||||
if (v === null) return 'Null'
|
||||
if (v === undefined) return 'Undefined'
|
||||
if(typeof(v)==="function") return "Function"
|
||||
return v.constructor && v.constructor.name;
|
||||
}function isPlainObject$1(obj){
|
||||
if (typeof obj !== 'object' || obj === null) return false;
|
||||
var proto = Object.getPrototypeOf(obj);
|
||||
if (proto === null) return true;
|
||||
var baseProto = proto;
|
||||
|
||||
while (Object.getPrototypeOf(baseProto) !== null) {
|
||||
baseProto = Object.getPrototypeOf(baseProto);
|
||||
}
|
||||
return proto === baseProto;
|
||||
}
|
||||
function isNumber$1(value){
|
||||
return !isNaN(parseInt(value))
|
||||
}
|
||||
|
||||
var utils = {
|
||||
getDataTypeName: getDataTypeName$1,
|
||||
isNumber: isNumber$1,
|
||||
isPlainObject: isPlainObject$1
|
||||
};
|
||||
|
||||
const deepMerge = cjs;
|
||||
const formatters = formatters$1;
|
||||
const {isPlainObject ,isNumber , getDataTypeName} = utils;
|
||||
|
||||
// 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter }
|
||||
// 不支持参数: let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*\s*)*)\s*\}/g
|
||||
|
||||
// 支持参数: { var | formatter(x,x,..) | formatter }
|
||||
let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*(\(.*\)){0,1}\s*)*)\s*\}/g;
|
||||
|
||||
// 有效的语言名称列表
|
||||
const languages = ["af","am","ar-dz","ar-iq","ar-kw","ar-ly","ar-ma","ar-sa","ar-tn","ar","az","be","bg","bi","bm","bn","bo","br","bs","ca","cs","cv","cy","da","de-at","de-ch","de","dv","el","en-au","en-ca","en-gb","en-ie","en-il","en-in","en-nz","en-sg","en-tt","en","eo","es-do","es-mx","es-pr","es-us","es","et","eu","fa","fi","fo","fr-ca","fr-ch","fr","fy","ga","gd","gl","gom-latn","gu","he","hi","hr","ht","hu","hy-am","id","is","it-ch","it","ja","jv","ka","kk","km","kn","ko","ku","ky","lb","lo","lt","lv","me","mi","mk","ml","mn","mr","ms-my","ms","mt","my","nb","ne","nl-be","nl","nn","oc-lnc","pa-in","pl","pt-br","pt","ro","ru","rw","sd","se","si","sk","sl","sq","sr-cyrl","sr","ss","sv-fi","sv","sw","ta","te","tet","tg","th","tk","tl-ph","tlh","tr","tzl","tzm-latn","tzm","ug-cn","uk","ur","uz-latn","uz","vi","x-pseudo","yo","zh-cn","zh-hk","zh-tw","zh"];
|
||||
|
||||
/**
|
||||
* 考虑到通过正则表达式进行插件的替换可能较慢,因此提供一个简单方法来过滤掉那些
|
||||
* 不需要进行插值处理的字符串
|
||||
* 原理很简单,就是判断一下是否同时具有{和}字符,如果有则认为可能有插值变量,如果没有则一定没有插件变量,则就不需要进行正则匹配
|
||||
* 从而可以减少不要的正则匹配
|
||||
* 注意:该方法只能快速判断一个字符串不包括插值变量
|
||||
* @param {*} str
|
||||
* @returns {boolean} true=可能包含插值变量,
|
||||
*/
|
||||
function hasInterpolation(str){
|
||||
return str.includes("{") && str.includes("}")
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
通过正则表达式对原始文本内容进行解析匹配后得到的
|
||||
formatters="| aaa(1,1) | bbb "
|
||||
|
||||
需要统一解析为
|
||||
|
||||
[
|
||||
[aaa,[1,1]], // [formatter'name,[args,...]]
|
||||
[bbb,[]],
|
||||
]
|
||||
|
||||
formatters="| aaa(1,1,"dddd") | bbb "
|
||||
|
||||
目前对参数采用简单的split(",")来解析,因为无法正确解析aaa(1,1,"dd,,dd")形式的参数
|
||||
在此场景下基本够用了,如果需要支持更复杂的参数解析,可以后续考虑使用正则表达式来解析
|
||||
|
||||
@returns [[<formatterName>,[<arg>,<arg>,...]]]
|
||||
*/
|
||||
function parseFormatters(formatters){
|
||||
if(!formatters) return []
|
||||
// 1. 先解析为 ["aaa()","bbb"]形式
|
||||
let result = formatters.trim().substr(1).trim().split("|").map(r=>r.trim());
|
||||
|
||||
// 2. 解析格式化器参数
|
||||
return result.map(formatter=>{
|
||||
let firstIndex = formatter.indexOf("(");
|
||||
let lastIndex = formatter.lastIndexOf(")");
|
||||
if(firstIndex!==-1 && lastIndex!==-1){ // 带参数的格式化器
|
||||
const argsContent = formatter.substr(firstIndex+1,lastIndex-firstIndex-1).trim();
|
||||
let args = argsContent=="" ? [] : argsContent.split(",").map(arg=>{
|
||||
arg = arg.trim();
|
||||
if(!isNaN(parseInt(arg))){
|
||||
return parseInt(arg) // 数字
|
||||
}else if((arg.startsWith('\"') && arg.endsWith('\"')) || (arg.startsWith('\'') && arg.endsWith('\'')) ){
|
||||
return arg.substr(1,arg.length-2) // 字符串
|
||||
}else if(arg.toLowerCase()==="true" || arg.toLowerCase()==="false"){
|
||||
return arg.toLowerCase()==="true" // 布尔值
|
||||
}else if((arg.startsWith('{') && arg.endsWith('}')) || (arg.startsWith('[') && arg.endsWith(']'))){
|
||||
try{
|
||||
return JSON.parse(arg)
|
||||
}catch(e){
|
||||
return String(arg)
|
||||
}
|
||||
}else {
|
||||
return String(arg)
|
||||
}
|
||||
});
|
||||
return [formatter.substr(0,firstIndex),args]
|
||||
}else {// 不带参数的格式化器
|
||||
return [formatter,[]]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取字符串中的插值变量
|
||||
* // [
|
||||
// {
|
||||
name:<变量名称>,formatters:[{name:<格式化器名称>,args:[<参数>,<参数>,....]]}],<匹配字符串>],
|
||||
// ....
|
||||
//
|
||||
* @param {*} str
|
||||
* @param {*} isFull =true 保留所有插值变量 =false 进行去重
|
||||
* @returns {Array}
|
||||
* [
|
||||
* {
|
||||
* name:"<变量名称>",
|
||||
* formatters:[
|
||||
* {name:"<格式化器名称>",args:[<参数>,<参数>,....]},
|
||||
* {name:"<格式化器名称>",args:[<参数>,<参数>,....]},
|
||||
* ],
|
||||
* match:"<匹配字符串>"
|
||||
* },
|
||||
* ...
|
||||
* ]
|
||||
*/
|
||||
function getInterpolatedVars(str){
|
||||
let vars = [];
|
||||
forEachInterpolatedVars(str,(varName,formatters,match)=>{
|
||||
let varItem = {
|
||||
name:varName,
|
||||
formatters:formatters.map(([formatter,args])=>{
|
||||
return {
|
||||
name:formatter,
|
||||
args:args
|
||||
}
|
||||
}),
|
||||
match:match
|
||||
};
|
||||
if(vars.findIndex(varDef=>((varDef.name===varItem.name) && (varItem.formatters.toString() == varDef.formatters.toString())))===-1){
|
||||
vars.push(varItem);
|
||||
}
|
||||
return ""
|
||||
});
|
||||
return vars
|
||||
}
|
||||
/**
|
||||
* 遍历str中的所有插值变量传递给callback,将callback返回的结果替换到str中对应的位置
|
||||
* @param {*} str
|
||||
* @param {Function(<变量名称>,[formatters],match[0])} callback
|
||||
* @returns 返回替换后的字符串
|
||||
*/
|
||||
function forEachInterpolatedVars(str,callback,options={}){
|
||||
let result=str, match;
|
||||
let opts = Object.assign({
|
||||
replaceAll:true, // 是否替换所有插值变量,当使用命名插值时应置为true,当使用位置插值时应置为false
|
||||
},options);
|
||||
varWithPipeRegexp.lastIndex=0;
|
||||
while ((match = varWithPipeRegexp.exec(result)) !== null) {
|
||||
const varname = match.groups.varname || "";
|
||||
// 解析格式化器和参数 = [<formatterName>,[<formatterName>,[<arg>,<arg>,...]]]
|
||||
const formatters = parseFormatters(match.groups.formatters);
|
||||
if(typeof(callback)==="function"){
|
||||
try{
|
||||
if(opts.replaceAll){
|
||||
result=result.replaceAll(match[0],callback(varname,formatters,match[0]));
|
||||
}else {
|
||||
result=result.replace(match[0],callback(varname,formatters,match[0]));
|
||||
}
|
||||
}catch{// callback函数可能会抛出异常,如果抛出异常,则中断匹配过程
|
||||
break
|
||||
}
|
||||
}
|
||||
varWithPipeRegexp.lastIndex=0;
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
// 缓存数据类型的格式化器,避免每次都调用getDataTypeDefaultFormatter
|
||||
let datatypeFormattersCache ={
|
||||
$activeLanguage:null,
|
||||
};
|
||||
/**
|
||||
* 取得指定数据类型的默认格式化器
|
||||
*
|
||||
* 可以为每一个数据类型指定一个格式化器,当传入插值变量时,会自动调用该格式化器来对值进行格式化转换
|
||||
|
||||
const formatters = {
|
||||
"*":{
|
||||
$types:{...} // 在所有语言下只作用于特定数据类型的格式化器
|
||||
}, // 在所有语言下生效的格式化器
|
||||
cn:{
|
||||
$types:{
|
||||
[数据类型]:(value)=>{...},
|
||||
},
|
||||
[格式化器名称]:(value)=>{...},
|
||||
[格式化器名称]:(value)=>{...},
|
||||
[格式化器名称]:(value)=>{...},
|
||||
},
|
||||
}
|
||||
* @param {*} scope
|
||||
* @param {*} activeLanguage
|
||||
* @param {*} dataType 数字类型
|
||||
* @returns {Function} 格式化函数
|
||||
*/
|
||||
function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){
|
||||
if(datatypeFormattersCache.$activeLanguage === activeLanguage) {
|
||||
if(dataType in datatypeFormattersCache) return datatypeFormattersCache[dataType]
|
||||
}else {// 清空缓存
|
||||
datatypeFormattersCache = { $activeLanguage:activeLanguage };
|
||||
}
|
||||
|
||||
// 先在当前作用域中查找,再在全局查找
|
||||
const targets = [scope.formatters,scope.global.formatters];
|
||||
for(const target of targets){
|
||||
if(activeLanguage in target){
|
||||
// 在当前语言的$types中查找
|
||||
let formatters = target[activeLanguage].$types || {};
|
||||
for(let [name,formatter] of Object.entries(formatters)){
|
||||
if(name === dataType && typeof(formatter)==="function") {
|
||||
datatypeFormattersCache[dataType] = formatter;
|
||||
return formatter
|
||||
}
|
||||
}
|
||||
}
|
||||
// 在所有语言的$types中查找
|
||||
let formatters = target["*"].$types || {};
|
||||
for(let [name,formatter] of Object.entries(formatters)){
|
||||
if(name === dataType && typeof(formatter)==="function") {
|
||||
datatypeFormattersCache[dataType] = formatter;
|
||||
return formatter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定名称的格式化器函数
|
||||
* @param {*} scope
|
||||
* @param {*} activeLanguage
|
||||
* @param {*} name 格式化器名称
|
||||
* @returns {Function} 格式化函数
|
||||
*/
|
||||
let formattersCache = { $activeLanguage:null};
|
||||
function getFormatter(scope,activeLanguage,name){
|
||||
if(formattersCache.$activeLanguage === activeLanguage) {
|
||||
if(name in formattersCache) return formattersCache[name]
|
||||
}else { // 当切换语言时需要清空缓存
|
||||
formattersCache = { $activeLanguage:activeLanguage };
|
||||
}
|
||||
// 先在当前作用域中查找,再在全局查找
|
||||
const targets = [scope.formatters,scope.global.formatters];
|
||||
for(const target of targets){
|
||||
// 优先在当前语言查找
|
||||
if(activeLanguage in target){
|
||||
let formatters = target[activeLanguage] || {};
|
||||
if((name in formatters) && typeof(formatters[name])==="function") return formattersCache[name] = formatters[name]
|
||||
}
|
||||
// 在所有语言的$types中查找
|
||||
let formatters = target["*"] || {};
|
||||
if((name in formatters) && typeof(formatters[name])==="function") return formattersCache[name] = formatters[name]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行格式化器并返回结果
|
||||
* @param {*} value
|
||||
* @param {*} formatters
|
||||
*/
|
||||
function executeFormatter(value,formatters){
|
||||
if(formatters.length===0) return value
|
||||
let result = value;
|
||||
try{
|
||||
for(let formatter of formatters){
|
||||
if(typeof(formatter) === "function") {
|
||||
result = formatter(result);
|
||||
}else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}catch(e){
|
||||
console.error(`Error while execute i18n formatter for ${value}: ${e.message} ` );
|
||||
}
|
||||
return result
|
||||
}
|
||||
/**
|
||||
* 将 [[格式化器名称,[参数,参数,...]],[格式化器名称,[参数,参数,...]]]格式化器转化为
|
||||
*
|
||||
*
|
||||
*
|
||||
* @param {*} scope
|
||||
* @param {*} activeLanguage
|
||||
* @param {*} formatters
|
||||
*/
|
||||
function buildFormatters(scope,activeLanguage,formatters){
|
||||
let results = [];
|
||||
for(let formatter of formatters){
|
||||
if(formatter[0]){
|
||||
const func = getFormatter(scope,activeLanguage,formatter[0]);
|
||||
if(typeof(func)==="function"){
|
||||
results.push((v)=>{
|
||||
return func(v,...formatter[1])
|
||||
});
|
||||
}else {// 格式化器无效或者没有定义时,查看当前值是否具有同名的方法,如果有则执行调用
|
||||
results.push((v)=>{
|
||||
if(typeof(v[formatter[0]])==="function"){
|
||||
return v[formatter[0]].call(v,...formatter[1])
|
||||
}else {
|
||||
return v
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 将value经过格式化器处理后返回
|
||||
* @param {*} scope
|
||||
* @param {*} activeLanguage
|
||||
* @param {*} formatters
|
||||
* @param {*} value
|
||||
* @returns
|
||||
*/
|
||||
function getFormattedValue(scope,activeLanguage,formatters,value){
|
||||
// 1. 取得格式化器函数列表
|
||||
const formatterFuncs = buildFormatters(scope,activeLanguage,formatters);
|
||||
// 3. 查找每种数据类型默认格式化器,并添加到formatters最前面,默认数据类型格式化器优先级最高
|
||||
const defaultFormatter = getDataTypeDefaultFormatter(scope,activeLanguage,getDataTypeName(value));
|
||||
if(defaultFormatter){
|
||||
formatterFuncs.splice(0,0,defaultFormatter);
|
||||
}
|
||||
// 3. 执行格式化器
|
||||
value = executeFormatter(value,formatterFuncs);
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串可以进行变量插值替换,
|
||||
* replaceInterpolatedVars("<模板字符串>",{变量名称:变量值,变量名称:变量值,...})
|
||||
* replaceInterpolatedVars("<模板字符串>",[变量值,变量值,...])
|
||||
* replaceInterpolatedVars("<模板字符串>",变量值,变量值,...])
|
||||
*
|
||||
- 当只有两个参数并且第2个参数是{}时,将第2个参数视为命名变量的字典
|
||||
replaceInterpolatedVars("this is {a}+{b},{a:1,b:2}) --> this is 1+2
|
||||
- 当只有两个参数并且第2个参数是[]时,将第2个参数视为位置参数
|
||||
replaceInterpolatedVars"this is {}+{}",[1,2]) --> this is 1+2
|
||||
- 普通位置参数替换
|
||||
replaceInterpolatedVars("this is {a}+{b}",1,2) --> this is 1+2
|
||||
-
|
||||
this == scope == { formatters: {}, ... }
|
||||
* @param {*} template
|
||||
* @returns
|
||||
*/
|
||||
function replaceInterpolatedVars(template,...args) {
|
||||
const scope = this;
|
||||
// 当前激活语言
|
||||
const activeLanguage = scope.global.activeLanguage;
|
||||
|
||||
// 没有变量插值则的返回原字符串
|
||||
if(args.length===0 || !hasInterpolation(template)) return template
|
||||
|
||||
// ****************************变量插值****************************
|
||||
if(args.length===1 && isPlainObject(args[0])){
|
||||
// 读取模板字符串中的插值变量列表
|
||||
// [[var1,[formatter,formatter,...],match],[var2,[formatter,formatter,...],match],...}
|
||||
let varValues = args[0];
|
||||
return forEachInterpolatedVars(template,(varname,formatters)=>{
|
||||
let value = (varname in varValues) ? varValues[varname] : '';
|
||||
return getFormattedValue(scope,activeLanguage,formatters,value)
|
||||
})
|
||||
}else {
|
||||
// ****************************位置插值****************************
|
||||
// 如果只有一个Array参数,则认为是位置变量列表,进行展开
|
||||
const params=(args.length===1 && Array.isArray(args[0])) ? [...args[0]] : args;
|
||||
if(params.length===0) return template // 没有变量则不需要进行插值处理,返回原字符串
|
||||
let i = 0;
|
||||
return forEachInterpolatedVars(template,(varname,formatters)=>{
|
||||
if(params.length>i){
|
||||
return getFormattedValue(scope,activeLanguage,formatters,params[i++])
|
||||
}else {
|
||||
throw new Error() // 抛出异常,停止插值处理
|
||||
}
|
||||
},{replaceAll:false})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 默认语言配置
|
||||
const defaultLanguageSettings = {
|
||||
defaultLanguage: "cn",
|
||||
activeLanguage: "cn",
|
||||
languages:{
|
||||
cn:{name:"cn",title:"中文",default:true},
|
||||
en:{name:"en",title:"英文"},
|
||||
},
|
||||
formatters
|
||||
};
|
||||
|
||||
function isMessageId(content){
|
||||
return parseInt(content)>0
|
||||
}
|
||||
/**
|
||||
* 根据值的单数和复数形式,从messages中取得相应的消息
|
||||
*
|
||||
* @param {*} messages 复数形式的文本内容 = [<=0时的内容>,<=1时的内容>,<=2时的内容>,...]
|
||||
* @param {*} value
|
||||
*/
|
||||
function getPluraMessage(messages,value){
|
||||
try{
|
||||
if(Array.isArray(messages)){
|
||||
return messages.length > value ? messages[value] : messages[messages.length-1]
|
||||
}else {
|
||||
return messages
|
||||
}
|
||||
}catch{
|
||||
return Array.isArray(messages) ? messages[0] : messages
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译函数
|
||||
*
|
||||
* translate("要翻译的文本内容") 如果默认语言是中文,则不会进行翻译直接返回
|
||||
* translate("I am {} {}","man") == I am man 位置插值
|
||||
* translate("I am {p}",{p:"man"}) 字典插值
|
||||
* translate("total {$count} items", {$count:1}) //复数形式
|
||||
* translate("total {} {} {} items",a,b,c) // 位置变量插值
|
||||
*
|
||||
* this===scope 当前绑定的scope
|
||||
*
|
||||
*/
|
||||
function translate(message) {
|
||||
const scope = this;
|
||||
const activeLanguage = scope.global.activeLanguage;
|
||||
let content = message;
|
||||
let vars=[]; // 插值变量列表
|
||||
let pluralVars= []; // 复数变量
|
||||
let pluraValue = null; // 复数值
|
||||
try{
|
||||
// 1. 预处理变量: 复数变量保存至pluralVars中 , 变量如果是Function则调用
|
||||
if(arguments.length === 2 && isPlainObject(arguments[1])){
|
||||
Object.entries(arguments[1]).forEach(([name,value])=>{
|
||||
if(typeof(value)==="function"){
|
||||
try{
|
||||
vars[name] = value();
|
||||
}catch(e){
|
||||
vars[name] = value;
|
||||
}
|
||||
}
|
||||
// 以$开头的视为复数变量
|
||||
if(name.startsWith("$")) pluralVars.push(name);
|
||||
});
|
||||
vars = [arguments[1]];
|
||||
}else if(arguments.length >= 2){
|
||||
vars = [...arguments].splice(1).map((arg,index)=>{
|
||||
try{
|
||||
arg = typeof(arg)==="function" ? arg() : arg;
|
||||
// 位置参数中以第一个数值变量为复数变量
|
||||
if(isNumber(arg)) pluraValue = parseInt(arg);
|
||||
}catch(e){ }
|
||||
return arg
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// 2. 取得翻译文本模板字符串
|
||||
if(activeLanguage === scope.defaultLanguage){
|
||||
// 2.1 从默认语言中取得翻译文本模板字符串
|
||||
// 如果当前语言就是默认语言,不需要查询加载,只需要做插值变换即可
|
||||
// 当源文件运用了babel插件后会将原始文本内容转换为msgId
|
||||
// 如果是msgId则从scope.default中读取,scope.default=默认语言包={<id>:<message>}
|
||||
if(isMessageId(content)){
|
||||
content = scope.default[content] || message;
|
||||
}
|
||||
}else {
|
||||
// 2.2 从当前语言包中取得翻译文本模板字符串
|
||||
// 如果没有启用babel插件将源文本转换为msgId,需要先将文本内容转换为msgId
|
||||
let msgId = isMessageId(content) ? content : scope.idMap[content];
|
||||
content = scope.messages[msgId] || content;
|
||||
}
|
||||
|
||||
// 3. 处理复数
|
||||
// 经过上面的处理,content可能是字符串或者数组
|
||||
// content = "原始文本内容" || 复数形式["原始文本内容","原始文本内容"....]
|
||||
// 如果是数组说明要启用复数机制,需要根据插值变量中的某个变量来判断复数形式
|
||||
if(Array.isArray(content) && content.length>0){
|
||||
// 如果存在复数命名变量,只取第一个复数变量
|
||||
if(pluraValue!==null){ // 启用的是位置插值,pluraIndex=第一个数字变量的位置
|
||||
content = getPluraMessage(content,pluraValue);
|
||||
}else if(pluralVar.length>0){
|
||||
content = getPluraMessage(content,parseInt(vars(pluralVar[0])));
|
||||
}else { // 如果找不到复数变量,则使用第一个内容
|
||||
content = content[0];
|
||||
}
|
||||
}
|
||||
// 进行插值处理
|
||||
if(vars.length==0){
|
||||
return content
|
||||
}else {
|
||||
return replaceInterpolatedVars.call(scope,content,...vars)
|
||||
}
|
||||
}catch(e){
|
||||
return content // 出错则返回原始文本
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多语言管理类
|
||||
*
|
||||
* 当导入编译后的多语言文件时(import("./languages")),会自动生成全局实例VoerkaI18n
|
||||
*
|
||||
* VoerkaI18n.languages // 返回支持的语言列表
|
||||
* VoerkaI18n.defaultLanguage // 默认语言
|
||||
* VoerkaI18n.language // 当前语言
|
||||
* VoerkaI18n.change(language) // 切换到新的语言
|
||||
*
|
||||
*
|
||||
* VoerkaI18n.on("change",(language)=>{}) // 注册语言切换事件
|
||||
* VoerkaI18n.off("change",(language)=>{})
|
||||
*
|
||||
* */
|
||||
class I18nManager{
|
||||
static instance = null; // 单例引用
|
||||
callbacks = [] // 当切换语言时的回调事件
|
||||
constructor(settings={}){
|
||||
if(I18nManager.instance!=null){
|
||||
return I18nManager.instance;
|
||||
}
|
||||
I18nManager.instance = this;
|
||||
this._settings = deepMerge(defaultLanguageSettings,settings);
|
||||
this._scopes=[];
|
||||
return I18nManager.instance;
|
||||
}
|
||||
get settings(){ return this._settings }
|
||||
get scopes(){ return this._scopes }
|
||||
// 当前激活语言
|
||||
get activeLanguage(){ return this._settings.activeLanguage}
|
||||
// 默认语言
|
||||
get defaultLanguage(){ return this.this._settings.defaultLanguage}
|
||||
// 支持的语言列表
|
||||
get languages(){ return this._settings.languages}
|
||||
// 订阅语言切换事件
|
||||
on(callback){
|
||||
this.callbacks.push(callback);
|
||||
}
|
||||
off(callback){
|
||||
for(let i=0;i<this.callbacks.length;i++){
|
||||
if(this.callbacks[i]===callback ){
|
||||
this.callbacks.splice(i,1);
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
offAll(){
|
||||
this.callbacks=[];
|
||||
}
|
||||
/**
|
||||
* 切换语言时触发语言切换事件回调
|
||||
*/
|
||||
async _triggerChangeEvents(newLanguage){
|
||||
try{
|
||||
await this._updateScopes(newLanguage);
|
||||
await (Promise.allSettled || Promise.all)(this.callbacks.map(async cb=>await cb(newLanguage)));
|
||||
}catch(e){
|
||||
console.warn("Error while executing language change events",e.message);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 切换语言
|
||||
*/
|
||||
async change(value){
|
||||
if(value in this.languages){
|
||||
await this._triggerChangeEvents(value);
|
||||
this._settings.activeLanguage = value;
|
||||
}else {
|
||||
throw new Error("Not supported language:"+value)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取指定作用域的下的语言包加载器
|
||||
*
|
||||
* 同时会进行语言兼容性处理
|
||||
*
|
||||
* 如scope里面定义了一个cn的语言包,当切换到zh-cn时,会自动加载cn语言包
|
||||
*
|
||||
*
|
||||
* @param {*} scope
|
||||
* @param {*} lang
|
||||
*/
|
||||
_getScopeLoader(scope,lang){
|
||||
|
||||
}
|
||||
/**
|
||||
* 当切换语言时调用此方法来加载更新语言包
|
||||
* @param {*} newLanguage
|
||||
*/
|
||||
async _updateScopes(newLanguage){
|
||||
// 并发执行所有作用域语言包的加载
|
||||
try{
|
||||
await (Promise.allSettled || Promise.all)(this._scopes.map(scope=>{
|
||||
return async ()=>{
|
||||
// 默认语言,所有均默认语言均采用静态加载方式,只需要简单的替换即可
|
||||
if(newLanguage === scope.defaultLanguage){
|
||||
scope.messages = scope.default;
|
||||
return
|
||||
}
|
||||
// 异步加载语言文件
|
||||
const loader = scope.loaders[newLanguage];
|
||||
if(typeof(loader) === "function"){
|
||||
try{
|
||||
scope.messages = await loader();
|
||||
}catch(e){
|
||||
console.warn(`Error loading language ${newLanguage} : ${e.message}`);
|
||||
scope.messages = defaultMessages; // 出错时回退到默认语言
|
||||
}
|
||||
}else {
|
||||
scope.messages = defaultMessages;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}catch(e){
|
||||
console.warn("Error while refreshing scope:",e.message);
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* 注册一个新的作用域
|
||||
*
|
||||
* 每一个库均对应一个作用域,每个作用域可以有多个语言包,且对应一个翻译函数
|
||||
* scope={
|
||||
* defaultLanguage:"cn",
|
||||
default: defaultMessages, // 转换文本信息
|
||||
messages : defaultMessages, // 当前语言的消息
|
||||
idMap:messageIds,
|
||||
formatters:{
|
||||
...formatters,
|
||||
...i18nSettings.formatters || {}
|
||||
},
|
||||
loaders:{}, // 异步加载语言文件的函数列表
|
||||
settings:{} // 引用全局VoerkaI18n实例的配置
|
||||
* }
|
||||
*
|
||||
* 除了默认语言外,其他语言采用动态加载的方式
|
||||
*
|
||||
* @param {*} scope
|
||||
*/
|
||||
register(scope){
|
||||
scope.global = this._settings;
|
||||
this._scopes.push(scope);
|
||||
}
|
||||
/**
|
||||
* 注册全局格式化器
|
||||
* @param {*} formatters
|
||||
*/
|
||||
registerFormatters(formatters){
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var runtime ={
|
||||
getInterpolatedVars,
|
||||
replaceInterpolatedVars,
|
||||
I18nManager,
|
||||
translate,
|
||||
languages,
|
||||
defaultLanguageSettings
|
||||
};
|
||||
|
||||
module.exports = runtime;
|
@ -1,881 +0,0 @@
|
||||
var isMergeableObject = function isMergeableObject(value) {
|
||||
return isNonNullObject(value)
|
||||
&& !isSpecial(value)
|
||||
};
|
||||
|
||||
function isNonNullObject(value) {
|
||||
return !!value && typeof value === 'object'
|
||||
}
|
||||
|
||||
function isSpecial(value) {
|
||||
var stringValue = Object.prototype.toString.call(value);
|
||||
|
||||
return stringValue === '[object RegExp]'
|
||||
|| stringValue === '[object Date]'
|
||||
|| isReactElement(value)
|
||||
}
|
||||
|
||||
// see https://github.com/facebook/react/blob/b5ac963fb791d1298e7f396236383bc955f916c1/src/isomorphic/classic/element/ReactElement.js#L21-L25
|
||||
var canUseSymbol = typeof Symbol === 'function' && Symbol.for;
|
||||
var REACT_ELEMENT_TYPE = canUseSymbol ? Symbol.for('react.element') : 0xeac7;
|
||||
|
||||
function isReactElement(value) {
|
||||
return value.$$typeof === REACT_ELEMENT_TYPE
|
||||
}
|
||||
|
||||
function emptyTarget(val) {
|
||||
return Array.isArray(val) ? [] : {}
|
||||
}
|
||||
|
||||
function cloneUnlessOtherwiseSpecified(value, options) {
|
||||
return (options.clone !== false && options.isMergeableObject(value))
|
||||
? deepmerge(emptyTarget(value), value, options)
|
||||
: value
|
||||
}
|
||||
|
||||
function defaultArrayMerge(target, source, options) {
|
||||
return target.concat(source).map(function(element) {
|
||||
return cloneUnlessOtherwiseSpecified(element, options)
|
||||
})
|
||||
}
|
||||
|
||||
function getMergeFunction(key, options) {
|
||||
if (!options.customMerge) {
|
||||
return deepmerge
|
||||
}
|
||||
var customMerge = options.customMerge(key);
|
||||
return typeof customMerge === 'function' ? customMerge : deepmerge
|
||||
}
|
||||
|
||||
function getEnumerableOwnPropertySymbols(target) {
|
||||
return Object.getOwnPropertySymbols
|
||||
? Object.getOwnPropertySymbols(target).filter(function(symbol) {
|
||||
return target.propertyIsEnumerable(symbol)
|
||||
})
|
||||
: []
|
||||
}
|
||||
|
||||
function getKeys(target) {
|
||||
return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target))
|
||||
}
|
||||
|
||||
function propertyIsOnObject(object, property) {
|
||||
try {
|
||||
return property in object
|
||||
} catch(_) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Protects from prototype poisoning and unexpected merging up the prototype chain.
|
||||
function propertyIsUnsafe(target, key) {
|
||||
return propertyIsOnObject(target, key) // Properties are safe to merge if they don't exist in the target yet,
|
||||
&& !(Object.hasOwnProperty.call(target, key) // unsafe if they exist up the prototype chain,
|
||||
&& Object.propertyIsEnumerable.call(target, key)) // and also unsafe if they're nonenumerable.
|
||||
}
|
||||
|
||||
function mergeObject(target, source, options) {
|
||||
var destination = {};
|
||||
if (options.isMergeableObject(target)) {
|
||||
getKeys(target).forEach(function(key) {
|
||||
destination[key] = cloneUnlessOtherwiseSpecified(target[key], options);
|
||||
});
|
||||
}
|
||||
getKeys(source).forEach(function(key) {
|
||||
if (propertyIsUnsafe(target, key)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (propertyIsOnObject(target, key) && options.isMergeableObject(source[key])) {
|
||||
destination[key] = getMergeFunction(key, options)(target[key], source[key], options);
|
||||
} else {
|
||||
destination[key] = cloneUnlessOtherwiseSpecified(source[key], options);
|
||||
}
|
||||
});
|
||||
return destination
|
||||
}
|
||||
|
||||
function deepmerge(target, source, options) {
|
||||
options = options || {};
|
||||
options.arrayMerge = options.arrayMerge || defaultArrayMerge;
|
||||
options.isMergeableObject = options.isMergeableObject || isMergeableObject;
|
||||
// cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge()
|
||||
// implementations can use it. The caller may not replace it.
|
||||
options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified;
|
||||
|
||||
var sourceIsArray = Array.isArray(source);
|
||||
var targetIsArray = Array.isArray(target);
|
||||
var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray;
|
||||
|
||||
if (!sourceAndTargetTypesMatch) {
|
||||
return cloneUnlessOtherwiseSpecified(source, options)
|
||||
} else if (sourceIsArray) {
|
||||
return options.arrayMerge(target, source, options)
|
||||
} else {
|
||||
return mergeObject(target, source, options)
|
||||
}
|
||||
}
|
||||
|
||||
deepmerge.all = function deepmergeAll(array, options) {
|
||||
if (!Array.isArray(array)) {
|
||||
throw new Error('first argument should be an array')
|
||||
}
|
||||
|
||||
return array.reduce(function(prev, next) {
|
||||
return deepmerge(prev, next, options)
|
||||
}, {})
|
||||
};
|
||||
|
||||
var deepmerge_1 = deepmerge;
|
||||
|
||||
var cjs = deepmerge_1;
|
||||
|
||||
/**
|
||||
* 内置的格式化器
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 字典格式化器
|
||||
* 根据输入data的值,返回后续参数匹配的结果
|
||||
* dict(data,<value1>,<result1>,<value2>,<result1>,<value3>,<result1>,...)
|
||||
*
|
||||
*
|
||||
* dict(1,1,"one",2,"two",3,"three",4,"four") == "one"
|
||||
* dict(2,1,"one",2,"two",3,"three",4,"four") == "two"
|
||||
* dict(3,1,"one",2,"two",3,"three",4,"four") == "three"
|
||||
* dict(4,1,"one",2,"two",3,"three",4,"four") == "four"
|
||||
* // 无匹配时返回原始值
|
||||
* dict(5,1,"one",2,"two",3,"three",4,"four") == 5
|
||||
* // 无匹配时并且后续参数个数是奇数,则返回最后一个参数
|
||||
* dict(5,1,"one",2,"two",3,"three",4,"four","more") == "more"
|
||||
*
|
||||
* 在翻译中使用
|
||||
* I have { value | dict(1,"one",2,"two",3,"three",4,"four")} apples
|
||||
*
|
||||
* @param {*} value
|
||||
* @param {...any} args
|
||||
* @returns
|
||||
*/
|
||||
function dict(value,...args){
|
||||
try{
|
||||
for(let i=0;i<args.length;i+=2){
|
||||
if(args[i]===value){
|
||||
return args[i+1]
|
||||
}
|
||||
}
|
||||
if(args.length >0 && (args.length % 2!==0)) return args[args.length-1]
|
||||
}catch{}
|
||||
return value
|
||||
}
|
||||
|
||||
|
||||
var formatters$1 = {
|
||||
"*":{
|
||||
$types:{
|
||||
Date:(value)=>value.toLocaleString()
|
||||
},
|
||||
time:(value)=> value.toLocaleTimeString(),
|
||||
date: (value)=> value.toLocaleDateString(),
|
||||
dict, //字典格式化器
|
||||
},
|
||||
cn:{
|
||||
$types:{
|
||||
Date:(value)=> `${value.getFullYear()}年${value.getMonth()+1}月${value.getDate()}日 ${value.getHours()}点${value.getMinutes()}分${value.getSeconds()}秒`
|
||||
},
|
||||
time:(value)=>`${value.getHours()}点${value.getMinutes()}分${value.getSeconds()}秒`,
|
||||
date: (value)=> `${value.getFullYear()}年${value.getMonth()+1}月${value.getDate()}日`,
|
||||
currency:(value)=>`${value}元`,
|
||||
},
|
||||
en:{
|
||||
currency:(value)=>`$${value}`,
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取指定变量类型名称
|
||||
* getDataTypeName(1) == Number
|
||||
* getDataTypeName("") == String
|
||||
* getDataTypeName(null) == Null
|
||||
* getDataTypeName(undefined) == Undefined
|
||||
* getDataTypeName(new Date()) == Date
|
||||
* getDataTypeName(new Error()) == Error
|
||||
*
|
||||
* @param {*} v
|
||||
* @returns
|
||||
*/
|
||||
function getDataTypeName$1(v){
|
||||
if (v === null) return 'Null'
|
||||
if (v === undefined) return 'Undefined'
|
||||
if(typeof(v)==="function") return "Function"
|
||||
return v.constructor && v.constructor.name;
|
||||
}function isPlainObject$1(obj){
|
||||
if (typeof obj !== 'object' || obj === null) return false;
|
||||
var proto = Object.getPrototypeOf(obj);
|
||||
if (proto === null) return true;
|
||||
var baseProto = proto;
|
||||
|
||||
while (Object.getPrototypeOf(baseProto) !== null) {
|
||||
baseProto = Object.getPrototypeOf(baseProto);
|
||||
}
|
||||
return proto === baseProto;
|
||||
}
|
||||
function isNumber$1(value){
|
||||
return !isNaN(parseInt(value))
|
||||
}
|
||||
|
||||
var utils = {
|
||||
getDataTypeName: getDataTypeName$1,
|
||||
isNumber: isNumber$1,
|
||||
isPlainObject: isPlainObject$1
|
||||
};
|
||||
|
||||
const deepMerge = cjs;
|
||||
const formatters = formatters$1;
|
||||
const {isPlainObject ,isNumber , getDataTypeName} = utils;
|
||||
|
||||
// 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter }
|
||||
// 不支持参数: let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*\s*)*)\s*\}/g
|
||||
|
||||
// 支持参数: { var | formatter(x,x,..) | formatter }
|
||||
let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*(\(.*\)){0,1}\s*)*)\s*\}/g;
|
||||
|
||||
// 有效的语言名称列表
|
||||
const languages = ["af","am","ar-dz","ar-iq","ar-kw","ar-ly","ar-ma","ar-sa","ar-tn","ar","az","be","bg","bi","bm","bn","bo","br","bs","ca","cs","cv","cy","da","de-at","de-ch","de","dv","el","en-au","en-ca","en-gb","en-ie","en-il","en-in","en-nz","en-sg","en-tt","en","eo","es-do","es-mx","es-pr","es-us","es","et","eu","fa","fi","fo","fr-ca","fr-ch","fr","fy","ga","gd","gl","gom-latn","gu","he","hi","hr","ht","hu","hy-am","id","is","it-ch","it","ja","jv","ka","kk","km","kn","ko","ku","ky","lb","lo","lt","lv","me","mi","mk","ml","mn","mr","ms-my","ms","mt","my","nb","ne","nl-be","nl","nn","oc-lnc","pa-in","pl","pt-br","pt","ro","ru","rw","sd","se","si","sk","sl","sq","sr-cyrl","sr","ss","sv-fi","sv","sw","ta","te","tet","tg","th","tk","tl-ph","tlh","tr","tzl","tzm-latn","tzm","ug-cn","uk","ur","uz-latn","uz","vi","x-pseudo","yo","zh-cn","zh-hk","zh-tw","zh"];
|
||||
|
||||
/**
|
||||
* 考虑到通过正则表达式进行插件的替换可能较慢,因此提供一个简单方法来过滤掉那些
|
||||
* 不需要进行插值处理的字符串
|
||||
* 原理很简单,就是判断一下是否同时具有{和}字符,如果有则认为可能有插值变量,如果没有则一定没有插件变量,则就不需要进行正则匹配
|
||||
* 从而可以减少不要的正则匹配
|
||||
* 注意:该方法只能快速判断一个字符串不包括插值变量
|
||||
* @param {*} str
|
||||
* @returns {boolean} true=可能包含插值变量,
|
||||
*/
|
||||
function hasInterpolation(str){
|
||||
return str.includes("{") && str.includes("}")
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
通过正则表达式对原始文本内容进行解析匹配后得到的
|
||||
formatters="| aaa(1,1) | bbb "
|
||||
|
||||
需要统一解析为
|
||||
|
||||
[
|
||||
[aaa,[1,1]], // [formatter'name,[args,...]]
|
||||
[bbb,[]],
|
||||
]
|
||||
|
||||
formatters="| aaa(1,1,"dddd") | bbb "
|
||||
|
||||
目前对参数采用简单的split(",")来解析,因为无法正确解析aaa(1,1,"dd,,dd")形式的参数
|
||||
在此场景下基本够用了,如果需要支持更复杂的参数解析,可以后续考虑使用正则表达式来解析
|
||||
|
||||
@returns [[<formatterName>,[<arg>,<arg>,...]]]
|
||||
*/
|
||||
function parseFormatters(formatters){
|
||||
if(!formatters) return []
|
||||
// 1. 先解析为 ["aaa()","bbb"]形式
|
||||
let result = formatters.trim().substr(1).trim().split("|").map(r=>r.trim());
|
||||
|
||||
// 2. 解析格式化器参数
|
||||
return result.map(formatter=>{
|
||||
let firstIndex = formatter.indexOf("(");
|
||||
let lastIndex = formatter.lastIndexOf(")");
|
||||
if(firstIndex!==-1 && lastIndex!==-1){ // 带参数的格式化器
|
||||
const argsContent = formatter.substr(firstIndex+1,lastIndex-firstIndex-1).trim();
|
||||
let args = argsContent=="" ? [] : argsContent.split(",").map(arg=>{
|
||||
arg = arg.trim();
|
||||
if(!isNaN(parseInt(arg))){
|
||||
return parseInt(arg) // 数字
|
||||
}else if((arg.startsWith('\"') && arg.endsWith('\"')) || (arg.startsWith('\'') && arg.endsWith('\'')) ){
|
||||
return arg.substr(1,arg.length-2) // 字符串
|
||||
}else if(arg.toLowerCase()==="true" || arg.toLowerCase()==="false"){
|
||||
return arg.toLowerCase()==="true" // 布尔值
|
||||
}else if((arg.startsWith('{') && arg.endsWith('}')) || (arg.startsWith('[') && arg.endsWith(']'))){
|
||||
try{
|
||||
return JSON.parse(arg)
|
||||
}catch(e){
|
||||
return String(arg)
|
||||
}
|
||||
}else {
|
||||
return String(arg)
|
||||
}
|
||||
});
|
||||
return [formatter.substr(0,firstIndex),args]
|
||||
}else {// 不带参数的格式化器
|
||||
return [formatter,[]]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取字符串中的插值变量
|
||||
* // [
|
||||
// {
|
||||
name:<变量名称>,formatters:[{name:<格式化器名称>,args:[<参数>,<参数>,....]]}],<匹配字符串>],
|
||||
// ....
|
||||
//
|
||||
* @param {*} str
|
||||
* @param {*} isFull =true 保留所有插值变量 =false 进行去重
|
||||
* @returns {Array}
|
||||
* [
|
||||
* {
|
||||
* name:"<变量名称>",
|
||||
* formatters:[
|
||||
* {name:"<格式化器名称>",args:[<参数>,<参数>,....]},
|
||||
* {name:"<格式化器名称>",args:[<参数>,<参数>,....]},
|
||||
* ],
|
||||
* match:"<匹配字符串>"
|
||||
* },
|
||||
* ...
|
||||
* ]
|
||||
*/
|
||||
function getInterpolatedVars(str){
|
||||
let vars = [];
|
||||
forEachInterpolatedVars(str,(varName,formatters,match)=>{
|
||||
let varItem = {
|
||||
name:varName,
|
||||
formatters:formatters.map(([formatter,args])=>{
|
||||
return {
|
||||
name:formatter,
|
||||
args:args
|
||||
}
|
||||
}),
|
||||
match:match
|
||||
};
|
||||
if(vars.findIndex(varDef=>((varDef.name===varItem.name) && (varItem.formatters.toString() == varDef.formatters.toString())))===-1){
|
||||
vars.push(varItem);
|
||||
}
|
||||
return ""
|
||||
});
|
||||
return vars
|
||||
}
|
||||
/**
|
||||
* 遍历str中的所有插值变量传递给callback,将callback返回的结果替换到str中对应的位置
|
||||
* @param {*} str
|
||||
* @param {Function(<变量名称>,[formatters],match[0])} callback
|
||||
* @returns 返回替换后的字符串
|
||||
*/
|
||||
function forEachInterpolatedVars(str,callback,options={}){
|
||||
let result=str, match;
|
||||
let opts = Object.assign({
|
||||
replaceAll:true, // 是否替换所有插值变量,当使用命名插值时应置为true,当使用位置插值时应置为false
|
||||
},options);
|
||||
varWithPipeRegexp.lastIndex=0;
|
||||
while ((match = varWithPipeRegexp.exec(result)) !== null) {
|
||||
const varname = match.groups.varname || "";
|
||||
// 解析格式化器和参数 = [<formatterName>,[<formatterName>,[<arg>,<arg>,...]]]
|
||||
const formatters = parseFormatters(match.groups.formatters);
|
||||
if(typeof(callback)==="function"){
|
||||
try{
|
||||
if(opts.replaceAll){
|
||||
result=result.replaceAll(match[0],callback(varname,formatters,match[0]));
|
||||
}else {
|
||||
result=result.replace(match[0],callback(varname,formatters,match[0]));
|
||||
}
|
||||
}catch{// callback函数可能会抛出异常,如果抛出异常,则中断匹配过程
|
||||
break
|
||||
}
|
||||
}
|
||||
varWithPipeRegexp.lastIndex=0;
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
// 缓存数据类型的格式化器,避免每次都调用getDataTypeDefaultFormatter
|
||||
let datatypeFormattersCache ={
|
||||
$activeLanguage:null,
|
||||
};
|
||||
/**
|
||||
* 取得指定数据类型的默认格式化器
|
||||
*
|
||||
* 可以为每一个数据类型指定一个格式化器,当传入插值变量时,会自动调用该格式化器来对值进行格式化转换
|
||||
|
||||
const formatters = {
|
||||
"*":{
|
||||
$types:{...} // 在所有语言下只作用于特定数据类型的格式化器
|
||||
}, // 在所有语言下生效的格式化器
|
||||
cn:{
|
||||
$types:{
|
||||
[数据类型]:(value)=>{...},
|
||||
},
|
||||
[格式化器名称]:(value)=>{...},
|
||||
[格式化器名称]:(value)=>{...},
|
||||
[格式化器名称]:(value)=>{...},
|
||||
},
|
||||
}
|
||||
* @param {*} scope
|
||||
* @param {*} activeLanguage
|
||||
* @param {*} dataType 数字类型
|
||||
* @returns {Function} 格式化函数
|
||||
*/
|
||||
function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){
|
||||
if(datatypeFormattersCache.$activeLanguage === activeLanguage) {
|
||||
if(dataType in datatypeFormattersCache) return datatypeFormattersCache[dataType]
|
||||
}else {// 清空缓存
|
||||
datatypeFormattersCache = { $activeLanguage:activeLanguage };
|
||||
}
|
||||
|
||||
// 先在当前作用域中查找,再在全局查找
|
||||
const targets = [scope.formatters,scope.global.formatters];
|
||||
for(const target of targets){
|
||||
if(activeLanguage in target){
|
||||
// 在当前语言的$types中查找
|
||||
let formatters = target[activeLanguage].$types || {};
|
||||
for(let [name,formatter] of Object.entries(formatters)){
|
||||
if(name === dataType && typeof(formatter)==="function") {
|
||||
datatypeFormattersCache[dataType] = formatter;
|
||||
return formatter
|
||||
}
|
||||
}
|
||||
}
|
||||
// 在所有语言的$types中查找
|
||||
let formatters = target["*"].$types || {};
|
||||
for(let [name,formatter] of Object.entries(formatters)){
|
||||
if(name === dataType && typeof(formatter)==="function") {
|
||||
datatypeFormattersCache[dataType] = formatter;
|
||||
return formatter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定名称的格式化器函数
|
||||
* @param {*} scope
|
||||
* @param {*} activeLanguage
|
||||
* @param {*} name 格式化器名称
|
||||
* @returns {Function} 格式化函数
|
||||
*/
|
||||
let formattersCache = { $activeLanguage:null};
|
||||
function getFormatter(scope,activeLanguage,name){
|
||||
if(formattersCache.$activeLanguage === activeLanguage) {
|
||||
if(name in formattersCache) return formattersCache[name]
|
||||
}else { // 当切换语言时需要清空缓存
|
||||
formattersCache = { $activeLanguage:activeLanguage };
|
||||
}
|
||||
// 先在当前作用域中查找,再在全局查找
|
||||
const targets = [scope.formatters,scope.global.formatters];
|
||||
for(const target of targets){
|
||||
// 优先在当前语言查找
|
||||
if(activeLanguage in target){
|
||||
let formatters = target[activeLanguage] || {};
|
||||
if((name in formatters) && typeof(formatters[name])==="function") return formattersCache[name] = formatters[name]
|
||||
}
|
||||
// 在所有语言的$types中查找
|
||||
let formatters = target["*"] || {};
|
||||
if((name in formatters) && typeof(formatters[name])==="function") return formattersCache[name] = formatters[name]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行格式化器并返回结果
|
||||
* @param {*} value
|
||||
* @param {*} formatters
|
||||
*/
|
||||
function executeFormatter(value,formatters){
|
||||
if(formatters.length===0) return value
|
||||
let result = value;
|
||||
try{
|
||||
for(let formatter of formatters){
|
||||
if(typeof(formatter) === "function") {
|
||||
result = formatter(result);
|
||||
}else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}catch(e){
|
||||
console.error(`Error while execute i18n formatter for ${value}: ${e.message} ` );
|
||||
}
|
||||
return result
|
||||
}
|
||||
/**
|
||||
* 将 [[格式化器名称,[参数,参数,...]],[格式化器名称,[参数,参数,...]]]格式化器转化为
|
||||
*
|
||||
*
|
||||
*
|
||||
* @param {*} scope
|
||||
* @param {*} activeLanguage
|
||||
* @param {*} formatters
|
||||
*/
|
||||
function buildFormatters(scope,activeLanguage,formatters){
|
||||
let results = [];
|
||||
for(let formatter of formatters){
|
||||
if(formatter[0]){
|
||||
const func = getFormatter(scope,activeLanguage,formatter[0]);
|
||||
if(typeof(func)==="function"){
|
||||
results.push((v)=>{
|
||||
return func(v,...formatter[1])
|
||||
});
|
||||
}else {// 格式化器无效或者没有定义时,查看当前值是否具有同名的方法,如果有则执行调用
|
||||
results.push((v)=>{
|
||||
if(typeof(v[formatter[0]])==="function"){
|
||||
return v[formatter[0]].call(v,...formatter[1])
|
||||
}else {
|
||||
return v
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 将value经过格式化器处理后返回
|
||||
* @param {*} scope
|
||||
* @param {*} activeLanguage
|
||||
* @param {*} formatters
|
||||
* @param {*} value
|
||||
* @returns
|
||||
*/
|
||||
function getFormattedValue(scope,activeLanguage,formatters,value){
|
||||
// 1. 取得格式化器函数列表
|
||||
const formatterFuncs = buildFormatters(scope,activeLanguage,formatters);
|
||||
// 3. 查找每种数据类型默认格式化器,并添加到formatters最前面,默认数据类型格式化器优先级最高
|
||||
const defaultFormatter = getDataTypeDefaultFormatter(scope,activeLanguage,getDataTypeName(value));
|
||||
if(defaultFormatter){
|
||||
formatterFuncs.splice(0,0,defaultFormatter);
|
||||
}
|
||||
// 3. 执行格式化器
|
||||
value = executeFormatter(value,formatterFuncs);
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串可以进行变量插值替换,
|
||||
* replaceInterpolatedVars("<模板字符串>",{变量名称:变量值,变量名称:变量值,...})
|
||||
* replaceInterpolatedVars("<模板字符串>",[变量值,变量值,...])
|
||||
* replaceInterpolatedVars("<模板字符串>",变量值,变量值,...])
|
||||
*
|
||||
- 当只有两个参数并且第2个参数是{}时,将第2个参数视为命名变量的字典
|
||||
replaceInterpolatedVars("this is {a}+{b},{a:1,b:2}) --> this is 1+2
|
||||
- 当只有两个参数并且第2个参数是[]时,将第2个参数视为位置参数
|
||||
replaceInterpolatedVars"this is {}+{}",[1,2]) --> this is 1+2
|
||||
- 普通位置参数替换
|
||||
replaceInterpolatedVars("this is {a}+{b}",1,2) --> this is 1+2
|
||||
-
|
||||
this == scope == { formatters: {}, ... }
|
||||
* @param {*} template
|
||||
* @returns
|
||||
*/
|
||||
function replaceInterpolatedVars(template,...args) {
|
||||
const scope = this;
|
||||
// 当前激活语言
|
||||
const activeLanguage = scope.global.activeLanguage;
|
||||
|
||||
// 没有变量插值则的返回原字符串
|
||||
if(args.length===0 || !hasInterpolation(template)) return template
|
||||
|
||||
// ****************************变量插值****************************
|
||||
if(args.length===1 && isPlainObject(args[0])){
|
||||
// 读取模板字符串中的插值变量列表
|
||||
// [[var1,[formatter,formatter,...],match],[var2,[formatter,formatter,...],match],...}
|
||||
let varValues = args[0];
|
||||
return forEachInterpolatedVars(template,(varname,formatters)=>{
|
||||
let value = (varname in varValues) ? varValues[varname] : '';
|
||||
return getFormattedValue(scope,activeLanguage,formatters,value)
|
||||
})
|
||||
}else {
|
||||
// ****************************位置插值****************************
|
||||
// 如果只有一个Array参数,则认为是位置变量列表,进行展开
|
||||
const params=(args.length===1 && Array.isArray(args[0])) ? [...args[0]] : args;
|
||||
if(params.length===0) return template // 没有变量则不需要进行插值处理,返回原字符串
|
||||
let i = 0;
|
||||
return forEachInterpolatedVars(template,(varname,formatters)=>{
|
||||
if(params.length>i){
|
||||
return getFormattedValue(scope,activeLanguage,formatters,params[i++])
|
||||
}else {
|
||||
throw new Error() // 抛出异常,停止插值处理
|
||||
}
|
||||
},{replaceAll:false})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 默认语言配置
|
||||
const defaultLanguageSettings = {
|
||||
defaultLanguage: "cn",
|
||||
activeLanguage: "cn",
|
||||
languages:{
|
||||
cn:{name:"cn",title:"中文",default:true},
|
||||
en:{name:"en",title:"英文"},
|
||||
},
|
||||
formatters
|
||||
};
|
||||
|
||||
function isMessageId(content){
|
||||
return parseInt(content)>0
|
||||
}
|
||||
/**
|
||||
* 根据值的单数和复数形式,从messages中取得相应的消息
|
||||
*
|
||||
* @param {*} messages 复数形式的文本内容 = [<=0时的内容>,<=1时的内容>,<=2时的内容>,...]
|
||||
* @param {*} value
|
||||
*/
|
||||
function getPluraMessage(messages,value){
|
||||
try{
|
||||
if(Array.isArray(messages)){
|
||||
return messages.length > value ? messages[value] : messages[messages.length-1]
|
||||
}else {
|
||||
return messages
|
||||
}
|
||||
}catch{
|
||||
return Array.isArray(messages) ? messages[0] : messages
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译函数
|
||||
*
|
||||
* translate("要翻译的文本内容") 如果默认语言是中文,则不会进行翻译直接返回
|
||||
* translate("I am {} {}","man") == I am man 位置插值
|
||||
* translate("I am {p}",{p:"man"}) 字典插值
|
||||
* translate("total {$count} items", {$count:1}) //复数形式
|
||||
* translate("total {} {} {} items",a,b,c) // 位置变量插值
|
||||
*
|
||||
* this===scope 当前绑定的scope
|
||||
*
|
||||
*/
|
||||
function translate(message) {
|
||||
const scope = this;
|
||||
const activeLanguage = scope.global.activeLanguage;
|
||||
let content = message;
|
||||
let vars=[]; // 插值变量列表
|
||||
let pluralVars= []; // 复数变量
|
||||
let pluraValue = null; // 复数值
|
||||
try{
|
||||
// 1. 预处理变量: 复数变量保存至pluralVars中 , 变量如果是Function则调用
|
||||
if(arguments.length === 2 && isPlainObject(arguments[1])){
|
||||
Object.entries(arguments[1]).forEach(([name,value])=>{
|
||||
if(typeof(value)==="function"){
|
||||
try{
|
||||
vars[name] = value();
|
||||
}catch(e){
|
||||
vars[name] = value;
|
||||
}
|
||||
}
|
||||
// 以$开头的视为复数变量
|
||||
if(name.startsWith("$")) pluralVars.push(name);
|
||||
});
|
||||
vars = [arguments[1]];
|
||||
}else if(arguments.length >= 2){
|
||||
vars = [...arguments].splice(1).map((arg,index)=>{
|
||||
try{
|
||||
arg = typeof(arg)==="function" ? arg() : arg;
|
||||
// 位置参数中以第一个数值变量为复数变量
|
||||
if(isNumber(arg)) pluraValue = parseInt(arg);
|
||||
}catch(e){ }
|
||||
return arg
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// 2. 取得翻译文本模板字符串
|
||||
if(activeLanguage === scope.defaultLanguage){
|
||||
// 2.1 从默认语言中取得翻译文本模板字符串
|
||||
// 如果当前语言就是默认语言,不需要查询加载,只需要做插值变换即可
|
||||
// 当源文件运用了babel插件后会将原始文本内容转换为msgId
|
||||
// 如果是msgId则从scope.default中读取,scope.default=默认语言包={<id>:<message>}
|
||||
if(isMessageId(content)){
|
||||
content = scope.default[content] || message;
|
||||
}
|
||||
}else {
|
||||
// 2.2 从当前语言包中取得翻译文本模板字符串
|
||||
// 如果没有启用babel插件将源文本转换为msgId,需要先将文本内容转换为msgId
|
||||
let msgId = isMessageId(content) ? content : scope.idMap[content];
|
||||
content = scope.messages[msgId] || content;
|
||||
}
|
||||
|
||||
// 3. 处理复数
|
||||
// 经过上面的处理,content可能是字符串或者数组
|
||||
// content = "原始文本内容" || 复数形式["原始文本内容","原始文本内容"....]
|
||||
// 如果是数组说明要启用复数机制,需要根据插值变量中的某个变量来判断复数形式
|
||||
if(Array.isArray(content) && content.length>0){
|
||||
// 如果存在复数命名变量,只取第一个复数变量
|
||||
if(pluraValue!==null){ // 启用的是位置插值,pluraIndex=第一个数字变量的位置
|
||||
content = getPluraMessage(content,pluraValue);
|
||||
}else if(pluralVar.length>0){
|
||||
content = getPluraMessage(content,parseInt(vars(pluralVar[0])));
|
||||
}else { // 如果找不到复数变量,则使用第一个内容
|
||||
content = content[0];
|
||||
}
|
||||
}
|
||||
// 进行插值处理
|
||||
if(vars.length==0){
|
||||
return content
|
||||
}else {
|
||||
return replaceInterpolatedVars.call(scope,content,...vars)
|
||||
}
|
||||
}catch(e){
|
||||
return content // 出错则返回原始文本
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多语言管理类
|
||||
*
|
||||
* 当导入编译后的多语言文件时(import("./languages")),会自动生成全局实例VoerkaI18n
|
||||
*
|
||||
* VoerkaI18n.languages // 返回支持的语言列表
|
||||
* VoerkaI18n.defaultLanguage // 默认语言
|
||||
* VoerkaI18n.language // 当前语言
|
||||
* VoerkaI18n.change(language) // 切换到新的语言
|
||||
*
|
||||
*
|
||||
* VoerkaI18n.on("change",(language)=>{}) // 注册语言切换事件
|
||||
* VoerkaI18n.off("change",(language)=>{})
|
||||
*
|
||||
* */
|
||||
class I18nManager{
|
||||
static instance = null; // 单例引用
|
||||
callbacks = [] // 当切换语言时的回调事件
|
||||
constructor(settings={}){
|
||||
if(I18nManager.instance!=null){
|
||||
return I18nManager.instance;
|
||||
}
|
||||
I18nManager.instance = this;
|
||||
this._settings = deepMerge(defaultLanguageSettings,settings);
|
||||
this._scopes=[];
|
||||
return I18nManager.instance;
|
||||
}
|
||||
get settings(){ return this._settings }
|
||||
get scopes(){ return this._scopes }
|
||||
// 当前激活语言
|
||||
get activeLanguage(){ return this._settings.activeLanguage}
|
||||
// 默认语言
|
||||
get defaultLanguage(){ return this.this._settings.defaultLanguage}
|
||||
// 支持的语言列表
|
||||
get languages(){ return this._settings.languages}
|
||||
// 订阅语言切换事件
|
||||
on(callback){
|
||||
this.callbacks.push(callback);
|
||||
}
|
||||
off(callback){
|
||||
for(let i=0;i<this.callbacks.length;i++){
|
||||
if(this.callbacks[i]===callback ){
|
||||
this.callbacks.splice(i,1);
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
offAll(){
|
||||
this.callbacks=[];
|
||||
}
|
||||
/**
|
||||
* 切换语言时触发语言切换事件回调
|
||||
*/
|
||||
async _triggerChangeEvents(newLanguage){
|
||||
try{
|
||||
await this._updateScopes(newLanguage);
|
||||
await (Promise.allSettled || Promise.all)(this.callbacks.map(async cb=>await cb(newLanguage)));
|
||||
}catch(e){
|
||||
console.warn("Error while executing language change events",e.message);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 切换语言
|
||||
*/
|
||||
async change(value){
|
||||
if(value in this.languages){
|
||||
await this._triggerChangeEvents(value);
|
||||
this._settings.activeLanguage = value;
|
||||
}else {
|
||||
throw new Error("Not supported language:"+value)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取指定作用域的下的语言包加载器
|
||||
*
|
||||
* 同时会进行语言兼容性处理
|
||||
*
|
||||
* 如scope里面定义了一个cn的语言包,当切换到zh-cn时,会自动加载cn语言包
|
||||
*
|
||||
*
|
||||
* @param {*} scope
|
||||
* @param {*} lang
|
||||
*/
|
||||
_getScopeLoader(scope,lang){
|
||||
|
||||
}
|
||||
/**
|
||||
* 当切换语言时调用此方法来加载更新语言包
|
||||
* @param {*} newLanguage
|
||||
*/
|
||||
async _updateScopes(newLanguage){
|
||||
// 并发执行所有作用域语言包的加载
|
||||
try{
|
||||
await (Promise.allSettled || Promise.all)(this._scopes.map(scope=>{
|
||||
return async ()=>{
|
||||
// 默认语言,所有均默认语言均采用静态加载方式,只需要简单的替换即可
|
||||
if(newLanguage === scope.defaultLanguage){
|
||||
scope.messages = scope.default;
|
||||
return
|
||||
}
|
||||
// 异步加载语言文件
|
||||
const loader = scope.loaders[newLanguage];
|
||||
if(typeof(loader) === "function"){
|
||||
try{
|
||||
scope.messages = await loader();
|
||||
}catch(e){
|
||||
console.warn(`Error loading language ${newLanguage} : ${e.message}`);
|
||||
scope.messages = defaultMessages; // 出错时回退到默认语言
|
||||
}
|
||||
}else {
|
||||
scope.messages = defaultMessages;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}catch(e){
|
||||
console.warn("Error while refreshing scope:",e.message);
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* 注册一个新的作用域
|
||||
*
|
||||
* 每一个库均对应一个作用域,每个作用域可以有多个语言包,且对应一个翻译函数
|
||||
* scope={
|
||||
* defaultLanguage:"cn",
|
||||
default: defaultMessages, // 转换文本信息
|
||||
messages : defaultMessages, // 当前语言的消息
|
||||
idMap:messageIds,
|
||||
formatters:{
|
||||
...formatters,
|
||||
...i18nSettings.formatters || {}
|
||||
},
|
||||
loaders:{}, // 异步加载语言文件的函数列表
|
||||
settings:{} // 引用全局VoerkaI18n实例的配置
|
||||
* }
|
||||
*
|
||||
* 除了默认语言外,其他语言采用动态加载的方式
|
||||
*
|
||||
* @param {*} scope
|
||||
*/
|
||||
register(scope){
|
||||
scope.global = this._settings;
|
||||
this._scopes.push(scope);
|
||||
}
|
||||
/**
|
||||
* 注册全局格式化器
|
||||
* @param {*} formatters
|
||||
*/
|
||||
registerFormatters(formatters){
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var runtime ={
|
||||
getInterpolatedVars,
|
||||
replaceInterpolatedVars,
|
||||
I18nManager,
|
||||
translate,
|
||||
languages,
|
||||
defaultLanguageSettings
|
||||
};
|
||||
|
||||
export { runtime as default };
|
@ -2,17 +2,27 @@
|
||||
"name": "@voerkai18n/runtime",
|
||||
"version": "1.0.0",
|
||||
"description": "Voerkai18n Runtime",
|
||||
"main": "index.js",
|
||||
"module": "index.esm.js",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "dist/index.esm.js",
|
||||
"homepage": "https://gitee.com/zhangfisher/voerka-i18n",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://gitee.com/zhangfisher/voerka-i18n.git"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "rollup -c"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"exports":{
|
||||
"import":"./dist/index.esm.js" ,
|
||||
"require":"./dist/index.cjs"
|
||||
},
|
||||
"author": "wxzhang",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-node-resolve": "^13.1.3",
|
||||
"deepmerge": "^4.2.2",
|
||||
"rollup": "^2.69.0",
|
||||
"rollup-plugin-terser": "^7.0.2"
|
||||
}
|
||||
}
|
||||
|
3
packages/runtime/readme.md
Normal file
3
packages/runtime/readme.md
Normal file
@ -0,0 +1,3 @@
|
||||
# @voerkai18n/runtime
|
||||
|
||||
`voerkai18n`运行时依赖
|
@ -10,11 +10,11 @@ export default [
|
||||
input: './index.js',
|
||||
output: [
|
||||
{
|
||||
file: 'index.esm.js',
|
||||
file: 'dist/index.esm.js',
|
||||
format:"esm"
|
||||
},
|
||||
{
|
||||
file: 'index.cjs',
|
||||
file: 'dist/index.cjs',
|
||||
exports:"default",
|
||||
format:"cjs"
|
||||
}
|
||||
@ -23,8 +23,8 @@ export default [
|
||||
resolve(),
|
||||
commonjs(),
|
||||
clear({targets:["dist"]}),
|
||||
//terser()
|
||||
terser()
|
||||
],
|
||||
//external:["@babel/runtime"]
|
||||
external:["@babel/runtime"]
|
||||
}
|
||||
]
|
108
pnpm-lock.yaml
generated
108
pnpm-lock.yaml
generated
@ -11,10 +11,12 @@ importers:
|
||||
'@rollup/plugin-commonjs': ^21.0.2
|
||||
dayjs: ^1.10.8
|
||||
deepmerge: ^4.2.2
|
||||
fs-extra: ^10.0.1
|
||||
gulp: ^4.0.2
|
||||
jest: ^27.5.1
|
||||
rollup: ^2.69.0
|
||||
rollup-plugin-clear: ^2.0.7
|
||||
shelljs: ^0.8.5
|
||||
vinyl: ^2.2.1
|
||||
devDependencies:
|
||||
'@babel/core': 7.17.5
|
||||
@ -24,34 +26,48 @@ importers:
|
||||
'@rollup/plugin-commonjs': 21.0.2_rollup@2.69.0
|
||||
dayjs: 1.10.8
|
||||
deepmerge: 4.2.2
|
||||
fs-extra: 10.0.1
|
||||
gulp: 4.0.2
|
||||
jest: 27.5.1
|
||||
rollup: 2.69.0
|
||||
rollup-plugin-clear: 2.0.7
|
||||
shelljs: 0.8.5
|
||||
vinyl: 2.2.1
|
||||
|
||||
packages/demo:
|
||||
packages/babel:
|
||||
specifiers: {}
|
||||
|
||||
packages/cli:
|
||||
specifiers:
|
||||
'@babel/cli': ^7.17.6
|
||||
'@babel/core': ^7.17.5
|
||||
'@voerkai18n/runtime': workspace:^1.0.0
|
||||
'@voerkai18n/tools': workspace:^1.0.0
|
||||
art-template: ^4.13.2
|
||||
commander: ^9.0.0
|
||||
cross-env: ^7.0.3
|
||||
deepmerge: ^4.2.2
|
||||
glob: ^7.2.0
|
||||
gulp: ^4.0.2
|
||||
logsets: ^1.0.8
|
||||
readjson: ^2.2.2
|
||||
through2: ^4.0.2
|
||||
vinyl: ^2.2.1
|
||||
dependencies:
|
||||
'@babel/cli': 7.17.6_@babel+core@7.17.5
|
||||
'@babel/core': 7.17.5
|
||||
'@voerkai18n/runtime': link:../runtime
|
||||
'@voerkai18n/tools': link:../tools
|
||||
devDependencies:
|
||||
art-template: 4.13.2
|
||||
commander: 9.0.0
|
||||
cross-env: 7.0.3
|
||||
deepmerge: 4.2.2
|
||||
glob: 7.2.0
|
||||
gulp: 4.0.2
|
||||
logsets: 1.0.8
|
||||
readjson: 2.2.2
|
||||
through2: 4.0.2
|
||||
vinyl: 2.2.1
|
||||
|
||||
packages/demo/apps/app:
|
||||
specifiers:
|
||||
'@voerkai18n/tools': workspace:^1.0.0
|
||||
devDependencies:
|
||||
'@voerkai18n/tools': link:../../../tools
|
||||
|
||||
packages/demo/apps/app/languages:
|
||||
packages/cli/languages:
|
||||
specifiers: {}
|
||||
|
||||
packages/formatters:
|
||||
@ -78,43 +94,14 @@ importers:
|
||||
specifiers:
|
||||
'@rollup/plugin-node-resolve': ^13.1.3
|
||||
deepmerge: ^4.2.2
|
||||
rollup: ^2.69.0
|
||||
rollup-plugin-terser: ^7.0.2
|
||||
devDependencies:
|
||||
'@rollup/plugin-node-resolve': 13.1.3_rollup@2.69.0
|
||||
deepmerge: 4.2.2
|
||||
rollup: 2.69.0
|
||||
rollup-plugin-terser: 7.0.2_rollup@2.69.0
|
||||
|
||||
packages/tools:
|
||||
specifiers:
|
||||
'@babel/cli': ^7.17.6
|
||||
'@babel/core': ^7.17.5
|
||||
'@voerkai18n/runtime': workspace:^1.0.0
|
||||
art-template: ^4.13.2
|
||||
commander: ^9.0.0
|
||||
cross-env: ^7.0.3
|
||||
deepmerge: ^4.2.2
|
||||
glob: ^7.2.0
|
||||
gulp: ^4.0.2
|
||||
logsets: ^1.0.8
|
||||
readjson: ^2.2.2
|
||||
through2: ^4.0.2
|
||||
vinyl: ^2.2.1
|
||||
dependencies:
|
||||
'@babel/cli': 7.17.6_@babel+core@7.17.5
|
||||
'@babel/core': 7.17.5
|
||||
'@voerkai18n/runtime': link:../runtime
|
||||
art-template: 4.13.2
|
||||
commander: 9.0.0
|
||||
deepmerge: 4.2.2
|
||||
glob: 7.2.0
|
||||
gulp: 4.0.2
|
||||
logsets: 1.0.8
|
||||
readjson: 2.2.2
|
||||
through2: 4.0.2
|
||||
vinyl: 2.2.1
|
||||
devDependencies:
|
||||
cross-env: 7.0.3
|
||||
|
||||
packages/vue:
|
||||
specifiers:
|
||||
deepmerge: ^4.2.2
|
||||
@ -2433,7 +2420,7 @@ packages:
|
||||
hasBin: true
|
||||
dependencies:
|
||||
cross-spawn: 7.0.3
|
||||
dev: true
|
||||
dev: false
|
||||
|
||||
/cross-spawn/7.0.3:
|
||||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
||||
@ -2442,7 +2429,6 @@ packages:
|
||||
path-key: 3.1.1
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
dev: true
|
||||
|
||||
/cssom/0.3.8:
|
||||
resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==}
|
||||
@ -2913,6 +2899,15 @@ packages:
|
||||
dependencies:
|
||||
map-cache: 0.2.2
|
||||
|
||||
/fs-extra/10.0.1:
|
||||
resolution: {integrity: sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
graceful-fs: 4.2.9
|
||||
jsonfile: 6.1.0
|
||||
universalify: 2.0.0
|
||||
dev: true
|
||||
|
||||
/fs-mkdirp-stream/1.0.0:
|
||||
resolution: {integrity: sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes=}
|
||||
engines: {node: '>= 0.10'}
|
||||
@ -4070,6 +4065,14 @@ packages:
|
||||
dependencies:
|
||||
minimist: 1.2.5
|
||||
|
||||
/jsonfile/6.1.0:
|
||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||
dependencies:
|
||||
universalify: 2.0.0
|
||||
optionalDependencies:
|
||||
graceful-fs: 4.2.9
|
||||
dev: true
|
||||
|
||||
/just-debounce/1.1.0:
|
||||
resolution: {integrity: sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ==}
|
||||
|
||||
@ -4583,7 +4586,6 @@ packages:
|
||||
/path-key/3.1.1:
|
||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/path-parse/1.0.7:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
@ -5032,11 +5034,19 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
dev: true
|
||||
|
||||
/shebang-regex/3.0.0:
|
||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
/shelljs/0.8.5:
|
||||
resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==}
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
glob: 7.2.0
|
||||
interpret: 1.4.0
|
||||
rechoir: 0.6.2
|
||||
dev: true
|
||||
|
||||
/signal-exit/3.0.7:
|
||||
@ -5513,6 +5523,11 @@ packages:
|
||||
engines: {node: '>= 4.0.0'}
|
||||
dev: true
|
||||
|
||||
/universalify/2.0.0:
|
||||
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
dev: true
|
||||
|
||||
/unset-value/1.0.0:
|
||||
resolution: {integrity: sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -5672,7 +5687,6 @@ packages:
|
||||
hasBin: true
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
dev: true
|
||||
|
||||
/word-wrap/1.2.3:
|
||||
resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==}
|
||||
|
81
readme.md
81
readme.md
@ -44,7 +44,7 @@
|
||||
|
||||
- **@voerkai18/runtime**
|
||||
|
||||
必须的运行时,安装到运行依赖`dependencies`中
|
||||
必须的运行时(大概12K),安装到运行依赖`dependencies`中
|
||||
|
||||
```javascript
|
||||
npm install --save @voerkai18/runtime
|
||||
@ -52,19 +52,23 @@
|
||||
pnpm add @voerkai18/runtime
|
||||
```
|
||||
|
||||
- **@voerkai18/tools**
|
||||
- **@voerkai18/cli**
|
||||
|
||||
包含文本提取/编译等命行工具,应该安装到开发依赖`devDependencies`中
|
||||
包含文本提取/编译等命令行工具,应该安装到开发依赖`devDependencies`中
|
||||
|
||||
```javascript
|
||||
npm install --save-dev @voerkai18/tools
|
||||
yarn add -D @voerkai18/tools
|
||||
pnpm add -D @voerkai18/tools
|
||||
npm install --save-dev @voerkai18/cli
|
||||
yarn add -D @voerkai18/cli
|
||||
pnpm add -D @voerkai18/cli
|
||||
```
|
||||
|
||||
- **@voerkai18/formatters**
|
||||
|
||||
可选的,一些额外的格式化器,可以按需进行安装到`dependencies`中,用来扩展翻译时对插值变量的额外处理。
|
||||
|
||||
- **@voerkai18/babel**
|
||||
|
||||
可选的`babel`插件,用来实现自动导入翻译函数和翻译文本映射自动替换。
|
||||
|
||||
|
||||
# 快速入门
|
||||
@ -537,11 +541,17 @@ t("{value | uppercase}",3) // == 3
|
||||
|
||||
定义在`@voerkai18n/runtime`里面的格式化器则全局有效,在所有场合均可以使用,但是其优先级低于作用域内的同名格式化器。目前内置的格式化器有:
|
||||
|
||||
| 名称 | | 说明 |
|
||||
| ---- | ---- | ---- |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
|
||||
### 扩展格式化器
|
||||
|
||||
除了可以在当前项目`languages/formatters.js`自定义格式化器和`@voerkai18n/runtime`里面的全局格式化器外,在`@voerkai18n/formatters`中包含了更多的格式化器。
|
||||
除了可以在当前项目`languages/formatters.js`自定义格式化器和`@voerkai18n/runtime`里面的全局格式化器外,单列了`@voerkai18n/formatters`项目用来包含了更多的格式化器。
|
||||
|
||||
作为开源项目,欢迎大家提交贡献更多的格式化器。`@voerkai18n/formatters`
|
||||
作为开源项目,欢迎大家提交贡献更多的格式化器。
|
||||
|
||||
## 日期时间
|
||||
|
||||
@ -626,28 +636,22 @@ t("我有{}辆车",100) // == "I have 100 cars"
|
||||
|
||||
当启用复数功能时,`t`函数需要知道根据哪个变量来决定采用何种复数形式。
|
||||
|
||||
**当采用位置变量插值时,`t`函数取第一个数字类型参数作为位置插值复数。**
|
||||
|
||||
|
||||
```javascript
|
||||
t("{}有{}辆车","张三",0)
|
||||
```
|
||||
|
||||
**当采用命名变量插值时,`t`函数约定当插值字典中存在以`$字符开头`的变量时,并且值是`数字`时,根据该变量来引用复数。**
|
||||
|
||||
下例中,`t`函数根据`$count`值来处理复数。
|
||||
|
||||
以上采用位置插值变量时只能处理第一个位置插值复数,当翻译内容存在多个位置插值变量时,因为无法获取哪一个位置变量是数字,因此就不能有效处理。如:
|
||||
```javascript
|
||||
t("{}有{}辆车","张三",0)
|
||||
t("{}有{}辆车","张三",1)
|
||||
```
|
||||
此种情况下就需要采用命名插值变量来处理。
|
||||
具体的方式是约定当插值字典中存在以`$字符开头`的变量时,并且值是`数字`时,根据该变量来引用复数。以下例中,`t`函数根据`$count`值来处理复数。
|
||||
```javascript
|
||||
t("{name}有{$count}辆车",{name:"Tom",$count:0}) // == "Tom don't have a car"
|
||||
t("{name}有{$count}辆车",{name:"Tom",$count:1}) // == "Tom have a car"
|
||||
t("{name}有{$count}辆车",{name:"Tom",$count:2}) // == "Tom have two cars"
|
||||
t("{name}有{$count}辆车",{name:"Tom",$count:100}) // == "Tom have 100 cars"
|
||||
t("{name}有{$count}辆车",{name:"张三",$count:1})
|
||||
```
|
||||
|
||||
|
||||
### 示例
|
||||
- **示例**
|
||||
|
||||
```javascript
|
||||
// languages/translates/default.json
|
||||
@ -658,7 +662,7 @@ t("{name}有{$count}辆车",{name:"Tom",$count:100}) // == "Tom have 100 cars"
|
||||
"Chapter Five","Chapter Six","Chapter Seven","Chapter Eight","Chapter Nine",
|
||||
"Chapter {}"
|
||||
],
|
||||
cn:["第零章","第一章", "第二章", "第三章","第四章","第五章","第六章","第七章","第八章","第九章"]
|
||||
cn:["起始","第一章", "第二章", "第三章","第四章","第五章","第六章","第七章","第八章","第九章",“第{}章”]
|
||||
}
|
||||
}
|
||||
// 翻译函数
|
||||
@ -671,6 +675,7 @@ t("第{}章",5) // == Chapter Five
|
||||
t("第{}章",6) // == Chapter Six
|
||||
t("第{}章",7) // == Chapter Seven
|
||||
...
|
||||
// 超过取最后一项
|
||||
t("第{}章",100) // == Chapter 100
|
||||
```
|
||||
|
||||
@ -775,7 +780,7 @@ import { t } from "../../../languages"
|
||||
- 在`babel.config.js`中配置插件
|
||||
|
||||
```javascript
|
||||
const i18nPlugin = require("@voerkai18n/tools/babel-plugin-voerkai18n")
|
||||
const i18nPlugin = require("@voerkai18n/babel")
|
||||
module.expors = {
|
||||
plugins: [
|
||||
[
|
||||
@ -920,16 +925,34 @@ const scope = new i18nScope({
|
||||
})
|
||||
```
|
||||
|
||||
## babel插件
|
||||
|
||||
全局安装`@voerkai18n/babel`插件用来进行自动导入t函数和自动文本映射。
|
||||
|
||||
```javascript
|
||||
> npm install -g @voerkai18n/babel
|
||||
> yarn global add @voerkai18n/babel
|
||||
> pnpm add -g @voerkai18n/babel
|
||||
```
|
||||
|
||||
然后在`babel.config.js`中使用,详见上节`自动导入翻译函数`介绍。
|
||||
|
||||
|
||||
|
||||
## VUE扩展
|
||||
|
||||
## React扩展
|
||||
|
||||
|
||||
|
||||
# 命令行
|
||||
|
||||
全局安装`@voerkai18n/tools`工具。
|
||||
全局安装`@voerkai18n/cli`工具。
|
||||
|
||||
```javascript
|
||||
> npm install -g @voerkai18n/tools
|
||||
> yarn global add @voerkai18n/tools
|
||||
> pnpm add -g @voerkai18n/tools
|
||||
> npm install -g @voerkai18n/cli
|
||||
> yarn global add @voerkai18n/cli
|
||||
> pnpm add -g @voerkai18n/cli
|
||||
```
|
||||
|
||||
然后就可以执行:
|
||||
@ -966,6 +989,8 @@ Options:
|
||||
-r, --reset 重新生成当前项目的语言配置
|
||||
-m, --moduleType [type] 生成的js模块类型,取值auto,esm,cjs (default: "auto")
|
||||
-lngs, --languages <languages...> 支持的语言列表 (default: ["cn","en"])
|
||||
-default, --defaultLanguage 默认语言
|
||||
-active, --activeLanguage 激活语言
|
||||
-h, --help display help for command
|
||||
```
|
||||
|
||||
@ -975,7 +1000,7 @@ Options:
|
||||
|
||||
```javascript
|
||||
//- `lngs`参数用来指定拟支持的语言名称列表
|
||||
> voerkai18n init -lngs cn,en,jp,de
|
||||
> voerkai18n init . -lngs cn en jp de -default cn
|
||||
```
|
||||
|
||||
运行`voerkai18n init`命令后,会在当前工程中创建相应配置文件。
|
||||
|
@ -1,11 +0,0 @@
|
||||
/**
|
||||
* 测试demp app的语言运行环境
|
||||
*/
|
||||
const i18n = require('../packages/tools/languages');
|
||||
const path = require("path")
|
||||
|
||||
|
||||
|
||||
test("导入多语言包",done=>{
|
||||
expect(t).toBeFunction()
|
||||
})
|
@ -10,7 +10,7 @@
|
||||
const babel = require("@babel/core");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const i18nPlugin = require("../packages/tools/babel-plugin-voerkai18n");
|
||||
const i18nPlugin = require("../packages/babel");
|
||||
|
||||
const code = `
|
||||
function test(a,b){
|
||||
|
163
test/cli.test.js
Normal file
163
test/cli.test.js
Normal file
@ -0,0 +1,163 @@
|
||||
/**
|
||||
* 测试demp app的语言运行环境
|
||||
*/
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs-extra");
|
||||
const shelljs = require("shelljs");
|
||||
|
||||
const APP_FOLDER = path.join(__dirname, "../demo/apps/app");
|
||||
const LANGUAGE_FOLDER = path.join(APP_FOLDER, "languages");
|
||||
const TRANSLATES_FOLDER = path.join(LANGUAGE_FOLDER, "translates");
|
||||
|
||||
const CLI_INDEX_FILE = path.join(__dirname, "../cli/index.js");
|
||||
|
||||
let SUPPORTED_LANGUAGES = ["cn", "en"];
|
||||
let DEFAULT_LANGUAGE = "cn"
|
||||
let ACTIVE_LANGUAGE = "cn"
|
||||
let MODULE_TYPE = "module"
|
||||
|
||||
|
||||
|
||||
async function importModule(url,onlyDefault=true) {
|
||||
try{
|
||||
return require(url)
|
||||
}catch(e){
|
||||
const result = await import(`file:///${url}`)
|
||||
return onlyDefault ? result.default : result
|
||||
}
|
||||
}
|
||||
|
||||
function createAppIndexFile(){
|
||||
fs.writeFileSync(path.join(APP_FOLDER, "index.js"), `
|
||||
t("a")
|
||||
t("b")
|
||||
t("c")
|
||||
t("d")
|
||||
t("e")
|
||||
`)
|
||||
}
|
||||
|
||||
// 重置演示应用
|
||||
function resetDemoApp(){
|
||||
fs.removeSync(LANGUAGE_FOLDER)
|
||||
}
|
||||
// 清除提取结果
|
||||
function clearExtractResults(){
|
||||
fs.emptyDirSync(path.join(LANGUAGE_FOLDER, "translates"))
|
||||
}
|
||||
// 清除编译结果
|
||||
function clearCompileResults(){
|
||||
fs.removeSync(path.join(LANGUAGE_FOLDER, "package.json"))
|
||||
fs.removeSync(path.join(LANGUAGE_FOLDER, "index.js"))
|
||||
fs.removeSync(path.join(LANGUAGE_FOLDER, "idMap.js"))
|
||||
fs.removeSync(path.join(LANGUAGE_FOLDER, "formatters.js"))
|
||||
fs.removeSync(path.join(LANGUAGE_FOLDER, "cn.js"))
|
||||
fs.removeSync(path.join(LANGUAGE_FOLDER, "en.js"))
|
||||
}
|
||||
|
||||
// 更新主工程的package.json文件
|
||||
function updateProjectPackageJson(pkg={}){
|
||||
pkg = Object.assign({type:MODULE_TYPE}, pkg)
|
||||
fs.writeJsonSync(path.join(APP_FOLDER, "package.json"), pkg)
|
||||
}
|
||||
|
||||
function initCommonjsApp(){
|
||||
shelljs.cd(APP_FOLDER);
|
||||
resetDemoApp()
|
||||
updateProjectPackageJson({type:"commonjs"})
|
||||
shelljs.exec(`node ${CLI_INDEX_FILE} init . -lngs ${SUPPORTED_LANGUAGES.join(" ")} -default ${DEFAULT_LANGUAGE} -active ${ACTIVE_LANGUAGE}`).code
|
||||
}
|
||||
|
||||
function initESMApp(){
|
||||
shelljs.cd(APP_FOLDER);
|
||||
resetDemoApp()
|
||||
updateProjectPackageJson({type:"module"})
|
||||
shelljs.exec(`node ${CLI_INDEX_FILE} init . -lngs ${SUPPORTED_LANGUAGES.join(" ")} -default ${DEFAULT_LANGUAGE} -active ${ACTIVE_LANGUAGE}`).code
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
resetDemoApp();
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
shelljs.cd(APP_FOLDER);
|
||||
updateProjectPackageJson({type:"module"})
|
||||
createAppIndexFile()
|
||||
})
|
||||
|
||||
test("清空工程目录国际化",done=>{
|
||||
resetDemoApp();
|
||||
expect(fs.existsSync(LANGUAGE_FOLDER)).toBe(false);
|
||||
done();
|
||||
})
|
||||
|
||||
|
||||
test("初始化工程(esm)",async () =>{
|
||||
let code = shelljs.exec(`node ${CLI_INDEX_FILE} init . -lngs ${SUPPORTED_LANGUAGES.join(" ")} -default ${DEFAULT_LANGUAGE} -active ${ACTIVE_LANGUAGE}`).code
|
||||
expect(code).toEqual(0);
|
||||
expect(fs.existsSync(path.join(LANGUAGE_FOLDER,"package.json"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(LANGUAGE_FOLDER,"settings.js"))).toBe(true);
|
||||
expect(fs.readJSONSync(path.join(LANGUAGE_FOLDER,"package.json")).type || "commonjs").toEqual(MODULE_TYPE);
|
||||
const langSettings = await importModule(path.join(LANGUAGE_FOLDER,"settings.js"));
|
||||
|
||||
expect(langSettings.languages.map(lng=>lng.name).join(",")).toEqual(SUPPORTED_LANGUAGES.join(","));
|
||||
expect(langSettings.defaultLanguage).toEqual(DEFAULT_LANGUAGE);
|
||||
expect(langSettings.activeLanguage).toEqual(ACTIVE_LANGUAGE);
|
||||
})
|
||||
test("初始化工程(cjs)",async () =>{
|
||||
updateProjectPackageJson({type:"commonjs"})
|
||||
let code = shelljs.exec(`node ${CLI_INDEX_FILE} init . -lngs ${SUPPORTED_LANGUAGES.join(" ")} -default ${DEFAULT_LANGUAGE} -active ${ACTIVE_LANGUAGE}`).code
|
||||
expect(code).toEqual(0);
|
||||
expect(fs.existsSync(path.join(LANGUAGE_FOLDER,"package.json"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(LANGUAGE_FOLDER,"settings.js"))).toBe(true);
|
||||
expect(fs.readJSONSync(path.join(LANGUAGE_FOLDER,"package.json")).type || "commonjs").toEqual("commonjs");
|
||||
const langSettings = await importModule(path.join(LANGUAGE_FOLDER,"settings.js"));
|
||||
|
||||
expect(langSettings.languages.map(lng=>lng.name).join(",")).toEqual(SUPPORTED_LANGUAGES.join(","));
|
||||
expect(langSettings.defaultLanguage).toEqual(DEFAULT_LANGUAGE);
|
||||
expect(langSettings.activeLanguage).toEqual(ACTIVE_LANGUAGE);
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
test("提取文本(esm)",(done) =>{
|
||||
let code = shelljs.exec(`node ${CLI_INDEX_FILE} extract`).code
|
||||
expect(code).toEqual(0);
|
||||
// 翻译文件夹
|
||||
expect(fs.existsSync(TRANSLATES_FOLDER)).toBe(true);
|
||||
// 翻译文件
|
||||
const msgFile = path.join(TRANSLATES_FOLDER,"default.json")
|
||||
expect(fs.existsSync(msgFile)).toBe(true);
|
||||
let messages = fs.readJSONSync(msgFile)
|
||||
messages = fs.readJSONSync(msgFile)
|
||||
expect("a" in messages).toBeTruthy();
|
||||
expect("b" in messages).toBeTruthy();
|
||||
expect("c" in messages).toBeTruthy();
|
||||
expect("d" in messages).toBeTruthy();
|
||||
expect("e" in messages).toBeTruthy();
|
||||
done()
|
||||
})
|
||||
|
||||
|
||||
test("编译命令(esm)",(done) =>{
|
||||
shelljs.exec(`node ${CLI_INDEX_FILE} extract`).code
|
||||
let code = shelljs.exec(`node ${CLI_INDEX_FILE} compile`).code
|
||||
expect(code).toEqual(0);
|
||||
expect(fs.existsSync(path.join(LANGUAGE_FOLDER,"index.js"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(LANGUAGE_FOLDER,"formatters.js"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(LANGUAGE_FOLDER,"cn.js"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(LANGUAGE_FOLDER,"en.js"))).toBe(true);
|
||||
done()
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
const extract = require("../packages/tools/extract.plugin");
|
||||
const extract = require("../packages/cli/extract.plugin");
|
||||
const gulp = require('gulp');
|
||||
const path = require('path');
|
||||
const Vinyl = require('vinyl');
|
||||
const { getTranslateTexts, normalizeLanguageOptions } = require("../packages/tools/extract.plugin");
|
||||
const { getTranslateTexts, normalizeLanguageOptions } = require("../packages/cli/extract.plugin");
|
||||
|
||||
|
||||
const languages = [{name:'en',title:"英文"},{name:'cn',title:"中文",default:true},{name:'de',title:"德语"},{name:'jp',title:"日本語"}]
|
||||
|
Loading…
x
Reference in New Issue
Block a user