From fbde8540c8d1c7fe2e8e7148afb2e3ab7b9a1090 Mon Sep 17 00:00:00 2001 From: wxzhang Date: Thu, 24 Mar 2022 16:48:03 +0800 Subject: [PATCH] update --- .../apps/app/auth/changepassword.js | 0 .../demo => demo}/apps/app/auth/login.html | 0 .../demo => demo}/apps/app/auth/login.js | 0 {packages/demo => demo}/apps/app/db/index.js | 0 {packages/demo => demo}/apps/app/db/models.js | 0 demo/apps/app/index.js | 7 + .../demo => demo}/apps/app/messages/index.js | 0 demo/apps/app/package.json | 1 + demo/apps/lib1/languages/idMap.js | 7 + demo/apps/lib2/languages/idMap.js | 7 + {packages/demo => demo}/compile.demo.js | 2 +- {packages/demo => demo}/data.js | 0 {packages/demo => demo}/extract.demo.js | 2 +- {packages/demo => demo}/package.json | 2 +- {packages/demo => demo}/utils.demo.js | 2 +- package.json | 9 +- .../index.js} | 5 +- packages/babel/package.json | 16 + packages/babel/readme.md | 3 + packages/babel/utils.js | 18 + packages/cli/index.js | 4 +- packages/cli/init.command.js | 2 +- packages/cli/languages/index.js | 2 +- packages/cli/package.json | 24 +- packages/cli/readme.md | 63 ++ packages/cli/utils.js | 17 +- packages/demo/apps/app/index.js | 51 - packages/demo/apps/app/package.json | 7 - packages/demo/babel.plugin.demo.js | 19 - packages/formatters/datetime.formatters.js | 14 +- packages/formatters/index.js | 2 + packages/formatters/package.json | 7 +- packages/formatters/readme.md | 3 + packages/runtime/dist/index.cjs | 1 + packages/runtime/dist/index.esm.js | 1 + packages/runtime/index.cjs | 883 ------------------ packages/runtime/index.esm.js | 881 ----------------- packages/runtime/package.json | 18 +- packages/runtime/readme.md | 3 + packages/runtime/rollup.config.js | 8 +- pnpm-lock.yaml | 108 ++- readme.md | 81 +- test/app.test.js | 11 - test/babel.test.js | 2 +- test/cli.test.js | 163 ++++ test/extract.test.js | 4 +- 46 files changed, 482 insertions(+), 1978 deletions(-) rename {packages/demo => demo}/apps/app/auth/changepassword.js (100%) rename {packages/demo => demo}/apps/app/auth/login.html (100%) rename {packages/demo => demo}/apps/app/auth/login.js (100%) rename {packages/demo => demo}/apps/app/db/index.js (100%) rename {packages/demo => demo}/apps/app/db/models.js (100%) create mode 100644 demo/apps/app/index.js rename {packages/demo => demo}/apps/app/messages/index.js (100%) create mode 100644 demo/apps/app/package.json create mode 100644 demo/apps/lib1/languages/idMap.js create mode 100644 demo/apps/lib2/languages/idMap.js rename {packages/demo => demo}/compile.demo.js (59%) rename {packages/demo => demo}/data.js (100%) rename {packages/demo => demo}/extract.demo.js (95%) rename {packages/demo => demo}/package.json (90%) rename {packages/demo => demo}/utils.demo.js (93%) rename packages/{cli/babel-plugin-voerkai18n.js => babel/index.js} (97%) create mode 100644 packages/babel/package.json create mode 100644 packages/babel/readme.md create mode 100644 packages/babel/utils.js create mode 100644 packages/cli/readme.md delete mode 100644 packages/demo/apps/app/index.js delete mode 100644 packages/demo/apps/app/package.json delete mode 100644 packages/demo/babel.plugin.demo.js create mode 100644 packages/formatters/index.js create mode 100644 packages/formatters/readme.md create mode 100644 packages/runtime/dist/index.cjs create mode 100644 packages/runtime/dist/index.esm.js delete mode 100644 packages/runtime/index.cjs delete mode 100644 packages/runtime/index.esm.js create mode 100644 packages/runtime/readme.md delete mode 100644 test/app.test.js create mode 100644 test/cli.test.js diff --git a/packages/demo/apps/app/auth/changepassword.js b/demo/apps/app/auth/changepassword.js similarity index 100% rename from packages/demo/apps/app/auth/changepassword.js rename to demo/apps/app/auth/changepassword.js diff --git a/packages/demo/apps/app/auth/login.html b/demo/apps/app/auth/login.html similarity index 100% rename from packages/demo/apps/app/auth/login.html rename to demo/apps/app/auth/login.html diff --git a/packages/demo/apps/app/auth/login.js b/demo/apps/app/auth/login.js similarity index 100% rename from packages/demo/apps/app/auth/login.js rename to demo/apps/app/auth/login.js diff --git a/packages/demo/apps/app/db/index.js b/demo/apps/app/db/index.js similarity index 100% rename from packages/demo/apps/app/db/index.js rename to demo/apps/app/db/index.js diff --git a/packages/demo/apps/app/db/models.js b/demo/apps/app/db/models.js similarity index 100% rename from packages/demo/apps/app/db/models.js rename to demo/apps/app/db/models.js diff --git a/demo/apps/app/index.js b/demo/apps/app/index.js new file mode 100644 index 0000000..16fa34a --- /dev/null +++ b/demo/apps/app/index.js @@ -0,0 +1,7 @@ + + t("a") + t("b") + t("c") + t("d") + t("e") + \ No newline at end of file diff --git a/packages/demo/apps/app/messages/index.js b/demo/apps/app/messages/index.js similarity index 100% rename from packages/demo/apps/app/messages/index.js rename to demo/apps/app/messages/index.js diff --git a/demo/apps/app/package.json b/demo/apps/app/package.json new file mode 100644 index 0000000..089153b --- /dev/null +++ b/demo/apps/app/package.json @@ -0,0 +1 @@ +{"type":"module"} diff --git a/demo/apps/lib1/languages/idMap.js b/demo/apps/lib1/languages/idMap.js new file mode 100644 index 0000000..5adaca2 --- /dev/null +++ b/demo/apps/lib1/languages/idMap.js @@ -0,0 +1,7 @@ +export default { + "a":1, + "b":2, + "c{}{}":3, + "d{a}{b}":4, + "e":5 +} \ No newline at end of file diff --git a/demo/apps/lib2/languages/idMap.js b/demo/apps/lib2/languages/idMap.js new file mode 100644 index 0000000..3053d9d --- /dev/null +++ b/demo/apps/lib2/languages/idMap.js @@ -0,0 +1,7 @@ +module.exports = { + "a":1, + "b":2, + "c{}{}":3, + "d{a}{b}":4, + "e":5 +} \ No newline at end of file diff --git a/packages/demo/compile.demo.js b/demo/compile.demo.js similarity index 59% rename from packages/demo/compile.demo.js rename to demo/compile.demo.js index f5d615d..f179a79 100644 --- a/packages/demo/compile.demo.js +++ b/demo/compile.demo.js @@ -1,6 +1,6 @@ -const compile = require('@voerkai18n/tools/compile.command'); +const compile = require('@voerkai18n/cli/compile.command'); const path = require("path") diff --git a/packages/demo/data.js b/demo/data.js similarity index 100% rename from packages/demo/data.js rename to demo/data.js diff --git a/packages/demo/extract.demo.js b/demo/extract.demo.js similarity index 95% rename from packages/demo/extract.demo.js rename to demo/extract.demo.js index 02e9828..946d176 100644 --- a/packages/demo/extract.demo.js +++ b/demo/extract.demo.js @@ -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'); diff --git a/packages/demo/package.json b/demo/package.json similarity index 90% rename from packages/demo/package.json rename to demo/package.json index fd3d7b2..0189d1b 100644 --- a/packages/demo/package.json +++ b/demo/package.json @@ -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", diff --git a/packages/demo/utils.demo.js b/demo/utils.demo.js similarity index 93% rename from packages/demo/utils.demo.js rename to demo/utils.demo.js index 50e3006..559348c 100644 --- a/packages/demo/utils.demo.js +++ b/demo/utils.demo.js @@ -1,4 +1,4 @@ -const { objectStringify } = require("../tools/stringify") +const { objectStringify } = require("@voerkai18n/cli/stringify") const path = require("path"); const fs = require("fs"); diff --git a/package.json b/package.json index 66881d1..8babf63 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/packages/cli/babel-plugin-voerkai18n.js b/packages/babel/index.js similarity index 97% rename from packages/cli/babel-plugin-voerkai18n.js rename to packages/babel/index.js index 65184a1..995fff5 100644 --- a/packages/cli/babel-plugin-voerkai18n.js +++ b/packages/babel/index.js @@ -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 + * * 使用方法: * * { diff --git a/packages/babel/package.json b/packages/babel/package.json new file mode 100644 index 0000000..bced5d2 --- /dev/null +++ b/packages/babel/package.json @@ -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" +} diff --git a/packages/babel/readme.md b/packages/babel/readme.md new file mode 100644 index 0000000..11a870b --- /dev/null +++ b/packages/babel/readme.md @@ -0,0 +1,3 @@ +# @voerkai18n/babel + +`Babel`转码插件,用来对翻译文本进行自动转码 diff --git a/packages/babel/utils.js b/packages/babel/utils.js new file mode 100644 index 0000000..ca7718b --- /dev/null +++ b/packages/babel/utils.js @@ -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 +} \ No newline at end of file diff --git a/packages/cli/index.js b/packages/cli/index.js index 235aec9..e55eccd 100644 --- a/packages/cli/index.js +++ b/packages/cli/index.js @@ -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 ', t('支持的语言列表'), ['cn','en']) + .option('-lngs, --languages ', t('支持的语言列表'), ['cn','en']) + .option('-default, --defaultLanguage ', t('默认语言'), 'cn') + .option('-active, --activeLanguage ', t('激活语言'), 'cn') .hook("preAction",async function(location){ const lang= process.env.LANGUAGE if(lang){ diff --git a/packages/cli/init.command.js b/packages/cli/init.command.js index 1a1f8fb..5af2ec3 100644 --- a/packages/cli/init.command.js +++ b/packages/cli/init.command.js @@ -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)}`) diff --git a/packages/cli/languages/index.js b/packages/cli/languages/index.js index da5d66c..58029af 100644 --- a/packages/cli/languages/index.js +++ b/packages/cli/languages/index.js @@ -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映射列表 diff --git a/packages/cli/package.json b/packages/cli/package.json index 1fe4836..b46be0a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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": { - } } diff --git a/packages/cli/readme.md b/packages/cli/readme.md new file mode 100644 index 0000000..b0a8b29 --- /dev/null +++ b/packages/cli/readme.md @@ -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 支持的语言列表 (default: ["cn","en"]) + -default, --defaultLanguage 默认语言 + -active, --activeLanguage 激活语言 + -h, --help display help for command + + +``` + + +## 提取文本 - extract + +```shell + +扫描并提取所有待翻译的字符串到文件夹中 + +Arguments: + location 工程项目所在目录 (default: "./") + +Options: + -d, --debug 输出调试信息 + -lngs, --languages 支持的语言 + -default, --defaultLanguage 默认语言 + -active, --activeLanguage 激活语言 + -ns, --namespaces 翻译名称空间 + -e, --exclude 排除要扫描的文件夹,多个用逗号分隔 + -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 + +``` + diff --git a/packages/cli/utils.js b/packages/cli/utils.js index 03fa71c..9e797bb 100644 --- a/packages/cli/utils.js +++ b/packages/cli/utils.js @@ -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 diff --git a/packages/demo/apps/app/index.js b/packages/demo/apps/app/index.js deleted file mode 100644 index 62b1770..0000000 --- a/packages/demo/apps/app/index.js +++ /dev/null @@ -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 } - diff --git a/packages/demo/apps/app/package.json b/packages/demo/apps/app/package.json deleted file mode 100644 index 477348e..0000000 --- a/packages/demo/apps/app/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "commonjs", - "license": "MIT", - "devDependencies": { - "@voerkai18n/tools": "workspace:^1.0.0" - } -} \ No newline at end of file diff --git a/packages/demo/babel.plugin.demo.js b/packages/demo/babel.plugin.demo.js deleted file mode 100644 index de1b48a..0000000 --- a/packages/demo/babel.plugin.demo.js +++ /dev/null @@ -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) -}); \ No newline at end of file diff --git a/packages/formatters/datetime.formatters.js b/packages/formatters/datetime.formatters.js index b42e50e..38974e9 100644 --- a/packages/formatters/datetime.formatters.js +++ b/packages/formatters/datetime.formatters.js @@ -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() } diff --git a/packages/formatters/index.js b/packages/formatters/index.js new file mode 100644 index 0000000..58d4eb3 --- /dev/null +++ b/packages/formatters/index.js @@ -0,0 +1,2 @@ +import './datetime.formatters.js'; +import './currency.formatters.js'; diff --git a/packages/formatters/package.json b/packages/formatters/package.json index 50cc226..6af2f9d 100644 --- a/packages/formatters/package.json +++ b/packages/formatters/package.json @@ -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", diff --git a/packages/formatters/readme.md b/packages/formatters/readme.md new file mode 100644 index 0000000..ca19ffe --- /dev/null +++ b/packages/formatters/readme.md @@ -0,0 +1,3 @@ +# @voerkai18n/formatters + + diff --git a/packages/runtime/dist/index.cjs b/packages/runtime/dist/index.cjs new file mode 100644 index 0000000..78580be --- /dev/null +++ b/packages/runtime/dist/index.cjs @@ -0,0 +1 @@ +"use strict";var t=function(t){return function(t){return!!t&&"object"==typeof t}(t)&&!function(t){var r=Object.prototype.toString.call(t);return"[object RegExp]"===r||"[object Date]"===r||function(t){return t.$$typeof===e}(t)}(t)};var e="function"==typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function r(t,e){return!1!==e.clone&&e.isMergeableObject(t)?l((r=t,Array.isArray(r)?[]:{}),t,e):t;var r}function a(t,e,a){return t.concat(e).map((function(t){return r(t,a)}))}function n(t){return Object.keys(t).concat(function(t){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(t).filter((function(e){return t.propertyIsEnumerable(e)})):[]}(t))}function s(t,e){try{return e in t}catch(t){return!1}}function i(t,e,a){var i={};return a.isMergeableObject(t)&&n(t).forEach((function(e){i[e]=r(t[e],a)})),n(e).forEach((function(n){(function(t,e){return s(t,e)&&!(Object.hasOwnProperty.call(t,e)&&Object.propertyIsEnumerable.call(t,e))})(t,n)||(s(t,n)&&a.isMergeableObject(e[n])?i[n]=function(t,e){if(!e.customMerge)return l;var r=e.customMerge(t);return"function"==typeof r?r:l}(n,a)(t[n],e[n],a):i[n]=r(e[n],a))})),i}function l(e,n,s){(s=s||{}).arrayMerge=s.arrayMerge||a,s.isMergeableObject=s.isMergeableObject||t,s.cloneUnlessOtherwiseSpecified=r;var l=Array.isArray(n);return l===Array.isArray(e)?l?s.arrayMerge(e,n,s):i(e,n,s):r(n,s)}l.all=function(t,e){if(!Array.isArray(t))throw new Error("first argument should be an array");return t.reduce((function(t,r){return l(t,r,e)}),{})};var o=l;var c={"*":{$types:{Date:t=>t.toLocaleString()},time:t=>t.toLocaleTimeString(),shorttime:t=>t.toLocaleTimeString(),date:t=>t.toLocaleDateString(),dict:function(t,...e){for(let r=0;r0&&e.length%2!=0?e[e.length-1]:t}},cn:{$types:{Date:t=>`${t.getFullYear()}年${t.getMonth()+1}月${t.getDate()}日 ${t.getHours()}点${t.getMinutes()}分${t.getSeconds()}秒`},shortime:t=>t.toLocaleTimeString(),time:t=>`${t.getHours()}点${t.getMinutes()}分${t.getSeconds()}秒`,date:t=>`${t.getFullYear()}年${t.getMonth()+1}月${t.getDate()}日`,shortdate:t=>`${t.getFullYear()}-${t.getMonth()+1}-${t.getDate()}`,currency:t=>`${t}元`},en:{currency:t=>`$${t}`}};const u=o,g=class{constructor(){this._callbacks=[]}on(t){this._callbacks.includes(t)||this._callbacks.push(t)}off(t){for(let e=0;ee(...t)))):await Promise.all(this._callbacks.map((e=>e(...t))))}},f=class{constructor(t={},e){if(this._id=t.id||(new Date).getTime().toString()+parseInt(1e3*Math.random()),this._languages=t.languages,this._defaultLanguage=t.defaultLanguage||"cn",this._activeLanguage=t.activeLanguage,this._default=t.default,this._messages=t.messages,this._idMap=t.idMap,this._formatters=t.formatters,this._loaders=t.loaders,this._global=null,this.$cache={activeLanguage:null,typedFormatters:{},formatters:{}},!globalThis.VoerkaI18n){const{I18nManager:e}=E;globalThis.VoerkaI18n=new e({defaultLanguage:this.defaultLanguage,activeLanguage:this.activeLanguage,languages:t.languages})}this.global=globalThis.VoerkaI18n,this._loading=!1,this.register(e)}get id(){return this._id}get defaultLanguage(){return this._defaultLanguage}get activeLanguage(){return this._activeLanguage}get default(){return this._default}get messages(){return this._messages}get idMap(){return this._idMap}get formatters(){return this._formatters}get loaders(){return this._loaders}get global(){return this._global}set global(t){this._global=t}register(t){this.global.register(this).then(t).catch(t)}registerFormatter(t,e,{language:r="*"}={}){if("string"!=typeof t)throw new TypeError("Formatter must be a function");DataTypes.includes(t)?this.formatters[r].$types[t]=e:this.formatters[r][t]=e}_fallback(){this._messages=this._default,this._activeLanguage=this.defaultLanguage}async refresh(t){if(this._loading=Promise.resolve(),t||(t=this.activeLanguage),t===this.defaultLanguage)return void(this._messages=this._default);const e=this.loaders[t];if("function"==typeof e)try{this._messages=(await e()).default,this._activeLanguage=t}catch(e){console.warn(`Error while loading language <${t}> on i18nScope(${this.id}): ${e.message}`),this._fallback()}else this._fallback()}get on(){return this.global.on.bind(this.global)}get off(){return this.global.off.bind(this.global)}get offAll(){return this.global.offAll.bind(this.global)}get change(){return this.global.change.bind(this.global)}};let h=c,p=/\{\s*(?\w+)?(?(\s*\|\s*\w*(\(.*\)){0,1}\s*)*)\s*\}/g;const m=["String","Number","Boolean","Object","Array","Function","Error","Symbol","RegExp","Date","Null","Undefined","Set","Map","WeakSet","WeakMap"];function b(t){return null===t?"Null":void 0===t?"Undefined":"function"==typeof t?"Function":t.constructor&&t.constructor.name}function y(t){if("object"!=typeof t||null===t)return!1;var e=Object.getPrototypeOf(t);if(null===e)return!0;for(var r=e;null!==Object.getPrototypeOf(r);)r=Object.getPrototypeOf(r);return e===r}function d(t){return!isNaN(parseInt(t))}function _(t){if(!t)return[];return t.trim().substr(1).trim().split("|").map((t=>t.trim())).map((t=>{let e=t.indexOf("("),r=t.lastIndexOf(")");if(-1!==e&&-1!==r){const a=t.substr(e+1,r-e-1).trim();let n=""==a?[]:a.split(",").map((t=>{if(t=t.trim(),!isNaN(parseInt(t)))return parseInt(t);if(t.startsWith('"')&&t.endsWith('"')||t.startsWith("'")&&t.endsWith("'"))return t.substr(1,t.length-2);if("true"===t.toLowerCase()||"false"===t.toLowerCase())return"true"===t.toLowerCase();if(!(t.startsWith("{")&&t.endsWith("}")||t.startsWith("[")&&t.endsWith("]")))return String(t);try{return JSON.parse(t)}catch(e){return String(t)}}));return[t.substr(0,e),n]}return[t,[]]}))}function v(t,e,r={}){let a,n=t,s=Object.assign({replaceAll:!0},r);for(p.lastIndex=0;null!==(a=p.exec(n));){const t=a.groups.varname||"",r=_(a.groups.formatters);if("function"==typeof e)try{n=s.replaceAll?n.replaceAll(a[0],e(t,r,a[0])):n.replace(a[0],e(t,r,a[0]))}catch{break}p.lastIndex=0}return n}function $(t,e=null){t.$cache={activeLanguage:e,typedFormatters:{},formatters:{}}}function A(t,e,r){if(t.$cache||$(t),t.$cache.activeLanguage===e){if(r in t.$cache.formatters)return t.$cache.formatters[r]}else $(t,e);const a=[t.formatters,t.global.formatters];for(const n of a){if(e in n){let a=n[e]||{};if(r in a&&"function"==typeof a[r])return t.$cache.formatters[r]=a[r]}let a=n["*"]||{};if(r in a&&"function"==typeof a[r])return t.$cache.formatters[r]=a[r]}}function L(t,e,r,a){const n=function(t,e,r){let a=[];for(let n of r)if(n[0]){const r=A(t,e,n[0]);"function"==typeof r?a.push((t=>r(t,...n[1]))):a.push((t=>"function"==typeof t[n[0]]?t[n[0]].call(t,...n[1]):t))}return a}(t,e,r),s=function(t,e,r){if(t.$cache||$(t),t.$cache.activeLanguage===e){if(r in t.$cache.typedFormatters)return t.$cache.typedFormatters[r]}else $(t,e);const a=[t.formatters,t.global.formatters];for(const n of a)if(n){if(e in n&&y(n[e].$types)){let a=n[e].$types;if(r in a&&"function"==typeof a[r])return t.$cache.typedFormatters[r]=a[r]}if("*"in n&&y(n["*"].$types)){let e=n["*"].$types;if(r in e&&"function"==typeof e[r])return t.$cache.typedFormatters[r]=e[r]}}}(t,e,b(a));return s&&n.splice(0,0,s),a=function(t,e){if(0===e.length)return t;let r=t;try{for(let t of e){if("function"!=typeof t)return r;r=t(r)}}catch(e){console.error(`Error while execute i18n formatter for ${t}: ${e.message} `)}return r}(a,n),a}function w(t,...e){const r=this,a=r.global.activeLanguage;if(0===e.length||(!(n=t).includes("{")||!n.includes("}")))return t;var n;if(1===e.length&&y(e[0])){let n=e[0];return v(t,((t,e)=>{let s=t in n?n[t]:"";return L(r,a,e,s)}))}{const n=1===e.length&&Array.isArray(e[0])?[...e[0]]:e;if(0===n.length)return t;let s=0;return v(t,((t,e)=>{if(n.length>s)return L(r,a,e,n[s++]);throw new Error}),{replaceAll:!1})}}const S={defaultLanguage:"cn",activeLanguage:"cn",languages:{cn:{name:"cn",title:"中文",default:!0},en:{name:"en",title:"英文"}},formatters:h};function k(t){return parseInt(t)>0}function O(t,e){try{return Array.isArray(t)?t.length>e?t[e]:t[t.length-1]:t}catch{return Array.isArray(t)?t[0]:t}}function M(t){return t.replaceAll(/\\(?![trnbvf'"]{1})/g,"\\\\").replaceAll("\t","\\t").replaceAll("\n","\\n").replaceAll("\b","\\b").replaceAll("\r","\\r").replaceAll("\f","\\f").replaceAll("'","\\'").replaceAll('"','\\"').replaceAll("\v","\\v")}function j(t){return t.replaceAll("\\t","\t").replaceAll("\\n","\n").replaceAll("\\b","\b").replaceAll("\\r","\r").replaceAll("\\f","\f").replaceAll("\\'","'").replaceAll('\\"','"').replaceAll("\\v","\v").replaceAll(/\\\\(?![trnbvf'"]{1})/g,"\\")}class I extends g{static instance=null;callbacks=[];constructor(t={}){return super(),null!=I.instance||(I.instance=this,this._settings=u(S,t),this._scopes=[]),I.instance}get settings(){return this._settings}get scopes(){return this._scopes}get activeLanguage(){return this._settings.activeLanguage}get defaultLanguage(){return this.this._settings.defaultLanguage}get languages(){return this._settings.languages}get formatters(){return h}async change(t){if(-1===this.languages.findIndex((e=>e.name===t)))throw new Error("Not supported language:"+t);await this._refreshScopes(t),this._settings.activeLanguage=t,await this.emit(t)}async _refreshScopes(t){try{const e=this._scopes.map((e=>e.refresh(t)));Promise.allSettled?await Promise.allSettled(e):await Promise.all(e)}catch(t){console.warn("Error while refreshing i18n scopes:",t.message)}}async register(t){if(!(t instanceof f))throw new TypeError("Scope must be an instance of I18nScope");this._scopes.push(t),await t.refresh(this.activeLanguage)}registerFormatter(t,e,{language:r="*"}={}){if("string"!=typeof t)throw new TypeError("Formatter must be a function");m.includes(t)?this.formatters[r].$types[t]=e:this.formatters[r][t]=e}}var E={getInterpolatedVars:function(t){let e=[];return v(t,((t,r,a)=>{let n={name:t,formatters:r.map((([t,e])=>({name:t,args:e}))),match:a};return-1===e.findIndex((t=>t.name===n.name&&n.formatters.toString()==t.formatters.toString()))&&e.push(n),""})),e},replaceInterpolatedVars:w,I18nManager:I,translate:function(t){const e=this,r=e.global.activeLanguage;let a=t,n=[],s=[],i=null;try{if(2===arguments.length&&y(arguments[1])?(Object.entries(arguments[1]).forEach((([t,e])=>{if("function"==typeof e)try{n[t]=e()}catch(r){n[t]=e}t.startsWith("$")&&"number"==typeof n[t]&&s.push(t)})),n=[arguments[1]]):arguments.length>=2&&(n=[...arguments].splice(1).map(((t,e)=>{try{d(t="function"==typeof t?t():t)&&(i=parseInt(t))}catch(t){}return t}))),r===e.defaultLanguage)k(a)&&(a=e.default[a]||t);else{let t=k(a)?a:e.idMap[M(a)];a=e.messages[t]||a,a=Array.isArray(a)?a.map((t=>j(t))):j(a)}return Array.isArray(a)&&a.length>0&&(a=null!==i?O(a,i):pluralVar.length>0?O(a,parseInt(n(pluralVar[0]))):a[0]),0==n.length?a:w.call(e,a,...n)}catch(t){return a}},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"],i18nScope:f,defaultLanguageSettings:S,getDataTypeName:b,isNumber:d,isPlainObject:y};module.exports=E; diff --git a/packages/runtime/dist/index.esm.js b/packages/runtime/dist/index.esm.js new file mode 100644 index 0000000..f56be6e --- /dev/null +++ b/packages/runtime/dist/index.esm.js @@ -0,0 +1 @@ +var t=function(t){return function(t){return!!t&&"object"==typeof t}(t)&&!function(t){var r=Object.prototype.toString.call(t);return"[object RegExp]"===r||"[object Date]"===r||function(t){return t.$$typeof===e}(t)}(t)};var e="function"==typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function r(t,e){return!1!==e.clone&&e.isMergeableObject(t)?l((r=t,Array.isArray(r)?[]:{}),t,e):t;var r}function a(t,e,a){return t.concat(e).map((function(t){return r(t,a)}))}function n(t){return Object.keys(t).concat(function(t){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(t).filter((function(e){return t.propertyIsEnumerable(e)})):[]}(t))}function s(t,e){try{return e in t}catch(t){return!1}}function i(t,e,a){var i={};return a.isMergeableObject(t)&&n(t).forEach((function(e){i[e]=r(t[e],a)})),n(e).forEach((function(n){(function(t,e){return s(t,e)&&!(Object.hasOwnProperty.call(t,e)&&Object.propertyIsEnumerable.call(t,e))})(t,n)||(s(t,n)&&a.isMergeableObject(e[n])?i[n]=function(t,e){if(!e.customMerge)return l;var r=e.customMerge(t);return"function"==typeof r?r:l}(n,a)(t[n],e[n],a):i[n]=r(e[n],a))})),i}function l(e,n,s){(s=s||{}).arrayMerge=s.arrayMerge||a,s.isMergeableObject=s.isMergeableObject||t,s.cloneUnlessOtherwiseSpecified=r;var l=Array.isArray(n);return l===Array.isArray(e)?l?s.arrayMerge(e,n,s):i(e,n,s):r(n,s)}l.all=function(t,e){if(!Array.isArray(t))throw new Error("first argument should be an array");return t.reduce((function(t,r){return l(t,r,e)}),{})};var o=l;var c={"*":{$types:{Date:t=>t.toLocaleString()},time:t=>t.toLocaleTimeString(),shorttime:t=>t.toLocaleTimeString(),date:t=>t.toLocaleDateString(),dict:function(t,...e){for(let r=0;r0&&e.length%2!=0?e[e.length-1]:t}},cn:{$types:{Date:t=>`${t.getFullYear()}年${t.getMonth()+1}月${t.getDate()}日 ${t.getHours()}点${t.getMinutes()}分${t.getSeconds()}秒`},shortime:t=>t.toLocaleTimeString(),time:t=>`${t.getHours()}点${t.getMinutes()}分${t.getSeconds()}秒`,date:t=>`${t.getFullYear()}年${t.getMonth()+1}月${t.getDate()}日`,shortdate:t=>`${t.getFullYear()}-${t.getMonth()+1}-${t.getDate()}`,currency:t=>`${t}元`},en:{currency:t=>`$${t}`}};const u=o,g=class{constructor(){this._callbacks=[]}on(t){this._callbacks.includes(t)||this._callbacks.push(t)}off(t){for(let e=0;ee(...t)))):await Promise.all(this._callbacks.map((e=>e(...t))))}},f=class{constructor(t={},e){if(this._id=t.id||(new Date).getTime().toString()+parseInt(1e3*Math.random()),this._languages=t.languages,this._defaultLanguage=t.defaultLanguage||"cn",this._activeLanguage=t.activeLanguage,this._default=t.default,this._messages=t.messages,this._idMap=t.idMap,this._formatters=t.formatters,this._loaders=t.loaders,this._global=null,this.$cache={activeLanguage:null,typedFormatters:{},formatters:{}},!globalThis.VoerkaI18n){const{I18nManager:e}=E;globalThis.VoerkaI18n=new e({defaultLanguage:this.defaultLanguage,activeLanguage:this.activeLanguage,languages:t.languages})}this.global=globalThis.VoerkaI18n,this._loading=!1,this.register(e)}get id(){return this._id}get defaultLanguage(){return this._defaultLanguage}get activeLanguage(){return this._activeLanguage}get default(){return this._default}get messages(){return this._messages}get idMap(){return this._idMap}get formatters(){return this._formatters}get loaders(){return this._loaders}get global(){return this._global}set global(t){this._global=t}register(t){this.global.register(this).then(t).catch(t)}registerFormatter(t,e,{language:r="*"}={}){if("string"!=typeof t)throw new TypeError("Formatter must be a function");DataTypes.includes(t)?this.formatters[r].$types[t]=e:this.formatters[r][t]=e}_fallback(){this._messages=this._default,this._activeLanguage=this.defaultLanguage}async refresh(t){if(this._loading=Promise.resolve(),t||(t=this.activeLanguage),t===this.defaultLanguage)return void(this._messages=this._default);const e=this.loaders[t];if("function"==typeof e)try{this._messages=(await e()).default,this._activeLanguage=t}catch(e){console.warn(`Error while loading language <${t}> on i18nScope(${this.id}): ${e.message}`),this._fallback()}else this._fallback()}get on(){return this.global.on.bind(this.global)}get off(){return this.global.off.bind(this.global)}get offAll(){return this.global.offAll.bind(this.global)}get change(){return this.global.change.bind(this.global)}};let h=c,p=/\{\s*(?\w+)?(?(\s*\|\s*\w*(\(.*\)){0,1}\s*)*)\s*\}/g;const m=["String","Number","Boolean","Object","Array","Function","Error","Symbol","RegExp","Date","Null","Undefined","Set","Map","WeakSet","WeakMap"];function b(t){return null===t?"Null":void 0===t?"Undefined":"function"==typeof t?"Function":t.constructor&&t.constructor.name}function y(t){if("object"!=typeof t||null===t)return!1;var e=Object.getPrototypeOf(t);if(null===e)return!0;for(var r=e;null!==Object.getPrototypeOf(r);)r=Object.getPrototypeOf(r);return e===r}function d(t){return!isNaN(parseInt(t))}function _(t){if(!t)return[];return t.trim().substr(1).trim().split("|").map((t=>t.trim())).map((t=>{let e=t.indexOf("("),r=t.lastIndexOf(")");if(-1!==e&&-1!==r){const a=t.substr(e+1,r-e-1).trim();let n=""==a?[]:a.split(",").map((t=>{if(t=t.trim(),!isNaN(parseInt(t)))return parseInt(t);if(t.startsWith('"')&&t.endsWith('"')||t.startsWith("'")&&t.endsWith("'"))return t.substr(1,t.length-2);if("true"===t.toLowerCase()||"false"===t.toLowerCase())return"true"===t.toLowerCase();if(!(t.startsWith("{")&&t.endsWith("}")||t.startsWith("[")&&t.endsWith("]")))return String(t);try{return JSON.parse(t)}catch(e){return String(t)}}));return[t.substr(0,e),n]}return[t,[]]}))}function v(t,e,r={}){let a,n=t,s=Object.assign({replaceAll:!0},r);for(p.lastIndex=0;null!==(a=p.exec(n));){const t=a.groups.varname||"",r=_(a.groups.formatters);if("function"==typeof e)try{n=s.replaceAll?n.replaceAll(a[0],e(t,r,a[0])):n.replace(a[0],e(t,r,a[0]))}catch{break}p.lastIndex=0}return n}function $(t,e=null){t.$cache={activeLanguage:e,typedFormatters:{},formatters:{}}}function A(t,e,r){if(t.$cache||$(t),t.$cache.activeLanguage===e){if(r in t.$cache.formatters)return t.$cache.formatters[r]}else $(t,e);const a=[t.formatters,t.global.formatters];for(const n of a){if(e in n){let a=n[e]||{};if(r in a&&"function"==typeof a[r])return t.$cache.formatters[r]=a[r]}let a=n["*"]||{};if(r in a&&"function"==typeof a[r])return t.$cache.formatters[r]=a[r]}}function L(t,e,r,a){const n=function(t,e,r){let a=[];for(let n of r)if(n[0]){const r=A(t,e,n[0]);"function"==typeof r?a.push((t=>r(t,...n[1]))):a.push((t=>"function"==typeof t[n[0]]?t[n[0]].call(t,...n[1]):t))}return a}(t,e,r),s=function(t,e,r){if(t.$cache||$(t),t.$cache.activeLanguage===e){if(r in t.$cache.typedFormatters)return t.$cache.typedFormatters[r]}else $(t,e);const a=[t.formatters,t.global.formatters];for(const n of a)if(n){if(e in n&&y(n[e].$types)){let a=n[e].$types;if(r in a&&"function"==typeof a[r])return t.$cache.typedFormatters[r]=a[r]}if("*"in n&&y(n["*"].$types)){let e=n["*"].$types;if(r in e&&"function"==typeof e[r])return t.$cache.typedFormatters[r]=e[r]}}}(t,e,b(a));return s&&n.splice(0,0,s),a=function(t,e){if(0===e.length)return t;let r=t;try{for(let t of e){if("function"!=typeof t)return r;r=t(r)}}catch(e){console.error(`Error while execute i18n formatter for ${t}: ${e.message} `)}return r}(a,n),a}function w(t,...e){const r=this,a=r.global.activeLanguage;if(0===e.length||(!(n=t).includes("{")||!n.includes("}")))return t;var n;if(1===e.length&&y(e[0])){let n=e[0];return v(t,((t,e)=>{let s=t in n?n[t]:"";return L(r,a,e,s)}))}{const n=1===e.length&&Array.isArray(e[0])?[...e[0]]:e;if(0===n.length)return t;let s=0;return v(t,((t,e)=>{if(n.length>s)return L(r,a,e,n[s++]);throw new Error}),{replaceAll:!1})}}const S={defaultLanguage:"cn",activeLanguage:"cn",languages:{cn:{name:"cn",title:"中文",default:!0},en:{name:"en",title:"英文"}},formatters:h};function k(t){return parseInt(t)>0}function O(t,e){try{return Array.isArray(t)?t.length>e?t[e]:t[t.length-1]:t}catch{return Array.isArray(t)?t[0]:t}}function M(t){return t.replaceAll(/\\(?![trnbvf'"]{1})/g,"\\\\").replaceAll("\t","\\t").replaceAll("\n","\\n").replaceAll("\b","\\b").replaceAll("\r","\\r").replaceAll("\f","\\f").replaceAll("'","\\'").replaceAll('"','\\"').replaceAll("\v","\\v")}function j(t){return t.replaceAll("\\t","\t").replaceAll("\\n","\n").replaceAll("\\b","\b").replaceAll("\\r","\r").replaceAll("\\f","\f").replaceAll("\\'","'").replaceAll('\\"','"').replaceAll("\\v","\v").replaceAll(/\\\\(?![trnbvf'"]{1})/g,"\\")}class I extends g{static instance=null;callbacks=[];constructor(t={}){return super(),null!=I.instance||(I.instance=this,this._settings=u(S,t),this._scopes=[]),I.instance}get settings(){return this._settings}get scopes(){return this._scopes}get activeLanguage(){return this._settings.activeLanguage}get defaultLanguage(){return this.this._settings.defaultLanguage}get languages(){return this._settings.languages}get formatters(){return h}async change(t){if(-1===this.languages.findIndex((e=>e.name===t)))throw new Error("Not supported language:"+t);await this._refreshScopes(t),this._settings.activeLanguage=t,await this.emit(t)}async _refreshScopes(t){try{const e=this._scopes.map((e=>e.refresh(t)));Promise.allSettled?await Promise.allSettled(e):await Promise.all(e)}catch(t){console.warn("Error while refreshing i18n scopes:",t.message)}}async register(t){if(!(t instanceof f))throw new TypeError("Scope must be an instance of I18nScope");this._scopes.push(t),await t.refresh(this.activeLanguage)}registerFormatter(t,e,{language:r="*"}={}){if("string"!=typeof t)throw new TypeError("Formatter must be a function");m.includes(t)?this.formatters[r].$types[t]=e:this.formatters[r][t]=e}}var E={getInterpolatedVars:function(t){let e=[];return v(t,((t,r,a)=>{let n={name:t,formatters:r.map((([t,e])=>({name:t,args:e}))),match:a};return-1===e.findIndex((t=>t.name===n.name&&n.formatters.toString()==t.formatters.toString()))&&e.push(n),""})),e},replaceInterpolatedVars:w,I18nManager:I,translate:function(t){const e=this,r=e.global.activeLanguage;let a=t,n=[],s=[],i=null;try{if(2===arguments.length&&y(arguments[1])?(Object.entries(arguments[1]).forEach((([t,e])=>{if("function"==typeof e)try{n[t]=e()}catch(r){n[t]=e}t.startsWith("$")&&"number"==typeof n[t]&&s.push(t)})),n=[arguments[1]]):arguments.length>=2&&(n=[...arguments].splice(1).map(((t,e)=>{try{d(t="function"==typeof t?t():t)&&(i=parseInt(t))}catch(t){}return t}))),r===e.defaultLanguage)k(a)&&(a=e.default[a]||t);else{let t=k(a)?a:e.idMap[M(a)];a=e.messages[t]||a,a=Array.isArray(a)?a.map((t=>j(t))):j(a)}return Array.isArray(a)&&a.length>0&&(a=null!==i?O(a,i):pluralVar.length>0?O(a,parseInt(n(pluralVar[0]))):a[0]),0==n.length?a:w.call(e,a,...n)}catch(t){return a}},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"],i18nScope:f,defaultLanguageSettings:S,getDataTypeName:b,isNumber:d,isPlainObject:y};export{E as default}; diff --git a/packages/runtime/index.cjs b/packages/runtime/index.cjs deleted file mode 100644 index 97f30ec..0000000 --- a/packages/runtime/index.cjs +++ /dev/null @@ -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,,,,,,,...) - * - * - * 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;i0 && (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*(?\w+)?(?(\s*\|\s*\w*\s*)*)\s*\}/g - -// 支持参数: { var | formatter(x,x,..) | formatter } -let varWithPipeRegexp = /\{\s*(?\w+)?(?(\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 [[,[,,...]]] - */ -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 || ""; - // 解析格式化器和参数 = [,[,[,,...]]] - 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=默认语言包={:} - 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;iawait 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; diff --git a/packages/runtime/index.esm.js b/packages/runtime/index.esm.js deleted file mode 100644 index c22bc2b..0000000 --- a/packages/runtime/index.esm.js +++ /dev/null @@ -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,,,,,,,...) - * - * - * 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;i0 && (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*(?\w+)?(?(\s*\|\s*\w*\s*)*)\s*\}/g - -// 支持参数: { var | formatter(x,x,..) | formatter } -let varWithPipeRegexp = /\{\s*(?\w+)?(?(\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 [[,[,,...]]] - */ -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 || ""; - // 解析格式化器和参数 = [,[,[,,...]]] - 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=默认语言包={:} - 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;iawait 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 }; diff --git a/packages/runtime/package.json b/packages/runtime/package.json index d689f7c..b05a5c8 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -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" } } diff --git a/packages/runtime/readme.md b/packages/runtime/readme.md new file mode 100644 index 0000000..135e060 --- /dev/null +++ b/packages/runtime/readme.md @@ -0,0 +1,3 @@ +# @voerkai18n/runtime + +`voerkai18n`运行时依赖 \ No newline at end of file diff --git a/packages/runtime/rollup.config.js b/packages/runtime/rollup.config.js index 57e230b..80067c2 100644 --- a/packages/runtime/rollup.config.js +++ b/packages/runtime/rollup.config.js @@ -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"] } ] \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f090f24..a3aeb1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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==} diff --git a/readme.md b/readme.md index 75d9fa2..011a4be 100644 --- a/readme.md +++ b/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 支持的语言列表 (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`命令后,会在当前工程中创建相应配置文件。 diff --git a/test/app.test.js b/test/app.test.js deleted file mode 100644 index 581ec5a..0000000 --- a/test/app.test.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * 测试demp app的语言运行环境 - */ - const i18n = require('../packages/tools/languages'); - const path = require("path") - - - -test("导入多语言包",done=>{ - expect(t).toBeFunction() -}) \ No newline at end of file diff --git a/test/babel.test.js b/test/babel.test.js index f225ae0..73e4f42 100644 --- a/test/babel.test.js +++ b/test/babel.test.js @@ -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){ diff --git a/test/cli.test.js b/test/cli.test.js new file mode 100644 index 0000000..9ef46a5 --- /dev/null +++ b/test/cli.test.js @@ -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() +}) + + + + + + + + + diff --git a/test/extract.test.js b/test/extract.test.js index 8d4a745..9a48aa9 100644 --- a/test/extract.test.js +++ b/test/extract.test.js @@ -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:"日本語"}]