增加自动翻译机制
This commit is contained in:
parent
834ae4cbfd
commit
af2384683d
12
.gitignore
vendored
12
.gitignore
vendored
@ -4,4 +4,14 @@ node_modules
|
||||
/packages/**/node_modules
|
||||
/coverage
|
||||
/packages/apps/vueapp/src/languages
|
||||
/packages/apps/app/languages
|
||||
/packages/apps/app/languages
|
||||
/packages/apps/test
|
||||
node_modules/
|
||||
docs/.vuepress/.cache/
|
||||
docs/.vuepress/.temp/
|
||||
docs/.vuepress/dist/
|
||||
/packages/cli/baidu.api.txt
|
||||
node_modules/
|
||||
docs/.vuepress/.cache/
|
||||
docs/.vuepress/.temp/
|
||||
docs/.vuepress/dist/
|
||||
|
17
package.json
17
package.json
@ -6,11 +6,12 @@
|
||||
"scripts": {
|
||||
"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",
|
||||
"list:package":"node ./packages/autopublish/index.js list"
|
||||
|
||||
"test:app": "cross-env NODE_OPTIONS=--experimental-vm-modules node node_modules/jest/bin/jest.js -- app",
|
||||
"list:package": "node ./packages/autopublish/index.js list",
|
||||
"autopublish": "node ./packages/autopublish/index.js",
|
||||
"docs:build": "cross-env NODE_OPTIONS=--openssl-legacy-provider vuepress build docs",
|
||||
"docs:dev": "cross-env NODE_OPTIONS=--openssl-legacy-provider && vuepress dev docs",
|
||||
"docs:clean-dev": "vuepress dev docs --clean-cache"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
@ -20,6 +21,7 @@
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@rollup/plugin-babel": "^5.3.1",
|
||||
"@rollup/plugin-commonjs": "^21.0.2",
|
||||
"@voerkai18n/autopublish": "^1.0.3",
|
||||
"dayjs": "^1.11.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"fs-extra": "^10.0.1",
|
||||
@ -29,9 +31,12 @@
|
||||
"rollup": "^2.70.1",
|
||||
"rollup-plugin-clear": "^2.0.7",
|
||||
"shelljs": "^0.8.5",
|
||||
"vinyl": "^2.2.1"
|
||||
"vinyl": "^2.2.1",
|
||||
"vuepress": "^2.0.0-beta.38",
|
||||
"vuepress-theme-hope": "^2.0.0-beta.36"
|
||||
},
|
||||
"dependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"inquirer": "^8.2.2"
|
||||
}
|
||||
}
|
||||
|
@ -1,32 +1,32 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
"1": "a",
|
||||
"2": "b",
|
||||
"3": "c",
|
||||
"4": "d",
|
||||
"5": "e",
|
||||
"6": "请输入旧密码:",
|
||||
"7": "请再次输入旧密码:",
|
||||
"8": "请输入新密码:",
|
||||
"9": "请再次输入新密码:",
|
||||
"10": "密码至少需要6位,并且至少包含数字、字符或特殊符号中的两种",
|
||||
"11": "密码强度: {strength}",
|
||||
"12": "用户名或密码错误",
|
||||
"13": "请输入用户名:",
|
||||
"14": "请输入密码:",
|
||||
"15": "欢迎您: {}",
|
||||
"16": "数据库类型:{}、{}、{}",
|
||||
"17": "数据库密码:{pwd}",
|
||||
"18": "数据库地址:{url}",
|
||||
"19": "编码:{encode}",
|
||||
"20": "编码",
|
||||
"21": "名称",
|
||||
"22": "描述",
|
||||
"23": "文件名称",
|
||||
"24": "您有{}条未读消息",
|
||||
"25": "消息总数:{$count}",
|
||||
"26": "消息类型:{type}",
|
||||
"27": "登录",
|
||||
"28": "请输入密码:",
|
||||
"29": "头像",
|
||||
"30": "相片"
|
||||
"6": "Please enter your old password:",
|
||||
"7": "Please enter your old password again:",
|
||||
"8": "Please enter a new password:",
|
||||
"9": "Please enter the new password again:",
|
||||
"10": "The password needs at least 6 digits and contains at least two of numbers, characters or special symbols",
|
||||
"11": "Password strength: {strength}",
|
||||
"12": "Wrong user name or password",
|
||||
"13": "Please enter user name:",
|
||||
"14": "Please input a password:",
|
||||
"15": "Welcome: {}",
|
||||
"16": "Database type: {}, {}, {}",
|
||||
"17": "Database password: {PWD}",
|
||||
"18": "Database address: {URL}",
|
||||
"19": "Code: {encode}",
|
||||
"20": "code",
|
||||
"21": "name",
|
||||
"22": "describe",
|
||||
"23": "File name",
|
||||
"24": "You have {} unread messages",
|
||||
"25": "Total messages: {$count}",
|
||||
"26": "Message type: {type}",
|
||||
"27": "Sign in",
|
||||
"28": "Please input a password:",
|
||||
"29": "head portrait",
|
||||
"30": "photo"
|
||||
}
|
@ -47,7 +47,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
// 在所有语言下生效的格式化器
|
||||
"*":{
|
||||
//[格式化名称]:(value)=>{...},
|
||||
|
@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"c": 3,
|
||||
|
@ -1,9 +1,10 @@
|
||||
|
||||
const messageIds = require("./idMap")
|
||||
const { translate,i18nScope } = require("./runtime.js")
|
||||
import messageIds from "./idMap.js"
|
||||
import runtime from "./runtime.js"
|
||||
const { translate,i18nScope } = runtime
|
||||
|
||||
const formatters = require("./formatters.js")
|
||||
const defaultMessages = require("./cn.js")
|
||||
import formatters from "./formatters.js"
|
||||
import defaultMessages from "./zh.js"
|
||||
const activeMessages = defaultMessages
|
||||
|
||||
|
||||
@ -11,16 +12,24 @@ const activeMessages = defaultMessages
|
||||
const scopeSettings = {
|
||||
"languages": [
|
||||
{
|
||||
"name": "cn",
|
||||
"title": "cn"
|
||||
"name": "zh",
|
||||
"title": "中文"
|
||||
},
|
||||
{
|
||||
"name": "en",
|
||||
"title": "en"
|
||||
"title": "英语"
|
||||
},
|
||||
{
|
||||
"name": "de",
|
||||
"title": "德语"
|
||||
},
|
||||
{
|
||||
"name": "jp",
|
||||
"title": "日语"
|
||||
}
|
||||
],
|
||||
"defaultLanguage": "cn",
|
||||
"activeLanguage": "cn",
|
||||
"defaultLanguage": "zh",
|
||||
"activeLanguage": "zh",
|
||||
"namespaces": {}
|
||||
}
|
||||
|
||||
@ -33,12 +42,16 @@ const scope = new i18nScope({
|
||||
idMap:messageIds, // 消息id映射列表
|
||||
formatters, // 当前作用域的格式化函数列表
|
||||
loaders:{
|
||||
"en" : ()=>import("./en.js")
|
||||
"en" : ()=>import("./en.js"),
|
||||
"de" : ()=>import("./de.js"),
|
||||
"jp" : ()=>import("./jp.js")
|
||||
}
|
||||
})
|
||||
// 翻译函数
|
||||
const t = translate.bind(scope)
|
||||
const scopedTtranslate = translate.bind(scope)
|
||||
|
||||
module.exports.t = t
|
||||
module.exports.i18nScope = scope
|
||||
export {
|
||||
scopedTtranslate as t,
|
||||
scope as i18nScope
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,23 @@
|
||||
{
|
||||
"languages": [
|
||||
{
|
||||
"name": "cn",
|
||||
"title": "cn"
|
||||
"name": "zh",
|
||||
"title": "中文"
|
||||
},
|
||||
{
|
||||
"name": "en",
|
||||
"title": "en"
|
||||
"title": "英语"
|
||||
},
|
||||
{
|
||||
"name": "de",
|
||||
"title": "德语"
|
||||
},
|
||||
{
|
||||
"name": "jp",
|
||||
"title": "日语"
|
||||
}
|
||||
],
|
||||
"defaultLanguage": "cn",
|
||||
"activeLanguage": "cn",
|
||||
"defaultLanguage": "zh",
|
||||
"activeLanguage": "zh",
|
||||
"namespaces": {}
|
||||
}
|
@ -3,181 +3,241 @@
|
||||
"en": "a",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "a",
|
||||
"jp": "a"
|
||||
},
|
||||
"b": {
|
||||
"en": "b",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "b",
|
||||
"jp": "b"
|
||||
},
|
||||
"c": {
|
||||
"en": "c",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "c",
|
||||
"jp": "c"
|
||||
},
|
||||
"d": {
|
||||
"en": "d",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "d",
|
||||
"jp": "d"
|
||||
},
|
||||
"e": {
|
||||
"en": "e",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "e",
|
||||
"jp": "e"
|
||||
},
|
||||
"请输入旧密码:": {
|
||||
"en": "请输入旧密码:",
|
||||
"en": "Please enter your old password:",
|
||||
"$file": [
|
||||
"auth\\changepassword.js"
|
||||
]
|
||||
],
|
||||
"de": "Bitte geben Sie Ihr altes Passwort ein:",
|
||||
"jp": "古いパスワードを入力してください:"
|
||||
},
|
||||
"请再次输入旧密码:": {
|
||||
"en": "请再次输入旧密码:",
|
||||
"en": "Please enter your old password again:",
|
||||
"$file": [
|
||||
"auth\\changepassword.js"
|
||||
]
|
||||
],
|
||||
"de": "Bitte geben Sie Ihr altes Passwort erneut ein:",
|
||||
"jp": "古いパスワードをもう一度入力してください:"
|
||||
},
|
||||
"请输入新密码:": {
|
||||
"en": "请输入新密码:",
|
||||
"en": "Please enter a new password:",
|
||||
"$file": [
|
||||
"auth\\changepassword.js"
|
||||
]
|
||||
],
|
||||
"de": "Bitte geben Sie ein neues Passwort ein:",
|
||||
"jp": "新しいパスワードを入力してください:"
|
||||
},
|
||||
"请再次输入新密码:": {
|
||||
"en": "请再次输入新密码:",
|
||||
"en": "Please enter the new password again:",
|
||||
"$file": [
|
||||
"auth\\changepassword.js"
|
||||
]
|
||||
],
|
||||
"de": "Bitte geben Sie das neue Passwort erneut ein:",
|
||||
"jp": "新しいパスワードを再度入力してください:"
|
||||
},
|
||||
"密码至少需要6位,并且至少包含数字、字符或特殊符号中的两种": {
|
||||
"en": "密码至少需要6位,并且至少包含数字、字符或特殊符号中的两种",
|
||||
"en": "The password needs at least 6 digits and contains at least two of numbers, characters or special symbols",
|
||||
"$file": [
|
||||
"auth\\changepassword.js"
|
||||
]
|
||||
],
|
||||
"de": "Das Passwort benötigt mindestens sechs Ziffern und enthält mindestens zwei Ziffern, Zeichen oder Sondersymbole",
|
||||
"jp": "パスワードには少なくとも6ビットが必要で、数字、文字、または特殊記号の少なくとも2種類が含まれています。"
|
||||
},
|
||||
"密码强度: {strength}": {
|
||||
"en": "密码强度: {strength}",
|
||||
"en": "Password strength: {strength}",
|
||||
"$file": [
|
||||
"auth\\changepassword.js"
|
||||
]
|
||||
],
|
||||
"de": "Kennwortstärke: {Stärke}",
|
||||
"jp": "パスワードの強度:{strength}"
|
||||
},
|
||||
"用户名或密码错误": {
|
||||
"en": "用户名或密码错误",
|
||||
"en": "Wrong user name or password",
|
||||
"$file": [
|
||||
"auth\\login.js"
|
||||
]
|
||||
],
|
||||
"de": "Falscher Benutzername oder falsches Passwort",
|
||||
"jp": "ユーザー名またはパスワードが間違っています"
|
||||
},
|
||||
"请输入用户名:": {
|
||||
"en": "请输入用户名:",
|
||||
"en": "Please enter user name:",
|
||||
"$file": [
|
||||
"auth\\login.js",
|
||||
"auth\\login.html"
|
||||
]
|
||||
],
|
||||
"de": "Bitte geben Sie Ihren Benutzernamen ein:",
|
||||
"jp": "ユーザー名を入力してください:"
|
||||
},
|
||||
"请输入密码:": {
|
||||
"en": "请输入密码:",
|
||||
"en": "Please input a password:",
|
||||
"$file": [
|
||||
"auth\\login.js"
|
||||
]
|
||||
],
|
||||
"de": "Bitte geben Sie ein Passwort ein:",
|
||||
"jp": "パスワードを入力してください:"
|
||||
},
|
||||
"欢迎您: {}": {
|
||||
"en": "欢迎您: {}",
|
||||
"en": "Welcome: {}",
|
||||
"$file": [
|
||||
"auth\\login.js"
|
||||
]
|
||||
],
|
||||
"de": "Willkommen: {}",
|
||||
"jp": "ようこそ"
|
||||
},
|
||||
"数据库类型:{}、{}、{}": {
|
||||
"en": "数据库类型:{}、{}、{}",
|
||||
"en": "Database type: {}, {}, {}",
|
||||
"$file": [
|
||||
"db\\index.js"
|
||||
]
|
||||
],
|
||||
"de": "Datenbanktyp: {}, {}, {}, {}",
|
||||
"jp": "データベースタイプ:{}、{}、{}"
|
||||
},
|
||||
"数据库密码:{pwd}": {
|
||||
"en": "数据库密码:{pwd}",
|
||||
"en": "Database password: {PWD}",
|
||||
"$file": [
|
||||
"db\\index.js"
|
||||
]
|
||||
],
|
||||
"de": "Datenbankpasswort: {PWD}",
|
||||
"jp": "データベースパスワード:{pwd}"
|
||||
},
|
||||
"数据库地址:{url}": {
|
||||
"en": "数据库地址:{url}",
|
||||
"en": "Database address: {URL}",
|
||||
"$file": [
|
||||
"db\\index.js"
|
||||
]
|
||||
],
|
||||
"de": "Datenbankadresse: {URL}",
|
||||
"jp": "データベースアドレス:{url}"
|
||||
},
|
||||
"编码:{encode}": {
|
||||
"en": "编码:{encode}",
|
||||
"en": "Code: {encode}",
|
||||
"$file": [
|
||||
"db\\index.js"
|
||||
]
|
||||
],
|
||||
"de": "Code: {kodieren}",
|
||||
"jp": "エンコーディング:{encode}"
|
||||
},
|
||||
"编码": {
|
||||
"en": "编码",
|
||||
"en": "code",
|
||||
"$file": [
|
||||
"db\\models.js"
|
||||
]
|
||||
],
|
||||
"de": "Code",
|
||||
"jp": "エンコーディング"
|
||||
},
|
||||
"名称": {
|
||||
"en": "名称",
|
||||
"en": "name",
|
||||
"$file": [
|
||||
"db\\models.js"
|
||||
]
|
||||
],
|
||||
"de": "Name",
|
||||
"jp": "名前"
|
||||
},
|
||||
"描述": {
|
||||
"en": "描述",
|
||||
"en": "describe",
|
||||
"$file": [
|
||||
"db\\models.js"
|
||||
]
|
||||
],
|
||||
"de": "Beschreibung",
|
||||
"jp": "説明"
|
||||
},
|
||||
"文件名称": {
|
||||
"en": "文件名称",
|
||||
"en": "File name",
|
||||
"$file": [
|
||||
"db\\models.js"
|
||||
]
|
||||
],
|
||||
"de": "Dateiname",
|
||||
"jp": "ファイル名"
|
||||
},
|
||||
"您有{}条未读消息": {
|
||||
"en": "您有{}条未读消息",
|
||||
"en": "You have {} unread messages",
|
||||
"$file": [
|
||||
"messages\\index.js"
|
||||
]
|
||||
],
|
||||
"de": "Sie haben {} ungelesene Nachrichten",
|
||||
"jp": "未読メッセージがあります"
|
||||
},
|
||||
"消息总数:{$count}": {
|
||||
"en": "消息总数:{$count}",
|
||||
"en": "Total messages: {$count}",
|
||||
"$file": [
|
||||
"messages\\index.js"
|
||||
]
|
||||
],
|
||||
"de": "Gesamtnachrichten: {$count}",
|
||||
"jp": "合計メッセージ:{$count}"
|
||||
},
|
||||
"消息类型:{type}": {
|
||||
"en": "消息类型:{type}",
|
||||
"en": "Message type: {type}",
|
||||
"$file": [
|
||||
"messages\\index.js"
|
||||
]
|
||||
],
|
||||
"de": "Nachrichtentyp: {type}",
|
||||
"jp": "メッセージタイプ:{type}"
|
||||
},
|
||||
"登录": {
|
||||
"en": "登录",
|
||||
"en": "Sign in",
|
||||
"$file": [
|
||||
"auth\\login.html"
|
||||
]
|
||||
],
|
||||
"de": "Anmelden",
|
||||
"jp": "ログイン"
|
||||
},
|
||||
"请输入密码:": {
|
||||
"en": "请输入密码:",
|
||||
"en": "Please input a password:",
|
||||
"$file": [
|
||||
"auth\\login.html"
|
||||
]
|
||||
],
|
||||
"de": "Bitte geben Sie ein Passwort ein:",
|
||||
"jp": "パスワードを入力してください:"
|
||||
},
|
||||
"头像": {
|
||||
"en": "头像",
|
||||
"en": "head portrait",
|
||||
"$file": [
|
||||
"auth\\login.html"
|
||||
]
|
||||
],
|
||||
"de": "Kopfportrait",
|
||||
"jp": "アイコン"
|
||||
},
|
||||
"相片": {
|
||||
"en": "相片",
|
||||
"en": "photo",
|
||||
"$file": [
|
||||
"auth\\login.html"
|
||||
]
|
||||
],
|
||||
"de": "Foto",
|
||||
"jp": "写真"
|
||||
}
|
||||
}
|
@ -18,46 +18,86 @@
|
||||
*
|
||||
*/
|
||||
|
||||
const fs = require("fs-extra");
|
||||
const inquirer = require("inquirer");
|
||||
const semver = require("semver")
|
||||
const path = require("path");
|
||||
const shelljs = require("shelljs");
|
||||
const createLogger = require("logsets");
|
||||
const { Command ,Option} = require('commander');
|
||||
const dayjs = require("dayjs");
|
||||
const relativeTime = require("dayjs/plugin/relativeTime");
|
||||
dayjs.extend(relativeTime);
|
||||
require('dayjs/locale/zh-cn')
|
||||
dayjs.locale("zh-cn");
|
||||
|
||||
const logger = createLogger();
|
||||
|
||||
|
||||
const program =new Command()
|
||||
|
||||
|
||||
const fs = require("fs-extra");
|
||||
const inquirer = require("inquirer");
|
||||
const semver = require("semver")
|
||||
const path = require("path");
|
||||
const shelljs = require("shelljs");
|
||||
const createLogger = require("logsets");
|
||||
const TaskListPlugin = require("logsets/plugins/tasklist")
|
||||
const TablePlugin = require("logsets/plugins/table")
|
||||
|
||||
const { Command ,Option} = require('commander');
|
||||
|
||||
const dayjs = require("dayjs");
|
||||
const relativeTime = require("dayjs/plugin/relativeTime");
|
||||
const { rejects } = require("assert");
|
||||
const { Console } = require("console");
|
||||
dayjs.extend(relativeTime);
|
||||
require('dayjs/locale/zh-CN')
|
||||
dayjs.locale("zh-CN");
|
||||
|
||||
const logger = createLogger();
|
||||
logger.use(TaskListPlugin)
|
||||
logger.use(TablePlugin)
|
||||
|
||||
const program =new Command()
|
||||
|
||||
// 排除要发布的包
|
||||
const exclude_packages = ["autopublish"]
|
||||
|
||||
function getPackages(){
|
||||
let workspaceRoot = process.cwd()
|
||||
if(!fs.existsSync(path.join(workspaceRoot,"pnpm-workspace.yaml"))){
|
||||
console.log("命令只能在工作区根目录下执行")
|
||||
return
|
||||
}
|
||||
// 获取包最后一次提交的时间
|
||||
const getLastCommitScript = "git log --format=%cd --date=iso -1 -- {packagePath}"
|
||||
return fs.readdirSync(path.join(workspaceRoot,"packages")).map(packageName=>{
|
||||
// 读取所有包
|
||||
let packages = fs.readdirSync(path.join(workspaceRoot,"packages")).map(packageName=>{
|
||||
const packageFolder = path.join(workspaceRoot,"packages",packageName)
|
||||
const pkgFile = path.join(workspaceRoot,"packages",packageName,"package.json")
|
||||
if(fs.existsSync(pkgFile)){
|
||||
const { name, version }= fs.readJSONSync(pkgFile)
|
||||
const lastCommit = shelljs.exec(getLastCommitScript.replace("{packagePath}",`packages/${packageName}/package.json`), { silent: true }).stdout.trim()
|
||||
const { name, version,lastPublish,dependencies,devDependencies }= fs.readJSONSync(pkgFile)
|
||||
// 读取工作区包依赖
|
||||
let packageDependencies =[]
|
||||
Object.entries({...dependencies,...devDependencies}).forEach(([name,version])=>{
|
||||
if(version.startsWith("workspace:") && !exclude_packages.includes(name.replace("@voerkai18n/",""))){
|
||||
packageDependencies.push(name)
|
||||
}
|
||||
})
|
||||
return {
|
||||
name,
|
||||
name, // 完整包名
|
||||
value:packageName, // 文件夹名称
|
||||
version,
|
||||
lastCommit
|
||||
lastPublish,
|
||||
isDirty: packageIsDirty(packageFolder), // 包自上次发布之后是否已修改
|
||||
dependencies:packageDependencies // 依赖的工作区包
|
||||
}
|
||||
}
|
||||
}).filter(pkgInfo=>pkgInfo)
|
||||
}).filter(pkgInfo=>pkgInfo && !exclude_packages.includes(pkgInfo.value))
|
||||
|
||||
// 根据依赖关系进行排序
|
||||
for(let i=0;i<packages.length;i++){
|
||||
for(let j=i;j<packages.length;j++){
|
||||
let pkgInfo2 = packages[j]
|
||||
if( packages[i].dependencies.includes(pkgInfo2.name)){
|
||||
let p = packages[i]
|
||||
packages[i] = packages[j]
|
||||
packages[j] = p
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果某个包isDirty=true,则依赖于其的其他包isDirty=true
|
||||
packages.forEach(package => {
|
||||
if(package.isDirty){
|
||||
packages.forEach(p=>{
|
||||
if(p.name!==package.name && p.dependencies.includes(package.name)){
|
||||
p.isDirty = true
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
return packages
|
||||
}
|
||||
|
||||
function assertInWorkspaceRoot(){
|
||||
@ -66,162 +106,334 @@
|
||||
throw new Error("命令只能在工作区根目录下执行")
|
||||
}
|
||||
}
|
||||
|
||||
function assertInPackageRoot(){
|
||||
const currentFolder = process.cwd()
|
||||
const workspaceRoot = path.join(currentFolder,"../../")
|
||||
|
||||
const inPackageRoot = fs.existsSync(path.join(currentFolder,"package.json")) && fs.existsSync(path.join(workspaceRoot,"pnpm-workspace.yaml"))
|
||||
|
||||
/**
|
||||
* 返回当前包是否有未提交的文件
|
||||
*
|
||||
* 如果包文件夹下有未提交的文件,则返回true
|
||||
*
|
||||
*/
|
||||
function getPackageLastChanges(packageName){
|
||||
const changeFiles = shelljs.exec(`git status -s .`, { silent: true }).stdout.trim()
|
||||
return changeFiles.length>0 ? changeFiles.split("\n") : []
|
||||
}
|
||||
if(!inPackageRoot){
|
||||
throw new Error("命令只能在工作区的包目录下执行")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行脚本,出错会返回错误信息
|
||||
* @param {*} script
|
||||
*/
|
||||
function execShellScript(script,options={}){
|
||||
if(shelljs.exec(script).code>0){
|
||||
throw new Error(`执行<${script}>失败`)
|
||||
let {code,stdout} = shelljs.exec(script,options)
|
||||
if(code>0){
|
||||
new Error(`执行<${script}>失败: ${stdout.trim()}`)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 异步执行脚本
|
||||
* @param {*} script
|
||||
* @param {*} options
|
||||
* @returns
|
||||
*/
|
||||
async function asyncExecShellScript(script,options={}){
|
||||
return new Promise((resolve,reject)=>{
|
||||
shelljs.exec(script,{...options,async:true},(code,stdout)=>{
|
||||
if(code>0){
|
||||
reject(new Error(`执行<${script}>失败: ${stdout.trim()}`))
|
||||
}else{
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 执行脚本并返回结果
|
||||
* @param {*} script
|
||||
*/
|
||||
function execShellScriptReturns(script,options={}){ ){
|
||||
return shelljs.exec(script,options).code>0).stdout.trim()
|
||||
function execShellScriptReturns(script,options={}){
|
||||
return shelljs.exec(script,options).stdout.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取指定包最近一次更新的时间
|
||||
* 通过遍历所有文件夹
|
||||
* @param {*} folder
|
||||
* @returns
|
||||
*/
|
||||
function getFolderLastModified(folder,patterns=[],options={}){
|
||||
patterns.push(...[
|
||||
"package.json",
|
||||
"**",
|
||||
"**/*",
|
||||
"!node_modules/**",
|
||||
"!node_modules/**/*",
|
||||
"!**/node_modules/**",
|
||||
"!**/node_modules/**/*",
|
||||
])
|
||||
|
||||
const glob = require("fast-glob")
|
||||
let files = glob.sync(patterns, {
|
||||
cwd: folder,
|
||||
absolute:true,
|
||||
...options
|
||||
})
|
||||
let lastUpdateTime = null
|
||||
for(let file of files){
|
||||
const { mtimeMs } = fs.statSync(file)
|
||||
lastUpdateTime = lastUpdateTime ? Math.max(lastUpdateTime,mtimeMs) : mtimeMs
|
||||
}
|
||||
return lastUpdateTime
|
||||
}
|
||||
|
||||
function getFileLastModified(file){
|
||||
const { mtimeMs } = fs.statSync(file)
|
||||
return mtimeMs
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} packageInfo {name:"@voerkai18n/autopublish",value:"",version:"1.0.0",lastPublish:"2020-05-01T00:00:00.000Z"}
|
||||
*/
|
||||
async function runPackageScript(workspaceRoot,packageInfo,{silent=false}={}){
|
||||
const packageFolder = path.join(workspaceRoot,"packages",packageInfo.value)
|
||||
const package = fs.readJSONSync(path.join(packageFolder,"package.json"))
|
||||
const lastModified = getFolderLastModified(packageFolder)
|
||||
// 进入包所在的文件夹
|
||||
shelljs.cd(packageFolder)
|
||||
// 每个包必须定义自己的发布脚本
|
||||
if("release" in package.scripts){
|
||||
await asyncExecShellScript(`pnpm release`,{silent})
|
||||
}else{
|
||||
const reason = `包[{}]没有定义自动发布脚本release`
|
||||
if(showLog) logger.log(reason,package.name)
|
||||
throw new Error(`未配置<release>脚本`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 执行Git提交命令
|
||||
*
|
||||
* 1. 检查当前包是否有未提交的文件
|
||||
* 2. 如果没有则不提交
|
||||
* 3. 如果有则提交
|
||||
* 返回指定包自上次发布之后是否有更新过
|
||||
*
|
||||
* @param {*} packageFolder
|
||||
*
|
||||
*
|
||||
*/
|
||||
function commitProject(packageName,{versionIncrementStep="patch",autoCommit=false}={}){
|
||||
const lastChanges = getPackageLastChanges(package.name)
|
||||
let lastCommit = shelljs.exec(`git log --format=%cd --date=iso -1 -- .`, { silent: true }).stdout.trim()
|
||||
let hasError = false // 执行过程是否出错了
|
||||
let isCommit = autoCommit // 是否执行了提交操作
|
||||
function packageIsDirty(packageFolder){
|
||||
const pkgFile = path.join(packageFolder,"package.json")
|
||||
if(!fs.existsSync(pkgFile)){
|
||||
logger.log("当前包[{}]不存在package.json文件",packageFolder)
|
||||
throw new Error("当前包不存在package.json文件")
|
||||
}
|
||||
const package = fs.readJSONSync(pkgFile)
|
||||
const lastModified = getFolderLastModified(packageFolder)
|
||||
const lastPublish = package.lastPublish
|
||||
// 由于上一次发布时会更新package.json文件,如果最后更新的文件时间==package.json文件最后更新时间,则说明没有更新
|
||||
const pkgLastModified = getFileLastModified(pkgFile)
|
||||
|
||||
if(lastCommit){
|
||||
lastCommit = dayjs(lastCommit)
|
||||
logger.log("最后一次提交:{}({})",lastCommit.format("YYYY-MM-DD HH:mm:ss"),lastCommit.fromNow())
|
||||
}
|
||||
if(lastChanges.length>0){
|
||||
logger.log("包[{}]存在{}个未提交的文件:",package.name,lastChanges.length)
|
||||
lastChanges.forEach(file=>logger.log(` - ${file.trim()}`))
|
||||
if(!autoCommit){
|
||||
const result = await inquirer.prompt({
|
||||
name:"isCommit",
|
||||
type:"confirm",
|
||||
message:"是否提交以上文件到仓库?"
|
||||
})
|
||||
isCommit = result.isCommit
|
||||
}
|
||||
if(isCommit){
|
||||
execShellScript(`git commit -a -m "Update ${package.name}"`)
|
||||
}
|
||||
}
|
||||
return dayjs(lastModified).isAfter(dayjs(lastPublish)) && !dayjs(pkgLastModified).isSame(dayjs(lastModified))
|
||||
}
|
||||
|
||||
let VERSION_STEPS = ["major", "minor", "patch","premajor","preminor","prepatch","prerelease"]
|
||||
program
|
||||
.command("publish")
|
||||
.description("发布当前工作区下的包")
|
||||
.option("-p, --package-name", "包名称")
|
||||
.option("-f, --force", "强制发布")
|
||||
.option("--no-auto-commit", "不提交源码")
|
||||
.option("-q, --query", "询问是否发布,否则会自动发布")
|
||||
.option("--no-add-version-tag", "不添加版本标签")
|
||||
.addOption(new Option('-i, --version-increment-step [value]', '版本增长方式').default("patch").choices(VERSION_STEPS))
|
||||
.action(async (options) => {
|
||||
console.log(JSON.stringify(options))
|
||||
const {versionIncrementStep,autoCommit,addVersionTag} = options
|
||||
|
||||
const packageFolder = process.cwd()
|
||||
const packageName = path.basename(packageFolder)
|
||||
const pkgFile = path.join(packageFolder,"package.json")
|
||||
const package = fs.readJSONSync(pkgFile)
|
||||
const packageBackup = Object.assign({},package) // 备份package.json,当操作失败时,还原
|
||||
|
||||
logger.log("将发布包:{}",`${packageName}`)
|
||||
|
||||
// 第一步: 提交代码
|
||||
commitProject(package,options)
|
||||
|
||||
// 第二步: 更新版本号和发布时间
|
||||
package.version = semver.inc(package.version,versionIncrementStep)
|
||||
package.lastPublish = dayjs().format()
|
||||
fs.writeJSONSync(pkgFile,package)
|
||||
/**
|
||||
* 发布所有包
|
||||
*
|
||||
* 将比对最后发布时间和最后修改时间的差别来决定是否发布
|
||||
*
|
||||
*
|
||||
* @param {*} packages
|
||||
*/
|
||||
async function publishAllPackages(packages,options={}){
|
||||
const tasks = logger.tasklist()
|
||||
const workspaceRoot = process.cwd()
|
||||
// 依次对每个包进行发布
|
||||
for(let package of packages){
|
||||
tasks.add(`发布包[${package.name}]`)
|
||||
try{
|
||||
if(package.isDirty){
|
||||
await runPackageScript(workspaceRoot,package,{silent:true,...options})
|
||||
let { version } = fs.readJSONSync(path.join(workspaceRoot,"packages",package.value,"package.json"))
|
||||
tasks.complete(`${package.version}->${version}`)
|
||||
}else{
|
||||
tasks.skip()
|
||||
}
|
||||
}catch(e){
|
||||
tasks.error(`${e.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 第三步:执行发布到Npm
|
||||
|
||||
/**
|
||||
* 发布包,并且在package.json中记录最后发布时间
|
||||
* 本命令只能在包文件夹下执行
|
||||
* @param {*} options
|
||||
*/
|
||||
async function publishPackage(options){
|
||||
const { versionIncrementStep,silent=true } = options
|
||||
|
||||
// 此命令需要切换到包所在目录
|
||||
const packageFolder = process.cwd()
|
||||
const packageName = path.basename(packageFolder)
|
||||
const pkgFile = path.join(packageFolder,"package.json")
|
||||
|
||||
if(!fs.existsSync(pkgFile)){
|
||||
logger.log("当前包[{}]不存在package.json文件",packageName)
|
||||
throw new Error("当前包不存在package.json文件,请在包文件夹下执行")
|
||||
}
|
||||
|
||||
let package = fs.readJSONSync(pkgFile)
|
||||
const oldVersion = package.version
|
||||
let packageBackup = Object.assign({},package) // 备份package.json,当操作失败时,还原
|
||||
|
||||
logger.log("发布包:{}",`@voerkai18n/${packageName}`)
|
||||
|
||||
const tasks = logger.tasklist()
|
||||
|
||||
try{
|
||||
// 第一步: 更新版本号和发布时间
|
||||
tasks.add("更新版本号")
|
||||
await asyncExecShellScript(`npm version ${versionIncrementStep}`,{silent})
|
||||
// 重新读取包
|
||||
package = fs.readJSONSync(pkgFile)
|
||||
packageBackup = Object.assign({},package)
|
||||
tasks.complete(`${oldVersion}->${package.version}`)
|
||||
|
||||
// 第二步:构建包
|
||||
if("build" in package.scripts){
|
||||
tasks.add("构建包")
|
||||
await asyncExecShellScript(`pnpm build`,{silent})
|
||||
tasks.complete()
|
||||
}
|
||||
|
||||
|
||||
// 第三步:发布
|
||||
// 由于工程可能引用了工作区内的其他包,必须pnpm publish才能发布
|
||||
// pnpm publish会修正引用工作区其他包到的依赖信息,而npm publish不能识别工作区内的依赖,会导致报错
|
||||
try{
|
||||
execShellScript(`pnpm publish --no-git-checks --access publish`)
|
||||
// 当发布完毕后,由于更新了publish,因此需要重新提交代码
|
||||
}catch{
|
||||
fs.writeJSONSync(pkgFile,packageBackup)
|
||||
}
|
||||
tasks.add("发布包")
|
||||
await asyncExecShellScript(`pnpm publish --no-git-checks --access publish`,{silent})
|
||||
tasks.complete()
|
||||
|
||||
})
|
||||
|
||||
program
|
||||
.command("list")
|
||||
.description("列出各个包的最后一次提交时间和版本信息")
|
||||
.action(options => {
|
||||
assertInWorkspaceRoot()
|
||||
// 第四步:更新发布时间
|
||||
tasks.add("更新发布时间")
|
||||
package.lastPublish = dayjs().format()
|
||||
fs.writeFileSync(pkgFile,JSON.stringify(package,null,4))
|
||||
tasks.complete()
|
||||
|
||||
}catch(e){// 如果发布失败,则还原package.json
|
||||
fs.writeFileSync(pkgFile,JSON.stringify(packageBackup,null,4))
|
||||
tasks.error(`${e.message}`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
program
|
||||
.command("list")
|
||||
.description("列出各个包的最后一次提交时间和版本信息")
|
||||
.action(options => {
|
||||
assertInWorkspaceRoot()
|
||||
workspaceRoot = process.cwd()
|
||||
const table = logger.table({grid:1})
|
||||
table.addHeader("包名","版本号","最后提交时间","最后修改时间")
|
||||
getPackages().forEach(package => {
|
||||
if(package.lastCommit){
|
||||
console.log(`${package.name.padEnd(16)}\tVersion: ${package.version.padEnd(12)} lastCommit: ${dayjs(package.lastCommit).format("YYYY/MM/DD hh:mm:ss")}(${dayjs(package.lastCommit).fromNow()}) `)
|
||||
const lastPublish = package.lastPublish ? dayjs(package.lastPublish).format("MM/DD hh:mm:ss") : "None"
|
||||
const lastPublishRef = package.lastPublish ? `(${dayjs(package.lastPublish).fromNow()})` : ""
|
||||
const lastModified = getFolderLastModified(path.join(workspaceRoot,"packages",package.value))
|
||||
const lastUpdate = dayjs(lastModified).format("MM/DD hh:mm:ss")
|
||||
const lastUpdateRef = dayjs(lastModified).fromNow()
|
||||
if(package.lastPublish){
|
||||
table.addRow(package.name,package.version,`${lastPublish}(${lastPublishRef})`,`${lastUpdate}(${lastUpdateRef})`)
|
||||
}else{
|
||||
console.log(`${package.name.padEnd(16)}\tVersion: ${package.version.padEnd(12)} lastCommit: None `)
|
||||
table.addRow(package.name,package.version,"None",`${lastUpdate}(${lastUpdateRef})`)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
||||
program.parseAsync(process.argv);
|
||||
table.render()
|
||||
})
|
||||
|
||||
|
||||
// inquirer
|
||||
// .prompt([
|
||||
// {
|
||||
// type: "confirm",
|
||||
// name: "autoPublish",
|
||||
// message: "是否自动发布?",
|
||||
// default: true,
|
||||
// },
|
||||
// {
|
||||
// type: "checkbox",
|
||||
// name: "selectPackages",
|
||||
// message: "请选择要发布的库:",
|
||||
// choices: packages,
|
||||
// when: function (answer) {
|
||||
// return !answer.autoPublish;
|
||||
// },
|
||||
// },
|
||||
// ])
|
||||
// .then((answers) => {
|
||||
// console.log(answers);
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// if (error.isTtyError) {
|
||||
// // Prompt couldn't be rendered in the current environment
|
||||
// } else {
|
||||
// // Something else went wrong
|
||||
// }
|
||||
// });
|
||||
|
||||
async function answerForSelectPackages(packages,options){
|
||||
const workspaceRoot = process.cwd()
|
||||
return new Promise((resolve,reject) => {
|
||||
inquirer
|
||||
.prompt([
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "selectPackages",
|
||||
message: "请选择要发布的库:",
|
||||
choices: packages.map(package => {
|
||||
const lastPublish = package.lastPublish ? dayjs(package.lastPublish).format("MM/DD hh:mm:ss") : "None"
|
||||
const lastPublishRef = package.lastPublish ? `(${dayjs(package.lastPublish).fromNow()})` : ""
|
||||
const lastModified = getFolderLastModified(path.join(workspaceRoot,"packages",package.value))
|
||||
const lastUpdate = dayjs(lastModified).format("MM/DD hh:mm:ss")
|
||||
const lastUpdateRef = dayjs(lastModified).fromNow()
|
||||
return {
|
||||
...package,
|
||||
value: package,
|
||||
name:`${package.name.padEnd(24)}Version: ${package.version.padEnd(8)} LastPublish: ${lastPublish.padEnd(16)}${lastPublishRef} lastModified: ${lastUpdate}(${lastUpdateRef})`, }
|
||||
}),
|
||||
pageSize:12,
|
||||
loop: false
|
||||
},
|
||||
])
|
||||
.then((answers) => {
|
||||
resolve(answers.selectPackages)
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.log(error.message)
|
||||
reject(error)
|
||||
});
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 发布包的模式
|
||||
*
|
||||
* 1. 在包中使用
|
||||
* {
|
||||
* scripts:{
|
||||
* "release":"pnpm autopublish"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* 2. 发布所有包
|
||||
* {
|
||||
* scripts:{
|
||||
* "autopublish":"pnpm autopublish -a"
|
||||
* }
|
||||
* }
|
||||
* > pnpm autopublish -- -a // 自动发布,会询问要发布的
|
||||
* > pnpm autopublish -- -a --no-ask // 自动发布,不会询问全自动发布
|
||||
*
|
||||
*/
|
||||
const VERSION_STEPS = ["major", "minor", "patch","premajor","preminor","prepatch","prerelease"]
|
||||
program
|
||||
.description("自动发布包")
|
||||
.option("-a, --all", "发布所有包")
|
||||
.option("-n, --no-ask", "不询问")
|
||||
.option("-s, --no-silent", "静默显示脚本输出")
|
||||
.addOption(new Option('-i, --version-increment-step [value]', '版本增长方式').default("patch").choices(VERSION_STEPS))
|
||||
.action(async (options) => {
|
||||
// 发布所有包时只能在工作区根目录下执行
|
||||
if(options.all){
|
||||
assertInWorkspaceRoot()
|
||||
}else{// 发布指定包时只能在包目录下执行
|
||||
assertInPackageRoot()
|
||||
}
|
||||
if(options.all){ // 自动发布所有包
|
||||
const workspaceRoot = process.cwd()
|
||||
let packages = getPackages()
|
||||
if(options.ask){
|
||||
packages = await answerForSelectPackages(packages,options)
|
||||
}
|
||||
if(packages.length > 0){
|
||||
await publishAllPackages(packages,options)
|
||||
}
|
||||
}else{// 只发布指定的包
|
||||
await publishPackage(options)
|
||||
}
|
||||
})
|
||||
|
||||
program.parseAsync(process.argv);
|
||||
|
||||
|
@ -1,20 +1,20 @@
|
||||
{
|
||||
"name": "@voerkai18n/publish",
|
||||
"version": "1.0.1",
|
||||
"description": "发布项目工具",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"release": "npm version patch && pnpm publish --no-git-checks --access public",
|
||||
"postpublish": "git push --follow-tags && npm run build && npm publish"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"publish": "./index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": "^9.0.0",
|
||||
"semver": "^7.3.5"
|
||||
}
|
||||
}
|
||||
"name": "@voerkai18n/autopublish",
|
||||
"version": "1.0.2",
|
||||
"description": "自动发布工作区的包",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"release": "node ./index.js publish"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"autopublish": "./index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": "^9.0.0",
|
||||
"fast-glob": "^3.2.11"
|
||||
},
|
||||
"lastPublish": "2022-04-06T15:36:15+08:00"
|
||||
}
|
@ -1,5 +1,111 @@
|
||||
|
||||
|
||||
# 概述
|
||||
|
||||
`@voerkai18n`项目是一个标准的`monorepo`包工程,包含了`@voerkai18n/cli`、`@voerkai18n/runtime`、`@voerkai18n/utils`、`@voerkai18n/vue`、`@voerkai18n/vite`、`@voerkai18n/babel`、`@voerkai18n/react`、`@voerkai18n/formatters`等多个包,发布包时容易引起混乱问题,最大问题时:
|
||||
- 经常忘记哪个包最近什么时间修改,哪个包应该发布。
|
||||
- 由于包之间存在依赖关系,需要按一定的顺序进行发布
|
||||
|
||||
`@voerkai18n/autopublish`用来实现全自动或手动辅助进行发布。
|
||||
|
||||
**源码与文档:**[https://gitee.com/zhangfisher/voerka-i18n](https://gitee.com/zhangfisher/voerka-i18n)
|
||||
|
||||
[](https://gitee.com/zhangfisher/voerka-i18n)
|
||||
|
||||
`@voerkai18n/autopublish`辅助进行自动发布
|
||||
# 使用
|
||||
|
||||
源码与文档:[https://gitee.com/zhangfisher/voerka-i18n](https://gitee.com/zhangfisher/voerka-i18n)
|
||||
## 准备
|
||||
|
||||
`@voerkai18n/autopublish`用于采用`pnpmp`创建的`monorepo`包工程,不支持`lerna/yarn`。
|
||||
按照常规约定,包存放在`<projectRoot>/packages/<name>`。
|
||||
|
||||
## 第一步:配置包的发布脚本
|
||||
|
||||
将`@voerkai18n/autopublish`添加为包的开发依赖。
|
||||
|
||||
```javascript
|
||||
// 进入包文件夹后执行
|
||||
> pnpm add -D @voerkai18n/autopublish
|
||||
```
|
||||
然后,配置发布脚本:
|
||||
```json
|
||||
{
|
||||
"scripts":{
|
||||
"build":"默认的包构建命令",
|
||||
"release":"pnpm autopublish"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@voerkai18n/autopublish": "workspace:^1.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 发布脚本必须为`release`,不能是其他名称,特别是`publish`
|
||||
- `pnpm autopublish`也可以在包路径下单独执行。
|
||||
- `pnpm autopublish`默认依次执行:
|
||||
- `npm version patch`:升级版本号
|
||||
- `pnpm run build`(可选)
|
||||
- `pnpm publish --no-git-checks --access publish`
|
||||
- 默认每次发布均会升级`patch`版本号,可以通过`pnpm autopublish -i <版本递增方式>`来增加版本号,递增方式可选:[`"major"`, `"minor"`, `"patch"`,`"premajor"`,`"preminor"`,`"prepatch"`,`"prerelease"`]
|
||||
- 每次执行`pnpm autopublish`均会在当前包的`package.json`中添加`lastPublish`字段,用来记录发布的时间。这是下一次发布时进行自动比对发布的依据。
|
||||
|
||||
|
||||
## 第二步:配置工作区发布脚本
|
||||
|
||||
在当前工程的根文件夹下配置`package.json`
|
||||
```json
|
||||
{
|
||||
"scripts":{
|
||||
"list:package": "node ./packages/autopublish/index.js list",
|
||||
"autopublish": "node ./packages/autopublish/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@voerkai18n/autopublish": "workspace:^1.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
## 第三步:自动发布所有包
|
||||
|
||||
```javascript
|
||||
> pnpm autopublish -- -a -n
|
||||
```
|
||||
|
||||
`pnpm autopublish`会自动枚举出当前所有包,然后对比包路径`packages/<包名>`最后修改时间和`package.json`的`lastPublish`字段值,如果:
|
||||
- 最后修改时间大于最后发布时间,则发布该包
|
||||
- 最后修改时间关于或者小于最后发布时间,则忽略发布该包
|
||||
|
||||
因此,每次当修改完工程后,可以自动执行`pnpm autopublish -- -a -n`就可以进行全自动发布。
|
||||
|
||||
`pnpm autopublish`命令行参数:
|
||||
```shell
|
||||
自动发布包
|
||||
|
||||
Options:
|
||||
-a, --all 发布所有包
|
||||
-n, --no-ask 不询问
|
||||
-s, --no-silent 静默显示脚本输出
|
||||
-i, --version-increment-step [value] 版本增长方式 (choices: "major", "minor", "patch", "premajor", "preminor", "prepatch",
|
||||
"prerelease", default: "patch")
|
||||
-h, --help display help for command
|
||||
|
||||
Commands:
|
||||
list 列出各个包的最后一次提交时间和版本信息
|
||||
```
|
||||
|
||||
- `-a`代表要发布所有包,如果没有启用`-n`,则会让用户选择要发布哪一个包。如果启用`-n`参数,则会全自动比对发布时间和修改时间后发布。
|
||||
- `-no-ask`代表不会询问让用户选择要发布的包.
|
||||
- `--no-silent`代表是否不输出脚本输出。
|
||||
- 由于包之间存在依赖关系,`autopublish`会根据依赖关系进行排序发布和关联发布。比如`@voerkai18n/cli`依赖于`@voerkai18n/utils`,当`@voerkai18n/utils`有更新需要发布时,`@voerkai18n/cli`也会自动发布。
|
||||
|
||||
|
||||
## 第四步: 手动选择发布
|
||||
|
||||
`pnpm autopublish -- -a -n`会根据发布时间和修改时间进行自动发布。也支持手动选择要发布的包。
|
||||
```javascript
|
||||
> pnpm autopublish -- -a
|
||||
````
|
||||
如果不启用`-n`参数,则会列出当前工作区的所有包,让用户选择要发布的包。
|
||||
|
||||
## 列出包
|
||||
|
||||
`pnpm autopublish -- list`列出当前工程的所有包,并显示当前包最近更新和最近发布时间。
|
@ -11,9 +11,15 @@
|
||||
*
|
||||
* {
|
||||
* plugins:[
|
||||
* ["voerkai18n",{}]
|
||||
* ["voerkai18n",{
|
||||
* location:"./languages",
|
||||
* autoImport:"./languages",
|
||||
* moduleType:"esm"
|
||||
* }]
|
||||
* ]
|
||||
*
|
||||
*
|
||||
*
|
||||
* }
|
||||
*
|
||||
*
|
||||
|
@ -1,20 +1,24 @@
|
||||
{
|
||||
"name": "@voerkai18n/babel",
|
||||
"version": "1.0.4",
|
||||
"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",
|
||||
"release": "npm version patch && pnpm publish --no-git-checks --access public"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@voerkai18n/utils": "workspace:^1.0.0"
|
||||
}
|
||||
}
|
||||
"name": "@voerkai18n/babel",
|
||||
"version": "1.0.21",
|
||||
"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",
|
||||
"release": "pnpm autopublish"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@voerkai18n/utils": "workspace:^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@voerkai18n/autopublish": "workspace:^1.0.2"
|
||||
},
|
||||
"lastPublish": "2022-04-10T17:21:40+08:00"
|
||||
}
|
2556
packages/cli/available_languages.js
Normal file
2556
packages/cli/available_languages.js
Normal file
File diff suppressed because it is too large
Load Diff
59
packages/cli/baidu.translate.js
Normal file
59
packages/cli/baidu.translate.js
Normal file
@ -0,0 +1,59 @@
|
||||
const axios = require("axios")
|
||||
const md5 = require("md5");
|
||||
const qs = require("qs");
|
||||
|
||||
|
||||
/**
|
||||
* q string 是 请求翻译query UTF-8编码
|
||||
from string 是 翻译源语言 可设置为auto
|
||||
to string 是 翻译目标语言 不可设置为auto
|
||||
appid string 是 APPID 可在管理控制台查看
|
||||
salt string 是 随机数 可为字母或数字的字符串
|
||||
sign string 是 签名 appid+q+salt+密钥的MD5值
|
||||
*
|
||||
*
|
||||
*/
|
||||
module.exports = function(options={}){
|
||||
const { appkey,appid ,baseurl = "http://api.fanyi.baidu.com/api/trans/vip/translate" } = options;
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {*} texts 多条文本
|
||||
* @returns
|
||||
*/
|
||||
translate:async (texts=[],from,to)=>{
|
||||
|
||||
if(Array.isArray(texts)){
|
||||
texts = texts.join("\n");
|
||||
}
|
||||
// saltStep1. 拼接字符串1:
|
||||
// 拼接[appid=2015063000000001][q=apple][salt=1435660288][密钥=12345678]得到字符串1:“2015063000000001apple143566028812345678”
|
||||
// Step2. 计算签名:(对字符串1做MD5加密)
|
||||
// sign=MD5(2015063000000001apple143566028812345678),得到sign=f89f9594663708c1605f3d736d01d2d4
|
||||
const salt = new Date().getTime()
|
||||
const sign = md5(`${appid}${texts}${salt}${appkey}`);
|
||||
|
||||
let params = qs.stringify({
|
||||
q:texts,
|
||||
from,
|
||||
to,
|
||||
appid,
|
||||
salt,
|
||||
sign
|
||||
});
|
||||
return new Promise((resolve,reject)=>{
|
||||
axios.get(`${baseurl}?${params}`).then(res=>{
|
||||
const { data } = res;
|
||||
if(data.error_code){
|
||||
reject(data.error_msg)
|
||||
}else{
|
||||
resolve(res.data.trans_result.map(item=>item.dst));
|
||||
}
|
||||
}).catch(err=>{
|
||||
reject(err);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -11,11 +11,11 @@
|
||||
* - languages
|
||||
* translates
|
||||
* - en.json
|
||||
* - cn.json
|
||||
* - zh.json
|
||||
* - ...
|
||||
* idMap.js // id映射列表
|
||||
* settings.json // 配置文件
|
||||
* cn.js // 中文语言包
|
||||
* zh.js // 中文语言包
|
||||
* en.js // 英文语言包
|
||||
* [lang].js // 其他语言包
|
||||
*
|
||||
@ -25,7 +25,7 @@
|
||||
const glob = require("glob")
|
||||
const createLogger = require("logsets")
|
||||
const path = require("path")
|
||||
const { findModuleType,getCurrentPackageJson,installVoerkai18nRuntime,isInstallDependent} = require("@voerkai18n/utils")
|
||||
const { findModuleType,getCurrentPackageJson,installVoerkai18nRuntime,isInstallDependent,updateVoerkai18nRuntime} = require("@voerkai18n/utils")
|
||||
const { t } = require("./i18nProxy")
|
||||
const fs = require("fs-extra")
|
||||
const logger = createLogger()
|
||||
@ -126,10 +126,11 @@ module.exports =async function compile(langFolder,opts={}){
|
||||
logger.log(t(" - 运行时: {}"),"runtime.js")
|
||||
}else{//如果不嵌入则需要安装运行时依赖
|
||||
if(!isInstallDependent("@voerkai18n/runtime")){
|
||||
installVoerkai18nRuntime(langFolder,moduleType)
|
||||
installVoerkai18nRuntime(langFolder)
|
||||
logger.log(t(" - 安装运行时: {}"),"@voerkai18n/runtime")
|
||||
}else{
|
||||
logger.log(t(" - 运行时{}已安装"),"@voerkai18n/runtime")
|
||||
updateVoerkai18nRuntime(langFolder)
|
||||
logger.log(t(" - 更新运行时:{}"),"@voerkai18n/runtime")
|
||||
}
|
||||
}
|
||||
const templateContext = {
|
||||
|
@ -1,4 +1,5 @@
|
||||
const { findModuleType,t } = require("./utils")
|
||||
const { findModuleType } = require("@voerkai18n/utils")
|
||||
const { t } = require("./i18nProxy")
|
||||
const path = require("path")
|
||||
const fs = require("fs")
|
||||
const gulp = require("gulp")
|
||||
|
@ -96,7 +96,7 @@ function inNamespace(filePath,nsPath){
|
||||
* @param {*} content
|
||||
* @param {*} file
|
||||
* @param {*} options
|
||||
* @returns {namespace:{text:{cn:"",en:"",...,$file:""},text:{cn:"",en:"",...,$file:""}}
|
||||
* @returns {namespace:{text:{zh:"",en:"",...,$file:""},text:{zh:"",en:"",...,$file:""}}
|
||||
*/
|
||||
function extractTranslateTextUseRegexp(content,namespace,extractor,file,options){
|
||||
|
||||
@ -179,7 +179,7 @@ function getFileTypeExtractors(filetype,extractor){
|
||||
}
|
||||
/**
|
||||
* 找出要翻译的文本列表 {namespace:[text,text],...}
|
||||
* {namespace:{text:{cn:"",en:"",$source:""},...}
|
||||
* {namespace:{text:{zh:"",en:"",$source:""},...}
|
||||
* @param {*} content
|
||||
* @param {*} extractor
|
||||
* @returns
|
||||
@ -219,7 +219,7 @@ function getTranslateTexts(content,file,options){
|
||||
|
||||
const defaultExtractLanguages = [
|
||||
{name:'en',title:"英文"},
|
||||
{name:'cn',title:"中文",default:true},
|
||||
{name:'zh',title:"中文",default:true},
|
||||
{name:'de',title:"德语"},
|
||||
{name:'fr',title:"法语"},
|
||||
{name:'es',title:"西班牙语"},
|
||||
@ -231,8 +231,8 @@ function normalizeLanguageOptions(options){
|
||||
options = Object.assign({
|
||||
debug : true, // 输出调试信息,控制台输出相关的信息
|
||||
languages :defaultExtractLanguages, // 默认要支持的语言
|
||||
defaultLanguage: "cn", // 默认语言:指的是在源代码中的原始文本语言
|
||||
activeLanguage : "cn", // 当前激活语言:指的是当前启用的语言,比如在源码中使用中文,在默认激活的是英文
|
||||
defaultLanguage: "zh", // 默认语言:指的是在源代码中的原始文本语言
|
||||
activeLanguage : "zh", // 当前激活语言:指的是当前启用的语言,比如在源码中使用中文,在默认激活的是英文
|
||||
extractor : { // 匹配翻译函数并提取内容的正则表达式
|
||||
"*" : DefaultTranslateExtractor,
|
||||
"html,vue,jsx" : DefaultHtmlAttrExtractor
|
||||
@ -258,7 +258,7 @@ function normalizeLanguageOptions(options){
|
||||
}else{
|
||||
options.output = Object.assign({},{updateMode: 'sync',path:null},options.output)
|
||||
}
|
||||
// 语言配置 languages = [{name:"en",title:"英文"},{name:"cn",title:"中文",active:true,default:true}]
|
||||
// 语言配置 languages = [{name:"en",title:"英文"},{name:"zh",title:"中文",active:true,default:true}]
|
||||
if(!Array.isArray(options.languages)){
|
||||
throw new TypeError("options.languages must be an array")
|
||||
}else{
|
||||
@ -368,7 +368,7 @@ function updateLanguageFile(newTexts,toLangFile,options){
|
||||
}
|
||||
Object.entries(newTexts).forEach(([text,sourceLangs])=>{
|
||||
if(text in oldTexts){ // 合并
|
||||
let targetLangs = oldTexts[text] //{cn:'',en:''}
|
||||
let targetLangs = oldTexts[text] //{zh:'',en:''}
|
||||
Object.entries(sourceLangs).forEach(([langName,sourceText])=>{
|
||||
if(langName.startsWith("$")) return // 以$开头的为保留字段,不是翻译内容
|
||||
const langExists = langName in targetLangs
|
||||
|
@ -1,13 +1,30 @@
|
||||
const { Command } = require('commander');
|
||||
const createLogger = require("logsets")
|
||||
const bannerPluin = require("logsets/plugins/banner")
|
||||
|
||||
const path = require("path")
|
||||
const fs = require("fs-extra")
|
||||
const logger = createLogger()
|
||||
const { i18nScope ,t } = require("./i18nScope")
|
||||
const { getProjectRootFolder, getProjectSourceFolder } = require("@voerkai18n/utils")
|
||||
const { i18nScope ,t } = require("./i18nProxy")
|
||||
const { getProjectRootFolder, getProjectSourceFolder } = require("@voerkai18n/utils");
|
||||
const { translate } = require('../runtime/dist/index.cjs');
|
||||
|
||||
logger.use(bannerPluin)
|
||||
|
||||
const program = new Command();
|
||||
program
|
||||
.name("voerkai18n")
|
||||
.option("-v, --version", "当前版本号")
|
||||
.helpOption('-h, --help', '显示帮助')
|
||||
.action((options) => {
|
||||
const banner = logger.banner()
|
||||
banner.add("VoerkaI18n CLI")
|
||||
banner.add("VoerkaI18n command line interactive tools",{style:"darkGray"})
|
||||
banner.add()
|
||||
banner.add("版本号:",`${require("./package.json").version}`,{style:["","yellow"]})
|
||||
banner.render()
|
||||
})
|
||||
|
||||
|
||||
program
|
||||
.command('init')
|
||||
@ -15,12 +32,12 @@ program
|
||||
.description(t('初始化项目国际化配置'))
|
||||
.option('-D, --debug', t('输出调试信息'))
|
||||
.option('-r, --reset', t('重新生成当前项目的语言配置'))
|
||||
.option('-lngs, --languages <languages...>', t('支持的语言列表'), ['cn','en'])
|
||||
.option('-d, --defaultLanguage <name>', t('默认语言'), 'cn')
|
||||
.option('-lngs, --languages <languages...>', t('支持的语言列表'), ['zh','en'])
|
||||
.option('-d, --defaultLanguage <name>', t('默认语言'), 'zh')
|
||||
// .option('-i, --installRuntime', t('自动安装默认语言'),true)
|
||||
.option('-a, --activeLanguage <name>', t('激活语言'), 'cn')
|
||||
.option('-a, --activeLanguage <name>', t('激活语言'), 'zh')
|
||||
.hook("preAction",async function(location){
|
||||
const lang= process.env.LANGUAGE || "cn"
|
||||
const lang= process.env.LANGUAGE || "zh"
|
||||
await i18nScope.change(lang)
|
||||
})
|
||||
.action((location,options) => {
|
||||
@ -39,16 +56,16 @@ program
|
||||
.command('extract')
|
||||
.description(t('扫描并提取所有待翻译的字符串到<languages/translates>文件夹中'))
|
||||
.option('-D, --debug', t('输出调试信息'))
|
||||
.option('-lngs, --languages <languages...>', t('支持的语言'), ['cn','en'])
|
||||
.option('-d, --defaultLanguage', t('默认语言'), 'cn')
|
||||
.option('-a, --activeLanguage', t('激活语言'), 'cn')
|
||||
.option('-lngs, --languages <languages...>', t('支持的语言'), ['zh','en'])
|
||||
.option('-d, --defaultLanguage', t('默认语言'), 'zh')
|
||||
.option('-a, --activeLanguage', t('激活语言'), 'zh')
|
||||
.option('-ns, --namespaces', t('翻译名称空间'))
|
||||
.option('-e, --exclude <folders>', t('排除要扫描的文件夹,多个用逗号分隔'))
|
||||
.option('-u, --updateMode', t('本次提取内容与已存在内容的数据合并策略,默认取值sync=同步,overwrite=覆盖,merge=合并'), 'sync')
|
||||
.option('-f, --filetypes', t('要扫描的文件类型'), 'js,vue,html,jsx,ts,mjs,cjs')
|
||||
.argument('[location]', t('工程项目所在目录'),"./")
|
||||
.hook("preAction",async function(location){
|
||||
const lang= process.env.LANGUAGE || "cn"
|
||||
const lang= process.env.LANGUAGE || "zh"
|
||||
await i18nScope.change(lang)
|
||||
})
|
||||
.action(async (location,options) => {
|
||||
@ -59,7 +76,7 @@ program
|
||||
logger.log(t("工程目录:{}"),location)
|
||||
const langSettingsFile = path.join(location,"languages","settings.json")
|
||||
if(fs.existsSync(langSettingsFile)){
|
||||
logger.log(t("语言配置文件<{}>已存在,将优先使用此配置文件中参数来提取文本","./languages/settings.json"))
|
||||
logger.log(t("语言配置文件<{}>已存在,将优先使用此配置文件中参数来提取文本"),"./languages/settings.json")
|
||||
let lngOptions = fs.readJSONSync(langSettingsFile)
|
||||
options.languages = lngOptions.languages
|
||||
options.defaultLanguage = lngOptions.defaultLanguage
|
||||
@ -74,12 +91,12 @@ program
|
||||
program
|
||||
.command('compile')
|
||||
.description(t('编译指定项目的语言包'))
|
||||
.option('-d, --debug', t('输出调试信息'))
|
||||
.option('-D, --debug', t('输出调试信息'))
|
||||
.option('--no-inline-runtime', t('不嵌入运行时源码'))
|
||||
.option('-m, --moduleType [types]', t('输出模块类型,取值auto,esm,cjs'), 'esm')
|
||||
.argument('[location]', t('工程项目所在目录'),"./")
|
||||
.hook("preAction",async function(location){
|
||||
const lang= process.env.LANGUAGE || "cn"
|
||||
const lang= process.env.LANGUAGE || "zh"
|
||||
await i18nScope.change(lang)
|
||||
})
|
||||
.action(async (location,options) => {
|
||||
@ -93,6 +110,29 @@ program
|
||||
compile(langFolder,options)
|
||||
});
|
||||
|
||||
program
|
||||
.command('translate')
|
||||
.argument('[location]', t('工程项目所在目录'))
|
||||
.description(t('调用在线翻译服务商的API翻译译指定项目的语言包,如使用百度云翻译服务'))
|
||||
.option('--no-backup', t('备份原始文件'))
|
||||
.option('--mode', t('翻译模式,取值auto=仅翻译未翻译的,full=全部翻译'), 'auto')
|
||||
.option('-p, --provider <value>', t('在线翻译服务提供者名称或翻译脚本文件'), 'baidu')
|
||||
.option('-m, --max-package-size <value>', t('将多个文本合并提交的最大包字节数'), 200)
|
||||
.option('--appkey [key]', t('API密钥'))
|
||||
.option('--appid [id]', t('API ID'))
|
||||
.option('-q, --qps <value>', t('翻译速度限制,即每秒可调用的API次数'), 1)
|
||||
.hook("preAction",async function(location){
|
||||
const lang= process.env.LANGUAGE || "zh"
|
||||
await i18nScope.change(lang)
|
||||
})
|
||||
.action((location,options) => {
|
||||
location = getProjectSourceFolder(location)
|
||||
const translate = require("./translate.command")
|
||||
translate(location,options)
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
program.parseAsync(process.argv);
|
||||
|
||||
|
@ -12,7 +12,34 @@ const { findModuleType } = require("@voerkai18n/utils")
|
||||
const createLogger = require("logsets")
|
||||
const logger = createLogger()
|
||||
|
||||
module.exports = function(srcPath,{debug = true,languages=["cn","en"],defaultLanguage="cn",activeLanguage="cn",reset=false,installRuntime=true}={}){
|
||||
function getLanguageList(langs,defaultLanguage){
|
||||
try{
|
||||
const available_languages = require("./available_languages")
|
||||
if(defaultLanguage in available_languages){
|
||||
return langs.map(lng=>{
|
||||
const langIndex = available_languages[defaultLanguage].findIndex(l=>l.name===lng)
|
||||
if(langIndex > -1 ){
|
||||
return {
|
||||
name:lng,
|
||||
title:available_languages[defaultLanguage][langIndex].title
|
||||
}
|
||||
}else{
|
||||
return {
|
||||
name:lng,
|
||||
title:lng
|
||||
}
|
||||
}
|
||||
})
|
||||
}else{
|
||||
return langs.map(lng=>({name:lng,title:lng}))
|
||||
}
|
||||
}catch(e){
|
||||
return langs.map(lng=>({name:lng,title:lng}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = function(srcPath,{debug = true,languages=["zh","en"],defaultLanguage="zh",activeLanguage="zh",reset=false,installRuntime=true}={}){
|
||||
// 语言文件夹名称
|
||||
const langPath = "languages"
|
||||
// 查找当前项目的语言包类型路径
|
||||
@ -29,7 +56,7 @@ module.exports = function(srcPath,{debug = true,languages=["cn","en"],defaultLan
|
||||
return
|
||||
}
|
||||
const settings = {
|
||||
languages:languages.map(lng=>({name:lng,title:lng})),
|
||||
languages:getLanguageList(languages,defaultLanguage),
|
||||
defaultLanguage,
|
||||
activeLanguage,
|
||||
namespaces:{}
|
||||
|
@ -1,4 +1,4 @@
|
||||
export default {
|
||||
module.exports = {
|
||||
"1": "支持的语言\\t: {}",
|
||||
"2": "默认语言\\t: {}",
|
||||
"3": "激活语言\\t: {}",
|
||||
@ -40,11 +40,19 @@ export default {
|
||||
"39": " - 语言包文件: {}",
|
||||
"40": " - idMap文件: {}",
|
||||
"41": " - 格式化器:{}",
|
||||
"42": "Now is { value | date | bjTime }",
|
||||
"43": " - 共合成{}条文本",
|
||||
"44": " - 运行时: {}",
|
||||
"45": "自动安装默认语言",
|
||||
"46": "不嵌入运行时源码",
|
||||
"47": " - 安装运行时: {}",
|
||||
"48": " - 运行时{}已安装"
|
||||
"42": " - 共合成{}条文本",
|
||||
"43": " - 运行时: {}",
|
||||
"44": "自动安装默认语言",
|
||||
"45": "不嵌入运行时源码",
|
||||
"46": " - 安装运行时: {}",
|
||||
"47": " - 更新运行时:{}",
|
||||
"48": "调用在线翻译服务商的API翻译译指定项目的语言包,如使用百度云翻译服务",
|
||||
"49": "API密钥",
|
||||
"50": "API ID",
|
||||
"51": "翻译速度限制,即每秒可调用的API次数",
|
||||
"52": "在线翻译服务提供者名称或翻译脚本文件",
|
||||
"53": "将多个文本合并提交的最大包字节数",
|
||||
"54": "正在翻译文件:{}",
|
||||
"55": "需要指定翻译脚本或者appkey和appid",
|
||||
"56": " - 翻译 -> {}"
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
export default {
|
||||
module.exports = {
|
||||
"1": "Supported languages\\t: {}",
|
||||
"2": "Default language\\t: {}",
|
||||
"3": "Active language\\t\\t: {}",
|
||||
@ -40,11 +40,19 @@ export default {
|
||||
"39": " - Language file: {}",
|
||||
"40": " - idMap file: {}",
|
||||
"41": " - Formatters: {}",
|
||||
"42": "Now is { value | date | bjTime }",
|
||||
"43": " - Total{} messages",
|
||||
"44": " - Runtime: {}",
|
||||
"45": "Auto install default language",
|
||||
"46": "Not inline runtime source",
|
||||
"47": " - Install runtime: {}",
|
||||
"48": " - Runtime{} is installed"
|
||||
"42": " - Total{} messages",
|
||||
"43": " - Runtime: {}",
|
||||
"44": "Auto install default language",
|
||||
"45": "Not inline runtime source",
|
||||
"46": " - Install runtime: {}",
|
||||
"47": " - Update runtime:{}",
|
||||
"48": "Call the API translation language package of the online translation service provider, eg:baidu translation service",
|
||||
"49": "API密钥",
|
||||
"50": "API ID",
|
||||
"51": "Translation speed limit. API calls per second",
|
||||
"52": "在线翻译服务提供者名称或翻译脚本文件",
|
||||
"53": "将多个文本合并提交的最大包字节数",
|
||||
"54": "正在翻译文件:{}",
|
||||
"55": "需要指定翻译脚本或者appkey和appid",
|
||||
"56": " - 翻译 -> {}"
|
||||
}
|
@ -10,7 +10,7 @@
|
||||
$types:{...}, // 只作用于特定数据类型的默认格式化器
|
||||
.... // 全局格式化器
|
||||
},
|
||||
cn:{
|
||||
zh:{
|
||||
// 只作用于特定数据类型的格式化器
|
||||
$types:{
|
||||
Date:(value)=>dayjs(value).format("YYYY年MM月DD日 HH:mm:ss"),
|
||||
@ -46,7 +46,7 @@
|
||||
|
||||
*
|
||||
*/
|
||||
export default {
|
||||
module.exports = {
|
||||
// 在所有语言下生效的格式化器
|
||||
"*":{
|
||||
//[格式化名称]:(value)=>{...},
|
||||
@ -56,7 +56,7 @@ export default {
|
||||
$types:{
|
||||
|
||||
},
|
||||
cn:{
|
||||
zh:{
|
||||
$types:{
|
||||
// 所有类型的默认格式化器
|
||||
// "*":{
|
||||
|
@ -1,4 +1,4 @@
|
||||
export default {
|
||||
module.exports = {
|
||||
"支持的语言\\t: {}": 1,
|
||||
"默认语言\\t: {}": 2,
|
||||
"激活语言\\t: {}": 3,
|
||||
@ -40,11 +40,19 @@ export default {
|
||||
" - 语言包文件: {}": 39,
|
||||
" - idMap文件: {}": 40,
|
||||
" - 格式化器:{}": 41,
|
||||
"Now is { value | date | bjTime }": 42,
|
||||
" - 共合成{}条文本": 43,
|
||||
" - 运行时: {}": 44,
|
||||
"自动安装默认语言": 45,
|
||||
"不嵌入运行时源码": 46,
|
||||
" - 安装运行时: {}": 47,
|
||||
" - 运行时{}已安装": 48
|
||||
" - 共合成{}条文本": 42,
|
||||
" - 运行时: {}": 43,
|
||||
"自动安装默认语言": 44,
|
||||
"不嵌入运行时源码": 45,
|
||||
" - 安装运行时: {}": 46,
|
||||
" - 更新运行时:{}": 47,
|
||||
"调用在线翻译服务商的API翻译译指定项目的语言包,如使用百度云翻译服务": 48,
|
||||
"API密钥": 49,
|
||||
"API ID": 50,
|
||||
"翻译速度限制,即每秒可调用的API次数": 51,
|
||||
"在线翻译服务提供者名称或翻译脚本文件": 52,
|
||||
"将多个文本合并提交的最大包字节数": 53,
|
||||
"正在翻译文件:{}": 54,
|
||||
"需要指定翻译脚本或者appkey和appid": 55,
|
||||
" - 翻译 -> {}": 56
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
|
||||
import messageIds from "./idMap.js"
|
||||
import { translate,i18nScope } from "./runtime"
|
||||
const messageIds = require("./idMap")
|
||||
const { translate,i18nScope } = require("./runtime.js")
|
||||
|
||||
import formatters from "./formatters.js"
|
||||
import defaultMessages from "./cn.js"
|
||||
const formatters = require("./formatters.js")
|
||||
const defaultMessages = require("./zh.js")
|
||||
const activeMessages = defaultMessages
|
||||
|
||||
|
||||
@ -11,16 +11,20 @@ const activeMessages = defaultMessages
|
||||
const scopeSettings = {
|
||||
"languages": [
|
||||
{
|
||||
"name": "cn",
|
||||
"title": "cn"
|
||||
"name": "zh",
|
||||
"title": "zh"
|
||||
},
|
||||
{
|
||||
"name": "en",
|
||||
"title": "en"
|
||||
},
|
||||
{
|
||||
"name": "de",
|
||||
"title": "de"
|
||||
}
|
||||
],
|
||||
"defaultLanguage": "cn",
|
||||
"activeLanguage": "cn",
|
||||
"defaultLanguage": "zh",
|
||||
"activeLanguage": "zh",
|
||||
"namespaces": {}
|
||||
}
|
||||
|
||||
@ -33,14 +37,13 @@ const scope = new i18nScope({
|
||||
idMap:messageIds, // 消息id映射列表
|
||||
formatters, // 当前作用域的格式化函数列表
|
||||
loaders:{
|
||||
"en" : ()=>import("./en.js")
|
||||
"en" : ()=>import("./en.js"),
|
||||
"de" : ()=>import("./de.js")
|
||||
}
|
||||
})
|
||||
// 翻译函数
|
||||
const t = translate.bind(scope)
|
||||
const scopedTtranslate = translate.bind(scope)
|
||||
|
||||
export {
|
||||
t,
|
||||
i18nScope:scope
|
||||
}
|
||||
module.exports.t = scopedTtranslate
|
||||
module.exports.i18nScope = scope
|
||||
|
||||
|
@ -1,3 +1,93 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 判断是否是JSON对象
|
||||
* @param {*} obj
|
||||
* @returns
|
||||
*/
|
||||
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))
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单进行对象合并
|
||||
*
|
||||
* options={
|
||||
* array:0 , // 数组合并策略,0-替换,1-合并,2-去重合并
|
||||
* }
|
||||
*
|
||||
* @param {*} toObj
|
||||
* @param {*} formObj
|
||||
* @returns 合并后的对象
|
||||
*/
|
||||
function deepMerge$1(toObj,formObj,options={}){
|
||||
let results = Object.assign({},toObj);
|
||||
Object.entries(formObj).forEach(([key,value])=>{
|
||||
if(key in results){
|
||||
if(typeof value === "object" && value !== null){
|
||||
if(Array.isArray(value)){
|
||||
if(options.array === 0){
|
||||
results[key] = value;
|
||||
}else if(options.array === 1){
|
||||
results[key] = [...results[key],...value];
|
||||
}else if(options.array === 2){
|
||||
results[key] = [...new Set([...results[key],...value])];
|
||||
}
|
||||
}else {
|
||||
results[key] = deepMerge$1(results[key],value,options);
|
||||
}
|
||||
}else {
|
||||
results[key] = value;
|
||||
}
|
||||
}else {
|
||||
results[key] = value;
|
||||
}
|
||||
});
|
||||
return results
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定变量类型名称
|
||||
* 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
var utils ={
|
||||
isPlainObject: isPlainObject$1,
|
||||
isNumber: isNumber$1,
|
||||
deepMerge: deepMerge$1,
|
||||
getDataTypeName: getDataTypeName$1
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* 简单的事件触发器
|
||||
@ -36,7 +126,7 @@ var scope = class i18nScope {
|
||||
// 每个作用域都有一个唯一的id
|
||||
this._id = options.id || (new Date().getTime().toString()+parseInt(Math.random()*1000));
|
||||
this._languages = options.languages; // 当前作用域的语言列表
|
||||
this._defaultLanguage = options.defaultLanguage || "cn"; // 默认语言名称
|
||||
this._defaultLanguage = options.defaultLanguage || "zh"; // 默认语言名称
|
||||
this._activeLanguage = options.activeLanguage; // 当前语言名称
|
||||
this._default = options.default; // 默认语言包
|
||||
this._messages = options.messages; // 当前语言包
|
||||
@ -127,7 +217,7 @@ var scope = class i18nScope {
|
||||
const loader = this.loaders[newLanguage];
|
||||
if(typeof(loader) === "function"){
|
||||
try{
|
||||
this._messages = (await loader()).default;
|
||||
this._messages = (await loader()).default;
|
||||
this._activeLanguage = newLanguage;
|
||||
}catch(e){
|
||||
console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`);
|
||||
@ -183,7 +273,7 @@ var scope = class i18nScope {
|
||||
return value
|
||||
}
|
||||
|
||||
var formatters$1 = {
|
||||
var formatters = {
|
||||
"*":{
|
||||
$types:{
|
||||
Date:(value)=>value.toLocaleString()
|
||||
@ -193,7 +283,7 @@ var formatters$1 = {
|
||||
date: (value)=> value.toLocaleDateString(),
|
||||
dict, //字典格式化器
|
||||
},
|
||||
cn:{
|
||||
zh:{
|
||||
$types:{
|
||||
Date:(value)=> `${value.getFullYear()}年${value.getMonth()+1}月${value.getDate()}日 ${value.getHours()}点${value.getMinutes()}分${value.getSeconds()}秒`
|
||||
},
|
||||
@ -210,9 +300,11 @@ var formatters$1 = {
|
||||
}
|
||||
};
|
||||
|
||||
const { getDataTypeName,isNumber,isPlainObject,deepMerge } = utils;
|
||||
const EventEmitter = eventemitter;
|
||||
const i18nScope = scope;
|
||||
let formatters = formatters$1;
|
||||
let inlineFormatters = formatters; // 内置格式化器
|
||||
|
||||
|
||||
|
||||
// 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter }
|
||||
@ -237,79 +329,7 @@ function hasInterpolation(str){
|
||||
return str.includes("{") && str.includes("}")
|
||||
}
|
||||
const DataTypes$1 = ["String","Number","Boolean","Object","Array","Function","Error","Symbol","RegExp","Date","Null","Undefined","Set","Map","WeakSet","WeakMap"];
|
||||
|
||||
/**
|
||||
* 获取指定变量类型名称
|
||||
* getDataTypeName(1) == Number
|
||||
* getDataTypeName("") == String
|
||||
* getDataTypeName(null) == Null
|
||||
* getDataTypeName(undefined) == Undefined
|
||||
* getDataTypeName(new Date()) == Date
|
||||
* getDataTypeName(new Error()) == Error
|
||||
*
|
||||
* @param {*} v
|
||||
* @returns
|
||||
*/
|
||||
function getDataTypeName(v){
|
||||
if (v === null) return 'Null'
|
||||
if (v === undefined) return 'Undefined'
|
||||
if(typeof(v)==="function") return "Function"
|
||||
return v.constructor && v.constructor.name;
|
||||
}function isPlainObject(obj){
|
||||
if (typeof obj !== 'object' || obj === null) return false;
|
||||
var proto = Object.getPrototypeOf(obj);
|
||||
if (proto === null) return true;
|
||||
var baseProto = proto;
|
||||
|
||||
while (Object.getPrototypeOf(baseProto) !== null) {
|
||||
baseProto = Object.getPrototypeOf(baseProto);
|
||||
}
|
||||
return proto === baseProto;
|
||||
}
|
||||
function isNumber(value){
|
||||
return !isNaN(parseInt(value))
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 简单进行对象合并
|
||||
*
|
||||
* options={
|
||||
* array:0 , // 数组合并策略,0-替换,1-合并,2-去重合并
|
||||
* object:0, // 对象合并策略,0-替换,1-合并,2-去重合并
|
||||
* }
|
||||
*
|
||||
* @param {*} toObj
|
||||
* @param {*} formObj
|
||||
* @returns 合并后的对象
|
||||
*/
|
||||
function deepMerge(toObj,formObj,options={}){
|
||||
let results = Object.assign({},toObj);
|
||||
Object.entries(formObj).forEach(([key,value])=>{
|
||||
if(key in results){
|
||||
if(typeof value === "object" && value !== null){
|
||||
if(Array.isArray(value)){
|
||||
if(options.array === 0){
|
||||
results[key] = value;
|
||||
}else if(options.array === 1){
|
||||
results[key] = [...results[key],...value];
|
||||
}else if(options.array === 2){
|
||||
results[key] = [...new Set([...results[key],...value])];
|
||||
}
|
||||
}else {
|
||||
results[key] = deepMerge(results[key],value,options);
|
||||
}
|
||||
}else {
|
||||
results[key] = value;
|
||||
}
|
||||
}else {
|
||||
results[key] = value;
|
||||
}
|
||||
});
|
||||
return results
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
通过正则表达式对原始文本内容进行解析匹配后得到的
|
||||
@ -452,7 +472,7 @@ function resetScopeCache(scope,activeLanguage=null){
|
||||
"*":{
|
||||
$types:{...} // 在所有语言下只作用于特定数据类型的格式化器
|
||||
}, // 在所有语言下生效的格式化器
|
||||
cn:{
|
||||
zh:{
|
||||
$types:{
|
||||
[数据类型]:(value)=>{...},
|
||||
},
|
||||
@ -653,13 +673,13 @@ function replaceInterpolatedVars(template,...args) {
|
||||
|
||||
// 默认语言配置
|
||||
const defaultLanguageSettings = {
|
||||
defaultLanguage: "cn",
|
||||
activeLanguage: "cn",
|
||||
defaultLanguage: "zh",
|
||||
activeLanguage: "zh",
|
||||
languages:[
|
||||
{name:"cn",title:"中文",default:true},
|
||||
{name:"zh",title:"中文",default:true},
|
||||
{name:"en",title:"英文"}
|
||||
],
|
||||
formatters
|
||||
formatters:inlineFormatters
|
||||
};
|
||||
|
||||
function isMessageId(content){
|
||||
@ -829,11 +849,11 @@ function translate(message) {
|
||||
// 当前激活语言
|
||||
get activeLanguage(){ return this._settings.activeLanguage}
|
||||
// 默认语言
|
||||
get defaultLanguage(){ return this.this._settings.defaultLanguage}
|
||||
get defaultLanguage(){ return this._settings.defaultLanguage}
|
||||
// 支持的语言列表
|
||||
get languages(){ return this._settings.languages}
|
||||
// 全局格式化器
|
||||
get formatters(){ return formatters }
|
||||
// 内置格式化器
|
||||
get formatters(){ return inlineFormatters }
|
||||
/**
|
||||
* 切换语言
|
||||
*/
|
||||
@ -889,7 +909,7 @@ function translate(message) {
|
||||
* 格式化器是一个简单的同步函数value=>{...},用来对输入进行格式化后返回结果
|
||||
*
|
||||
* registerFormatters(name,value=>{...}) // 适用于所有语言
|
||||
* registerFormatters(name,value=>{...},{langauge:"cn"}) // 适用于cn语言
|
||||
* registerFormatters(name,value=>{...},{langauge:"zh"}) // 适用于cn语言
|
||||
* registerFormatters(name,value=>{...},{langauge:"en"}) // 适用于en语言
|
||||
|
||||
* @param {*} formatters
|
||||
@ -919,4 +939,4 @@ var runtime ={
|
||||
isPlainObject
|
||||
};
|
||||
|
||||
export { runtime as default };
|
||||
module.exports = runtime;
|
||||
|
@ -1,15 +1,19 @@
|
||||
{
|
||||
"languages": [
|
||||
{
|
||||
"name": "cn",
|
||||
"title": "cn"
|
||||
"name": "zh",
|
||||
"title": "zh"
|
||||
},
|
||||
{
|
||||
"name": "en",
|
||||
"title": "en"
|
||||
},
|
||||
{
|
||||
"name": "de",
|
||||
"title": "de"
|
||||
}
|
||||
],
|
||||
"defaultLanguage": "cn",
|
||||
"activeLanguage": "cn",
|
||||
"defaultLanguage": "zh",
|
||||
"activeLanguage": "zh",
|
||||
"namespaces": {}
|
||||
}
|
@ -4,291 +4,396 @@
|
||||
"$file": [
|
||||
"compile.command.js",
|
||||
"extract.plugin.js"
|
||||
]
|
||||
],
|
||||
"de": "支持的语言\\t: {}"
|
||||
},
|
||||
"默认语言\\t: {}": {
|
||||
"en": "Default language\\t: {}",
|
||||
"$file": [
|
||||
"compile.command.js",
|
||||
"extract.plugin.js"
|
||||
]
|
||||
],
|
||||
"de": "默认语言\\t: {}"
|
||||
},
|
||||
"激活语言\\t: {}": {
|
||||
"en": "Active language\\t\\t: {}",
|
||||
"$file": [
|
||||
"compile.command.js",
|
||||
"extract.plugin.js"
|
||||
]
|
||||
],
|
||||
"de": "激活语言\\t: {}"
|
||||
},
|
||||
"名称空间\\t: {}": {
|
||||
"en": "Namespaces\\t\\t: {}",
|
||||
"$file": [
|
||||
"compile.command.js",
|
||||
"extract.plugin.js"
|
||||
]
|
||||
],
|
||||
"de": "名称空间\\t: {}"
|
||||
},
|
||||
" - 更新格式化器:{}": {
|
||||
"en": " - Update formatters:{}",
|
||||
"$file": [
|
||||
"compile.command.js"
|
||||
]
|
||||
],
|
||||
"de": " - 更新格式化器:{}"
|
||||
},
|
||||
" - 访问入口文件: {}": {
|
||||
"en": " - Entry of language: {}",
|
||||
"$file": [
|
||||
"compile.command.js"
|
||||
]
|
||||
],
|
||||
"de": " - 访问入口文件: {}"
|
||||
},
|
||||
"加载多语言配置文件<{}>失败: {} ": {
|
||||
"en": "Failed to load multilingual configuration file <{}>: {}",
|
||||
"$file": [
|
||||
"compile.command.js"
|
||||
]
|
||||
],
|
||||
"de": "加载多语言配置文件<{}>失败: {} "
|
||||
},
|
||||
"目标文件夹<{}>不存在": {
|
||||
"en": "The destination folder < {} > does not exist",
|
||||
"$file": [
|
||||
"extract.command.js"
|
||||
]
|
||||
],
|
||||
"de": "目标文件夹<{}>不存在"
|
||||
},
|
||||
"扫描提取范围:": {
|
||||
"en": "Scan for:",
|
||||
"$file": [
|
||||
"extract.command.js"
|
||||
]
|
||||
],
|
||||
"de": "扫描提取范围:"
|
||||
},
|
||||
"工程项目所在目录": {
|
||||
"en": "Project directory",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "工程项目所在目录"
|
||||
},
|
||||
"初始化项目国际化配置": {
|
||||
"en": "Initialize project i18n configuration",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "初始化项目国际化配置"
|
||||
},
|
||||
"输出调试信息": {
|
||||
"en": "Output debug information",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "输出调试信息"
|
||||
},
|
||||
"重新生成当前项目的语言配置": {
|
||||
"en": "Regenerate the language configuration of the current project",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "重新生成当前项目的语言配置"
|
||||
},
|
||||
"支持的语言列表": {
|
||||
"en": "Supported languages",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "支持的语言列表"
|
||||
},
|
||||
"工程目录:{}": {
|
||||
"en": "Folder of project:{}",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "工程目录:{}"
|
||||
},
|
||||
"扫描并提取所有待翻译的字符串到<languages/translates>文件夹中": {
|
||||
"en": "Scan and extract all strings to be translated into the <languages/translations> folder",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "扫描并提取所有待翻译的字符串到<languages/translates>文件夹中"
|
||||
},
|
||||
"支持的语言": {
|
||||
"en": "Supported languages",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "支持的语言"
|
||||
},
|
||||
"默认语言": {
|
||||
"en": "Default language",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "默认语言"
|
||||
},
|
||||
"激活语言": {
|
||||
"en": "Active language",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "激活语言"
|
||||
},
|
||||
"翻译名称空间": {
|
||||
"en": "Namespaces",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "翻译名称空间"
|
||||
},
|
||||
"排除要扫描的文件夹,多个用逗号分隔": {
|
||||
"en": "Exclude folders to scan, multiple separated by commas",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "排除要扫描的文件夹,多个用逗号分隔"
|
||||
},
|
||||
"本次提取内容与已存在内容的数据合并策略,默认取值sync=同步,overwrite=覆盖,merge=合并": {
|
||||
"en": " strategy of messages merge,with value of sync(default),overwrite,merge",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "本次提取内容与已存在内容的数据合并策略,默认取值sync=同步,overwrite=覆盖,merge=合并"
|
||||
},
|
||||
"要扫描的文件类型": {
|
||||
"en": "Type of file to scan",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "要扫描的文件类型"
|
||||
},
|
||||
"语言配置文件<{}>已存在,将优先使用此配置文件中参数来提取文本": {
|
||||
"en": "The language configuration file <{}> already exists. It will be used preferentially to extract messages",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "语言配置文件<{}>已存在,将优先使用此配置文件中参数来提取文本"
|
||||
},
|
||||
"编译指定项目的语言包": {
|
||||
"en": "Compiles the language messages for project",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "编译指定项目的语言包"
|
||||
},
|
||||
"输出模块类型,取值auto,esm,cjs": {
|
||||
"en": "Output module type, values: auto, esm, cjs",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "输出模块类型,取值auto,esm,cjs"
|
||||
},
|
||||
"语言包文件夹<{}>不存在": {
|
||||
"en": "The language messages folder <{}> does not exist",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "语言包文件夹<{}>不存在"
|
||||
},
|
||||
"语言配置文件{}文件已存在,跳过创建。\\n使用{}可以重新覆盖创建": {
|
||||
"en": "Language configuration {} file already exists, skipping creation\\n use {} to overwrite the creation",
|
||||
"$file": [
|
||||
"init.command.js"
|
||||
]
|
||||
],
|
||||
"de": "语言配置文件{}文件已存在,跳过创建。\\n使用{}可以重新覆盖创建"
|
||||
},
|
||||
"生成语言配置文件:{}": {
|
||||
"en": "Generate language configuration: {}",
|
||||
"$file": [
|
||||
"init.command.js"
|
||||
]
|
||||
],
|
||||
"de": "生成语言配置文件:{}"
|
||||
},
|
||||
"拟支持的语言:{}": {
|
||||
"en": "Languages to be supported:{}",
|
||||
"$file": [
|
||||
"init.command.js"
|
||||
]
|
||||
],
|
||||
"de": "拟支持的语言:{}"
|
||||
},
|
||||
"初始化成功,下一步:": {
|
||||
"en": "Initialization succeeded, next step:",
|
||||
"$file": [
|
||||
"init.command.js"
|
||||
]
|
||||
],
|
||||
"de": "初始化成功,下一步:"
|
||||
},
|
||||
" - 编辑{}确定拟支持的语言种类等参数": {
|
||||
"en": " - Edit language parameters in {}",
|
||||
"$file": [
|
||||
"init.command.js"
|
||||
]
|
||||
],
|
||||
"de": " - 编辑{}确定拟支持的语言种类等参数"
|
||||
},
|
||||
" - 运行<{}>扫描提取要翻译的文本": {
|
||||
"en": " - Run <{}> scan to extract the messages to be translated",
|
||||
"$file": [
|
||||
"init.command.js"
|
||||
]
|
||||
],
|
||||
"de": " - 运行<{}>扫描提取要翻译的文本"
|
||||
},
|
||||
" - 运行<{}>编译语言包": {
|
||||
"en": " - Run <{}> compile language messages",
|
||||
"$file": [
|
||||
"init.command.js"
|
||||
]
|
||||
],
|
||||
"de": " - 运行<{}>编译语言包"
|
||||
},
|
||||
"创建语言包文件夹: {}": {
|
||||
"en": "Create <languages> folder: {}",
|
||||
"$file": [
|
||||
"init.command.js"
|
||||
]
|
||||
],
|
||||
"de": "创建语言包文件夹: {}"
|
||||
},
|
||||
"模块类型\\t: {}": {
|
||||
"en": "Type of module\\t\\t: {}",
|
||||
"$file": [
|
||||
"compile.command.js"
|
||||
]
|
||||
],
|
||||
"de": "模块类型\\t: {}"
|
||||
},
|
||||
"编译结果输出至:{}": {
|
||||
"en": "Compile to:{}",
|
||||
"$file": [
|
||||
"compile.command.js"
|
||||
]
|
||||
],
|
||||
"de": "编译结果输出至:{}"
|
||||
},
|
||||
"读取语言文件{}失败:{}": {
|
||||
"en": "Error while read language file{}: {}",
|
||||
"$file": [
|
||||
"compile.command.js"
|
||||
]
|
||||
],
|
||||
"de": "读取语言文件{}失败:{}"
|
||||
},
|
||||
" - 语言包文件: {}": {
|
||||
"en": " - Language file: {}",
|
||||
"$file": [
|
||||
"compile.command.js"
|
||||
]
|
||||
],
|
||||
"de": " - 语言包文件: {}"
|
||||
},
|
||||
" - idMap文件: {}": {
|
||||
"en": " - idMap file: {}",
|
||||
"$file": [
|
||||
"compile.command.js"
|
||||
]
|
||||
],
|
||||
"de": " - idMap文件: {}"
|
||||
},
|
||||
" - 格式化器:{}": {
|
||||
"en": " - Formatters: {}",
|
||||
"$file": [
|
||||
"compile.command.js"
|
||||
]
|
||||
},
|
||||
"Now is { value | date | bjTime }": {
|
||||
"en": "Now is { value | date | bjTime }",
|
||||
"$file": [
|
||||
"templates\\formatters.js"
|
||||
]
|
||||
],
|
||||
"de": " - 格式化器:{}"
|
||||
},
|
||||
" - 共合成{}条文本": {
|
||||
"en": " - Total{} messages",
|
||||
"$file": [
|
||||
"compile.command.js"
|
||||
]
|
||||
],
|
||||
"de": " - 共合成{}条文本"
|
||||
},
|
||||
" - 运行时: {}": {
|
||||
"en": " - Runtime: {}",
|
||||
"$file": [
|
||||
"compile.command.js"
|
||||
]
|
||||
],
|
||||
"de": " - 运行时: {}"
|
||||
},
|
||||
"自动安装默认语言": {
|
||||
"en": "Auto install default language",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "自动安装默认语言",
|
||||
"zh": "自动安装默认语言"
|
||||
},
|
||||
"不嵌入运行时源码": {
|
||||
"en": "Not inline runtime source",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
],
|
||||
"de": "不嵌入运行时源码"
|
||||
},
|
||||
" - 安装运行时: {}": {
|
||||
"en": " - Install runtime: {}",
|
||||
"$file": [
|
||||
"compile.command.js"
|
||||
]
|
||||
],
|
||||
"de": " - 安装运行时: {}"
|
||||
},
|
||||
" - 运行时{}已安装": {
|
||||
"en": " - Runtime{} is installed",
|
||||
" - 更新运行时:{}": {
|
||||
"en": " - Update runtime:{}",
|
||||
"$file": [
|
||||
"compile.command.js"
|
||||
],
|
||||
"de": " - 更新运行时:{}"
|
||||
},
|
||||
"调用在线翻译服务商的API翻译译指定项目的语言包,如使用百度云翻译服务": {
|
||||
"en": "Call the API translation language package of the online translation service provider, eg:baidu translation service",
|
||||
"$file": [
|
||||
"index.js"
|
||||
],
|
||||
"de": "调用在线翻译服务商的API翻译译指定项目的语言包,如使用百度云翻译服务"
|
||||
},
|
||||
"API密钥": {
|
||||
"en": "API密钥",
|
||||
"$file": [
|
||||
"index.js"
|
||||
],
|
||||
"de": "API密钥"
|
||||
},
|
||||
"API ID": {
|
||||
"en": "API ID",
|
||||
"$file": [
|
||||
"index.js"
|
||||
],
|
||||
"de": "API ID"
|
||||
},
|
||||
"翻译速度限制,即每秒可调用的API次数": {
|
||||
"en": "Translation speed limit. API calls per second",
|
||||
"de": "翻译速度限制,即每秒可调用的API次数",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
},
|
||||
"在线翻译服务提供者名称或翻译脚本文件": {
|
||||
"en": "在线翻译服务提供者名称或翻译脚本文件",
|
||||
"de": "在线翻译服务提供者名称或翻译脚本文件",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
},
|
||||
"将多个文本合并提交的最大包字节数": {
|
||||
"en": "将多个文本合并提交的最大包字节数",
|
||||
"de": "将多个文本合并提交的最大包字节数",
|
||||
"$file": [
|
||||
"index.js"
|
||||
]
|
||||
},
|
||||
"正在翻译文件:{}": {
|
||||
"en": "正在翻译文件:{}",
|
||||
"de": "正在翻译文件:{}",
|
||||
"$file": [
|
||||
"translate.command.js"
|
||||
]
|
||||
},
|
||||
"需要指定翻译脚本或者appkey和appid": {
|
||||
"en": "需要指定翻译脚本或者appkey和appid",
|
||||
"de": "需要指定翻译脚本或者appkey和appid",
|
||||
"$file": [
|
||||
"translate.command.js"
|
||||
]
|
||||
},
|
||||
" - 翻译 -> {}": {
|
||||
"en": " - 翻译 -> {}",
|
||||
"de": " - 翻译 -> {}",
|
||||
"$file": [
|
||||
"translate.command.js"
|
||||
]
|
||||
}
|
||||
}
|
58
packages/cli/languages/zh.js
Normal file
58
packages/cli/languages/zh.js
Normal file
@ -0,0 +1,58 @@
|
||||
module.exports = {
|
||||
"1": "支持的语言\\t: {}",
|
||||
"2": "默认语言\\t: {}",
|
||||
"3": "激活语言\\t: {}",
|
||||
"4": "名称空间\\t: {}",
|
||||
"5": " - 更新格式化器:{}",
|
||||
"6": " - 访问入口文件: {}",
|
||||
"7": "加载多语言配置文件<{}>失败: {} ",
|
||||
"8": "目标文件夹<{}>不存在",
|
||||
"9": "扫描提取范围:",
|
||||
"10": "工程项目所在目录",
|
||||
"11": "初始化项目国际化配置",
|
||||
"12": "输出调试信息",
|
||||
"13": "重新生成当前项目的语言配置",
|
||||
"14": "支持的语言列表",
|
||||
"15": "工程目录:{}",
|
||||
"16": "扫描并提取所有待翻译的字符串到<languages/translates>文件夹中",
|
||||
"17": "支持的语言",
|
||||
"18": "默认语言",
|
||||
"19": "激活语言",
|
||||
"20": "翻译名称空间",
|
||||
"21": "排除要扫描的文件夹,多个用逗号分隔",
|
||||
"22": "本次提取内容与已存在内容的数据合并策略,默认取值sync=同步,overwrite=覆盖,merge=合并",
|
||||
"23": "要扫描的文件类型",
|
||||
"24": "语言配置文件<{}>已存在,将优先使用此配置文件中参数来提取文本",
|
||||
"25": "编译指定项目的语言包",
|
||||
"26": "输出模块类型,取值auto,esm,cjs",
|
||||
"27": "语言包文件夹<{}>不存在",
|
||||
"28": "语言配置文件{}文件已存在,跳过创建。\\n使用{}可以重新覆盖创建",
|
||||
"29": "生成语言配置文件:{}",
|
||||
"30": "拟支持的语言:{}",
|
||||
"31": "初始化成功,下一步:",
|
||||
"32": " - 编辑{}确定拟支持的语言种类等参数",
|
||||
"33": " - 运行<{}>扫描提取要翻译的文本",
|
||||
"34": " - 运行<{}>编译语言包",
|
||||
"35": "创建语言包文件夹: {}",
|
||||
"36": "模块类型\\t: {}",
|
||||
"37": "编译结果输出至:{}",
|
||||
"38": "读取语言文件{}失败:{}",
|
||||
"39": " - 语言包文件: {}",
|
||||
"40": " - idMap文件: {}",
|
||||
"41": " - 格式化器:{}",
|
||||
"42": " - 共合成{}条文本",
|
||||
"43": " - 运行时: {}",
|
||||
"44": "自动安装默认语言",
|
||||
"45": "不嵌入运行时源码",
|
||||
"46": " - 安装运行时: {}",
|
||||
"47": " - 更新运行时:{}",
|
||||
"48": "调用在线翻译服务商的API翻译译指定项目的语言包,如使用百度云翻译服务",
|
||||
"49": "API密钥",
|
||||
"50": "API ID",
|
||||
"51": "翻译速度限制,即每秒可调用的API次数",
|
||||
"52": "在线翻译服务提供者名称或翻译脚本文件",
|
||||
"53": "将多个文本合并提交的最大包字节数",
|
||||
"54": "正在翻译文件:{}",
|
||||
"55": "需要指定翻译脚本或者appkey和appid",
|
||||
"56": " - 翻译 -> {}"
|
||||
}
|
@ -1,46 +1,54 @@
|
||||
{
|
||||
"name": "@voerkai18n/cli",
|
||||
"version": "1.0.15",
|
||||
"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",
|
||||
"release": "npm version patch && pnpm publish --no-git-checks --access public"
|
||||
},
|
||||
"author": "wxzhang",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"voerkai18n": "./index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/cli": "^7.17.6",
|
||||
"@babel/core": "^7.17.5",
|
||||
"@voerkai18n/runtime": "workspace:^1.0.8",
|
||||
"@voerkai18n/utils": "workspace:^1.0.0",
|
||||
"art-template": "^4.13.2",
|
||||
"commander": "^9.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"deepmerge": "^4.2.2",
|
||||
"fs-extra": "^10.0.1",
|
||||
"glob": "^7.2.0",
|
||||
"gulp": "^4.0.2",
|
||||
"logsets": "^1.0.8",
|
||||
"shelljs": "^0.8.5",
|
||||
"through2": "^4.0.2",
|
||||
"vinyl": "^2.2.1"
|
||||
}
|
||||
}
|
||||
"name": "@voerkai18n/cli",
|
||||
"version": "1.0.26",
|
||||
"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 templates/** ",
|
||||
"compile": "node ./index.js compile -m cjs",
|
||||
"compile:en": "cross-env LANGUAGE=en node ./index.js compile -d",
|
||||
"build": "node ./index.js compile -m cjs",
|
||||
"release": "pnpm autopublish"
|
||||
},
|
||||
"author": "wxzhang",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"voerkai18n": "./index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/cli": "^7.17.6",
|
||||
"@babel/core": "^7.17.5",
|
||||
"@voerkai18n/runtime": "workspace:^1.0.14",
|
||||
"@voerkai18n/utils": "workspace:^1.0.6",
|
||||
"art-template": "^4.13.2",
|
||||
"axios": "^0.26.1",
|
||||
"commander": "^9.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"deepmerge": "^4.2.2",
|
||||
"fs-extra": "^10.0.1",
|
||||
"glob": "^7.2.0",
|
||||
"gulp": "^4.0.2",
|
||||
"logsets": "^1.0.8",
|
||||
"md5": "^2.3.0",
|
||||
"qs": "^6.10.3",
|
||||
"shelljs": "^0.8.5",
|
||||
"through2": "^4.0.2",
|
||||
"vinyl": "^2.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@voerkai18n/autopublish": "workspace:^1.0.2"
|
||||
},
|
||||
"lastPublish": "2022-04-10T20:14:07+08:00"
|
||||
}
|
@ -12,7 +12,7 @@
|
||||
* {
|
||||
* "保存{}\\n": "Save{}\n",
|
||||
* }
|
||||
* 这个转义方式比较不符合我们的预期,更关键的是,在require("cn.js")时,
|
||||
* 这个转义方式比较不符合我们的预期,更关键的是,在require("zh.js")时,
|
||||
* 得到的是:
|
||||
* {
|
||||
* "保存{}\\n": "Save{}\n",
|
||||
|
@ -10,7 +10,7 @@
|
||||
$types:{...}, // 只作用于特定数据类型的默认格式化器
|
||||
.... // 全局格式化器
|
||||
},
|
||||
cn:{
|
||||
zh:{
|
||||
// 只作用于特定数据类型的格式化器
|
||||
$types:{
|
||||
Date:(value)=>dayjs(value).format("YYYY年MM月DD日 HH:mm:ss"),
|
||||
|
193
packages/cli/translate.command.js
Normal file
193
packages/cli/translate.command.js
Normal file
@ -0,0 +1,193 @@
|
||||
|
||||
/**
|
||||
* 将extract插件扫描的文件编译为语言文件
|
||||
*
|
||||
* 编译后的语言文件用于运行环境使用
|
||||
*
|
||||
* 编译原理如下:
|
||||
*
|
||||
*
|
||||
* 编译后会在目标文件夹输出:
|
||||
*
|
||||
* - languages
|
||||
* translates
|
||||
* - en.json
|
||||
* - zh.json
|
||||
* - ...
|
||||
* idMap.js // id映射列表
|
||||
* settings.json // 配置文件
|
||||
* zh.js // 中文语言包
|
||||
* en.js // 英文语言包
|
||||
* [lang].js // 其他语言包
|
||||
*
|
||||
* @param {*} opts
|
||||
*/
|
||||
|
||||
const createLogger = require("logsets")
|
||||
const path = require("path")
|
||||
const { t } = require("./i18nProxy")
|
||||
const fs = require("fs-extra")
|
||||
const { glob } = require("glob")
|
||||
const { default: axios } = require("axios")
|
||||
const logger = createLogger()
|
||||
const TaskListPlugin = require("logsets/plugins/tasklist")
|
||||
const { deepMerge } = require("@voerkai18n/utils")
|
||||
logger.use(TaskListPlugin)
|
||||
|
||||
const delay = async (t) => new Promise(resolve=>setTimeout(resolve,t))
|
||||
|
||||
function normalizeTranslateOptions(opts={}) {
|
||||
let options = Object.assign({
|
||||
appkey:null,
|
||||
appid:null,
|
||||
backup:true, // 是否备份原始文件
|
||||
mode:"auto", // 是否全部翻译,auto=仅仅对未翻译的内容进行翻译,full=全部翻译
|
||||
provider:null, // 指定脚本文件来进行翻译
|
||||
}, opts)
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function getTranslateProvider(options={}){
|
||||
const { provider } = options;
|
||||
if(provider==="baidu"){
|
||||
return require("./baidu.translate.js")(options)
|
||||
}else{
|
||||
return require(provider)(options)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 翻译多条文本
|
||||
* @param {*} to
|
||||
* @param {*} messages
|
||||
* @param {*} options
|
||||
* @returns
|
||||
*/
|
||||
async function translateMessages(messages={},from,to,options={}){
|
||||
let { mode,qps=1 } = options
|
||||
if(messages.length===0) return;
|
||||
const provider = getTranslateProvider(options)
|
||||
await delay(1000/qps)
|
||||
let translatedMessages =await provider.translate(Object.keys(messages),from,to)
|
||||
Object.keys(messages).forEach((key,index)=>{
|
||||
messages[key][to] = translatedMessages[index]
|
||||
})
|
||||
return messages
|
||||
}
|
||||
/**
|
||||
* 翻译多行文本
|
||||
* @param {*} messages
|
||||
* @param {*} options
|
||||
* @returns
|
||||
*/
|
||||
async function translateMultiLineMessage(messages=[],from,to,options={}){
|
||||
if(messages.length===0) return;
|
||||
const provider = getTranslateProvider(options)
|
||||
await delay(1000/qps)
|
||||
let result = await provider.translate(messages,from,to)
|
||||
return result.join("\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译指定的语言
|
||||
* @param {*} messages
|
||||
* @param {*} from
|
||||
* @param {*} to
|
||||
* @param {*} options
|
||||
* @returns
|
||||
*/
|
||||
async function translateLanguage(messages,from,to,options={}){
|
||||
const { maxPackageSize,mode } = options;
|
||||
let result = {}
|
||||
let lngMessages = {} ,packageSize =0
|
||||
for(let [ text,lngs ] of Object.entries(messages)){
|
||||
// 如果mode=auto,则当翻译内容已经有变化时,则不再翻译
|
||||
if(mode=="auto" && typeof(lngs[to])==="string" && (lngs[to]!=text && lngs[to].trim()!="")){
|
||||
if(!(text in result)) result[text] = {}
|
||||
result[text][to] =lngs[to]
|
||||
continue;
|
||||
}
|
||||
// 由于百度翻译按\n来分行翻译,如果有\n则会出现多行翻译的情况,因此,如果有\n则就不将多条文件合并翻译
|
||||
if(text.includes("\n")){
|
||||
if(!(text in result)) result[text] = {}
|
||||
result[text][to] = await translateMultiLineMessage(text.split("\n"),from,to,options)
|
||||
}else{
|
||||
lngMessages[text]={[to]:''}
|
||||
packageSize+=text.length
|
||||
// 多个信息合并进行翻译,减少请求次数
|
||||
if(packageSize>=options.maxPackageSize){
|
||||
await translateMessages(lngMessages,from,to,options)
|
||||
result = deepMerge(result,lngMessages)
|
||||
packageSize=0
|
||||
lngMessages={}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 对剩余的信息进行翻译
|
||||
if(Object.keys(lngMessages).length > 0){
|
||||
requestCount++
|
||||
await translateMessages(lngMessages,from,to,options)
|
||||
result = deepMerge(result,lngMessages)
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译指定的文件
|
||||
* @param {*} file
|
||||
* @param {*} langSettings
|
||||
* @param {*} options
|
||||
*/
|
||||
async function translateMessageFile(file,langSettings,options={}){
|
||||
let context = this
|
||||
logger.log(t("正在翻译文件:{}"),path.basename(file))
|
||||
let messages = fs.readJSONSync(file);
|
||||
// texts = {text:{zh:"",en:"",...,jp:""}}
|
||||
let results = {}
|
||||
const tasks = logger.tasklist()
|
||||
for(let lng of langSettings.languages){
|
||||
if(lng.name === langSettings.defaultLanguage) continue
|
||||
try{
|
||||
tasks.add(t(" - 翻译 -> {}",lng.name))
|
||||
results = deepMerge(results,await translateLanguage(messages,langSettings.defaultLanguage,lng.name,options))
|
||||
tasks.complete()
|
||||
}catch(e){
|
||||
tasks.error(e.message || e)
|
||||
}
|
||||
}
|
||||
results = deepMerge(messages,results)
|
||||
// 写入原始文件
|
||||
fs.writeFileSync(file,JSON.stringify(results,null,4))
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports =async function translate(srcFolder,opts={}){
|
||||
const options = normalizeTranslateOptions(opts);
|
||||
let { backup, appkey,appid,provider="baidu",qps=1 } = options;
|
||||
if(!provider && !(appkey && appid) ) throw new Error(t("需要指定翻译脚本或者appkey和appid"))
|
||||
|
||||
const langFolder = path.join(srcFolder,"languages");
|
||||
const files = glob.sync(path.join(langFolder,"translates/*.json"))
|
||||
const langSettings = fs.readJSONSync(path.join(langFolder,"settings.json"))
|
||||
// 保存一些调用信息,用来在翻译完成后,显示
|
||||
let context = {
|
||||
provider,
|
||||
files:[] // 翻译的文件{filename:<翻译>,messages:<数量>,calls:<调用>,timeConsuming:<耗时>}
|
||||
}
|
||||
|
||||
// 枚举所有需要翻译的文件
|
||||
for(let file of files){
|
||||
// 备份原始文件
|
||||
const backupFile = path.join(langFolder,"translates","backup",path.basename(file))
|
||||
if(backup && !fs.existsSync(backupFile)){
|
||||
if(!fs.existsSync(path.dirname(backupFile))) fs.mkdirSync(path.dirname(backupFile))
|
||||
fs.copyFileSync(file,backupFile)
|
||||
}
|
||||
// 翻译文件
|
||||
let results = await translateMessageFile.call(context,file,langSettings,options)
|
||||
|
||||
}
|
||||
}
|
@ -1,20 +1,23 @@
|
||||
{
|
||||
"name": "@voerkai18n/formatters",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "currency.formatters.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"release": "npm version patch && pnpm publish --no-git-checks --access public"
|
||||
},
|
||||
"exports":{
|
||||
|
||||
},
|
||||
"author": "wxzhang",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"deepmerge": "^4.2.2",
|
||||
"gulp": "^4.0.2",
|
||||
"vinyl": "^2.2.1"
|
||||
}
|
||||
}
|
||||
"name": "@voerkai18n/formatters",
|
||||
"version": "1.0.5",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"release": "pnpm autopublish"
|
||||
},
|
||||
"exports": {
|
||||
"currency": "./currency.formatters.js",
|
||||
"datetime": "./datetime.formatters.js"
|
||||
},
|
||||
"author": "wxzhang",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dayjs": "^1.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@voerkai18n/autopublish": "workspace:^1.0.2"
|
||||
},
|
||||
"lastPublish": "2022-04-06T20:54:23+08:00"
|
||||
}
|
2
packages/runtime/dist/index.cjs
vendored
2
packages/runtime/dist/index.cjs
vendored
File diff suppressed because one or more lines are too long
1
packages/runtime/dist/index.cjs.map
vendored
1
packages/runtime/dist/index.cjs.map
vendored
File diff suppressed because one or more lines are too long
2
packages/runtime/dist/index.esm.js
vendored
2
packages/runtime/dist/index.esm.js
vendored
File diff suppressed because one or more lines are too long
1
packages/runtime/dist/index.esm.js.map
vendored
1
packages/runtime/dist/index.esm.js.map
vendored
File diff suppressed because one or more lines are too long
942
packages/runtime/dist/runtime.cjs
vendored
942
packages/runtime/dist/runtime.cjs
vendored
@ -1,942 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 判断是否是JSON对象
|
||||
* @param {*} obj
|
||||
* @returns
|
||||
*/
|
||||
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))
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单进行对象合并
|
||||
*
|
||||
* options={
|
||||
* array:0 , // 数组合并策略,0-替换,1-合并,2-去重合并
|
||||
* }
|
||||
*
|
||||
* @param {*} toObj
|
||||
* @param {*} formObj
|
||||
* @returns 合并后的对象
|
||||
*/
|
||||
function deepMerge$1(toObj,formObj,options={}){
|
||||
let results = Object.assign({},toObj);
|
||||
Object.entries(formObj).forEach(([key,value])=>{
|
||||
if(key in results){
|
||||
if(typeof value === "object" && value !== null){
|
||||
if(Array.isArray(value)){
|
||||
if(options.array === 0){
|
||||
results[key] = value;
|
||||
}else if(options.array === 1){
|
||||
results[key] = [...results[key],...value];
|
||||
}else if(options.array === 2){
|
||||
results[key] = [...new Set([...results[key],...value])];
|
||||
}
|
||||
}else {
|
||||
results[key] = deepMerge$1(results[key],value,options);
|
||||
}
|
||||
}else {
|
||||
results[key] = value;
|
||||
}
|
||||
}else {
|
||||
results[key] = value;
|
||||
}
|
||||
});
|
||||
return results
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定变量类型名称
|
||||
* 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
var utils ={
|
||||
isPlainObject: isPlainObject$1,
|
||||
isNumber: isNumber$1,
|
||||
deepMerge: deepMerge$1,
|
||||
getDataTypeName: getDataTypeName$1
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* 简单的事件触发器
|
||||
*
|
||||
*/
|
||||
|
||||
var eventemitter = class EventEmitter{
|
||||
constructor(){
|
||||
this._callbacks = [];
|
||||
}
|
||||
on(callback){
|
||||
if(this._callbacks.includes(callback)) return
|
||||
this._callbacks.push(callback);
|
||||
}
|
||||
off(callback){
|
||||
for(let i=0;i<this._callbacks.length;i++){
|
||||
if(this._callbacks[i]===callback ){
|
||||
this._callbacks.splice(i,1);
|
||||
}
|
||||
}
|
||||
}
|
||||
offAll(){
|
||||
this._callbacks = [];
|
||||
}
|
||||
async emit(...args){
|
||||
if(Promise.allSettled){
|
||||
await Promise.allSettled(this._callbacks.map(cb=>cb(...args)));
|
||||
}else {
|
||||
await Promise.all(this._callbacks.map(cb=>cb(...args)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var scope = class i18nScope {
|
||||
constructor(options={},callback){
|
||||
// 每个作用域都有一个唯一的id
|
||||
this._id = options.id || (new Date().getTime().toString()+parseInt(Math.random()*1000));
|
||||
this._languages = options.languages; // 当前作用域的语言列表
|
||||
this._defaultLanguage = options.defaultLanguage || "cn"; // 默认语言名称
|
||||
this._activeLanguage = options.activeLanguage; // 当前语言名称
|
||||
this._default = options.default; // 默认语言包
|
||||
this._messages = options.messages; // 当前语言包
|
||||
this._idMap = options.idMap; // 消息id映射列表
|
||||
this._formatters = options.formatters; // 当前作用域的格式化函数列表
|
||||
this._loaders = options.loaders; // 异步加载语言文件的函数列表
|
||||
this._global = null; // 引用全局VoerkaI18n配置,注册后自动引用
|
||||
// 主要用来缓存格式化器的引用,当使用格式化器时可以直接引用,避免检索
|
||||
this.$cache={
|
||||
activeLanguage : null,
|
||||
typedFormatters: {},
|
||||
formatters : {},
|
||||
};
|
||||
// 如果不存在全局VoerkaI18n实例,说明当前Scope是唯一或第一个加载的作用域,
|
||||
// 则使用当前作用域来初始化全局VoerkaI18n实例
|
||||
if(!globalThis.VoerkaI18n){
|
||||
const { I18nManager } = runtime;
|
||||
globalThis.VoerkaI18n = new I18nManager({
|
||||
defaultLanguage: this.defaultLanguage,
|
||||
activeLanguage : this.activeLanguage,
|
||||
languages: options.languages,
|
||||
});
|
||||
}
|
||||
this.global = globalThis.VoerkaI18n;
|
||||
// 正在加载语言包标识
|
||||
this._loading=false;
|
||||
// 在全局注册作用域
|
||||
this.register(callback);
|
||||
}
|
||||
// 作用域
|
||||
get id(){return this._id}
|
||||
// 默认语言名称
|
||||
get defaultLanguage(){return this._defaultLanguage}
|
||||
// 默认语言名称
|
||||
get activeLanguage(){return this._activeLanguage}
|
||||
// 默认语言包
|
||||
get default(){return this._default}
|
||||
// 当前语言包
|
||||
get messages(){return this._messages}
|
||||
// 消息id映射列表
|
||||
get idMap(){return this._idMap}
|
||||
// 当前作用域的格式化函数列表
|
||||
get formatters(){return this._formatters}
|
||||
// 异步加载语言文件的函数列表
|
||||
get loaders(){return this._loaders}
|
||||
// 引用全局VoerkaI18n配置,注册后自动引用
|
||||
get global(){return this._global}
|
||||
set global(value){this._global = value;}
|
||||
/**
|
||||
* 在全局注册作用域
|
||||
* @param {*} callback 当注册
|
||||
*/
|
||||
register(callback){
|
||||
if(!typeof(callback)==="function") callback = ()=>{};
|
||||
this.global.register(this).then(callback).catch(callback);
|
||||
}
|
||||
registerFormatter(name,formatter,{language="*"}={}){
|
||||
if(!typeof(formatter)==="function" || typeof(name)!=="string"){
|
||||
throw new TypeError("Formatter must be a function")
|
||||
}
|
||||
if(DataTypes.includes(name)){
|
||||
this.formatters[language].$types[name] = formatter;
|
||||
}else {
|
||||
this.formatters[language][name] = formatter;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 回退到默认语言
|
||||
*/
|
||||
_fallback(){
|
||||
this._messages = this._default;
|
||||
this._activeLanguage = this.defaultLanguage;
|
||||
}
|
||||
/**
|
||||
* 刷新当前语言包
|
||||
* @param {*} newLanguage
|
||||
*/
|
||||
async refresh(newLanguage){
|
||||
this._loading = Promise.resolve();
|
||||
if(!newLanguage) newLanguage = this.activeLanguage;
|
||||
// 默认语言,默认语言采用静态加载方式,只需要简单的替换即可
|
||||
if(newLanguage === this.defaultLanguage){
|
||||
this._messages = this._default;
|
||||
return
|
||||
}
|
||||
// 非默认语言需要异步加载语言包文件,加载器是一个异步函数
|
||||
// 如果没有加载器,则无法加载语言包,因此回退到默认语言
|
||||
const loader = this.loaders[newLanguage];
|
||||
if(typeof(loader) === "function"){
|
||||
try{
|
||||
this._messages = (await loader()).default;
|
||||
this._activeLanguage = newLanguage;
|
||||
}catch(e){
|
||||
console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`);
|
||||
this._fallback();
|
||||
}
|
||||
}else {
|
||||
this._fallback();
|
||||
}
|
||||
}
|
||||
// 以下方法引用全局VoerkaI18n实例的方法
|
||||
get on(){return this.global.on.bind(this.global)}
|
||||
get off(){return this.global.off.bind(this.global)}
|
||||
get offAll(){return this.global.offAll.bind(this.global)}
|
||||
get change(){
|
||||
return this.global.change.bind(this.global)
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 内置的格式化器
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 字典格式化器
|
||||
* 根据输入data的值,返回后续参数匹配的结果
|
||||
* dict(data,<value1>,<result1>,<value2>,<result1>,<value3>,<result1>,...)
|
||||
*
|
||||
*
|
||||
* dict(1,1,"one",2,"two",3,"three",4,"four") == "one"
|
||||
* dict(2,1,"one",2,"two",3,"three",4,"four") == "two"
|
||||
* dict(3,1,"one",2,"two",3,"three",4,"four") == "three"
|
||||
* dict(4,1,"one",2,"two",3,"three",4,"four") == "four"
|
||||
* // 无匹配时返回原始值
|
||||
* dict(5,1,"one",2,"two",3,"three",4,"four") == 5
|
||||
* // 无匹配时并且后续参数个数是奇数,则返回最后一个参数
|
||||
* dict(5,1,"one",2,"two",3,"three",4,"four","more") == "more"
|
||||
*
|
||||
* 在翻译中使用
|
||||
* I have { value | dict(1,"one",2,"two",3,"three",4,"four")} apples
|
||||
*
|
||||
* @param {*} value
|
||||
* @param {...any} args
|
||||
* @returns
|
||||
*/
|
||||
function dict(value,...args){
|
||||
for(let i=0;i<args.length;i+=2){
|
||||
if(args[i]===value){
|
||||
return args[i+1]
|
||||
}
|
||||
}
|
||||
if(args.length >0 && (args.length % 2!==0)) return args[args.length-1]
|
||||
return value
|
||||
}
|
||||
|
||||
var formatters$1 = {
|
||||
"*":{
|
||||
$types:{
|
||||
Date:(value)=>value.toLocaleString()
|
||||
},
|
||||
time:(value)=> value.toLocaleTimeString(),
|
||||
shorttime:(value)=> value.toLocaleTimeString(),
|
||||
date: (value)=> value.toLocaleDateString(),
|
||||
dict, //字典格式化器
|
||||
},
|
||||
cn:{
|
||||
$types:{
|
||||
Date:(value)=> `${value.getFullYear()}年${value.getMonth()+1}月${value.getDate()}日 ${value.getHours()}点${value.getMinutes()}分${value.getSeconds()}秒`
|
||||
},
|
||||
shortime:(value)=> value.toLocaleTimeString(),
|
||||
time:(value)=>`${value.getHours()}点${value.getMinutes()}分${value.getSeconds()}秒`,
|
||||
date: (value)=> `${value.getFullYear()}年${value.getMonth()+1}月${value.getDate()}日`,
|
||||
shortdate: (value)=> `${value.getFullYear()}-${value.getMonth()+1}-${value.getDate()}`,
|
||||
currency:(value)=>`${value}元`,
|
||||
},
|
||||
en:{
|
||||
currency:(value)=>{
|
||||
return `$${value}`
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const { getDataTypeName,isNumber,isPlainObject,deepMerge } = utils;
|
||||
const EventEmitter = eventemitter;
|
||||
const i18nScope = scope;
|
||||
let inlineFormatters = formatters$1; // 内置格式化器
|
||||
|
||||
|
||||
|
||||
// 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter }
|
||||
// 不支持参数: let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*\s*)*)\s*\}/g
|
||||
|
||||
// 支持参数: { var | formatter(x,x,..) | formatter }
|
||||
let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*(\(.*\)){0,1}\s*)*)\s*\}/g;
|
||||
|
||||
// 有效的语言名称列表
|
||||
const languages = ["af","am","ar-dz","ar-iq","ar-kw","ar-ly","ar-ma","ar-sa","ar-tn","ar","az","be","bg","bi","bm","bn","bo","br","bs","ca","cs","cv","cy","da","de-at","de-ch","de","dv","el","en-au","en-ca","en-gb","en-ie","en-il","en-in","en-nz","en-sg","en-tt","en","eo","es-do","es-mx","es-pr","es-us","es","et","eu","fa","fi","fo","fr-ca","fr-ch","fr","fy","ga","gd","gl","gom-latn","gu","he","hi","hr","ht","hu","hy-am","id","is","it-ch","it","ja","jv","ka","kk","km","kn","ko","ku","ky","lb","lo","lt","lv","me","mi","mk","ml","mn","mr","ms-my","ms","mt","my","nb","ne","nl-be","nl","nn","oc-lnc","pa-in","pl","pt-br","pt","ro","ru","rw","sd","se","si","sk","sl","sq","sr-cyrl","sr","ss","sv-fi","sv","sw","ta","te","tet","tg","th","tk","tl-ph","tlh","tr","tzl","tzm-latn","tzm","ug-cn","uk","ur","uz-latn","uz","vi","x-pseudo","yo","zh-cn","zh-hk","zh-tw","zh"];
|
||||
|
||||
/**
|
||||
* 考虑到通过正则表达式进行插件的替换可能较慢,因此提供一个简单方法来过滤掉那些
|
||||
* 不需要进行插值处理的字符串
|
||||
* 原理很简单,就是判断一下是否同时具有{和}字符,如果有则认为可能有插值变量,如果没有则一定没有插件变量,则就不需要进行正则匹配
|
||||
* 从而可以减少不要的正则匹配
|
||||
* 注意:该方法只能快速判断一个字符串不包括插值变量
|
||||
* @param {*} str
|
||||
* @returns {boolean} true=可能包含插值变量,
|
||||
*/
|
||||
function hasInterpolation(str){
|
||||
return str.includes("{") && str.includes("}")
|
||||
}
|
||||
const DataTypes$1 = ["String","Number","Boolean","Object","Array","Function","Error","Symbol","RegExp","Date","Null","Undefined","Set","Map","WeakSet","WeakMap"];
|
||||
|
||||
|
||||
/**
|
||||
通过正则表达式对原始文本内容进行解析匹配后得到的
|
||||
formatters="| aaa(1,1) | bbb "
|
||||
|
||||
需要统一解析为
|
||||
|
||||
[
|
||||
[aaa,[1,1]], // [formatter'name,[args,...]]
|
||||
[bbb,[]],
|
||||
]
|
||||
|
||||
formatters="| aaa(1,1,"dddd") | bbb "
|
||||
|
||||
目前对参数采用简单的split(",")来解析,因为无法正确解析aaa(1,1,"dd,,dd")形式的参数
|
||||
在此场景下基本够用了,如果需要支持更复杂的参数解析,可以后续考虑使用正则表达式来解析
|
||||
|
||||
@returns [[<formatterName>,[<arg>,<arg>,...]]]
|
||||
*/
|
||||
function parseFormatters(formatters){
|
||||
if(!formatters) return []
|
||||
// 1. 先解析为 ["aaa()","bbb"]形式
|
||||
let result = formatters.trim().substr(1).trim().split("|").map(r=>r.trim());
|
||||
|
||||
// 2. 解析格式化器参数
|
||||
return result.map(formatter=>{
|
||||
let firstIndex = formatter.indexOf("(");
|
||||
let lastIndex = formatter.lastIndexOf(")");
|
||||
if(firstIndex!==-1 && lastIndex!==-1){ // 带参数的格式化器
|
||||
const argsContent = formatter.substr(firstIndex+1,lastIndex-firstIndex-1).trim();
|
||||
let args = argsContent=="" ? [] : argsContent.split(",").map(arg=>{
|
||||
arg = arg.trim();
|
||||
if(!isNaN(parseInt(arg))){
|
||||
return parseInt(arg) // 数字
|
||||
}else if((arg.startsWith('\"') && arg.endsWith('\"')) || (arg.startsWith('\'') && arg.endsWith('\'')) ){
|
||||
return arg.substr(1,arg.length-2) // 字符串
|
||||
}else if(arg.toLowerCase()==="true" || arg.toLowerCase()==="false"){
|
||||
return arg.toLowerCase()==="true" // 布尔值
|
||||
}else if((arg.startsWith('{') && arg.endsWith('}')) || (arg.startsWith('[') && arg.endsWith(']'))){
|
||||
try{
|
||||
return JSON.parse(arg)
|
||||
}catch(e){
|
||||
return String(arg)
|
||||
}
|
||||
}else {
|
||||
return String(arg)
|
||||
}
|
||||
});
|
||||
return [formatter.substr(0,firstIndex),args]
|
||||
}else {// 不带参数的格式化器
|
||||
return [formatter,[]]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取字符串中的插值变量
|
||||
* // [
|
||||
// {
|
||||
name:<变量名称>,formatters:[{name:<格式化器名称>,args:[<参数>,<参数>,....]]}],<匹配字符串>],
|
||||
// ....
|
||||
//
|
||||
* @param {*} str
|
||||
* @param {*} isFull =true 保留所有插值变量 =false 进行去重
|
||||
* @returns {Array}
|
||||
* [
|
||||
* {
|
||||
* name:"<变量名称>",
|
||||
* formatters:[
|
||||
* {name:"<格式化器名称>",args:[<参数>,<参数>,....]},
|
||||
* {name:"<格式化器名称>",args:[<参数>,<参数>,....]},
|
||||
* ],
|
||||
* match:"<匹配字符串>"
|
||||
* },
|
||||
* ...
|
||||
* ]
|
||||
*/
|
||||
function getInterpolatedVars(str){
|
||||
let vars = [];
|
||||
forEachInterpolatedVars(str,(varName,formatters,match)=>{
|
||||
let varItem = {
|
||||
name:varName,
|
||||
formatters:formatters.map(([formatter,args])=>{
|
||||
return {
|
||||
name:formatter,
|
||||
args:args
|
||||
}
|
||||
}),
|
||||
match:match
|
||||
};
|
||||
if(vars.findIndex(varDef=>((varDef.name===varItem.name) && (varItem.formatters.toString() == varDef.formatters.toString())))===-1){
|
||||
vars.push(varItem);
|
||||
}
|
||||
return ""
|
||||
});
|
||||
return vars
|
||||
}
|
||||
/**
|
||||
* 遍历str中的所有插值变量传递给callback,将callback返回的结果替换到str中对应的位置
|
||||
* @param {*} str
|
||||
* @param {Function(<变量名称>,[formatters],match[0])} callback
|
||||
* @returns 返回替换后的字符串
|
||||
*/
|
||||
function forEachInterpolatedVars(str,callback,options={}){
|
||||
let result=str, match;
|
||||
let opts = Object.assign({
|
||||
replaceAll:true, // 是否替换所有插值变量,当使用命名插值时应置为true,当使用位置插值时应置为false
|
||||
},options);
|
||||
varWithPipeRegexp.lastIndex=0;
|
||||
while ((match = varWithPipeRegexp.exec(result)) !== null) {
|
||||
const varname = match.groups.varname || "";
|
||||
// 解析格式化器和参数 = [<formatterName>,[<formatterName>,[<arg>,<arg>,...]]]
|
||||
const formatters = parseFormatters(match.groups.formatters);
|
||||
if(typeof(callback)==="function"){
|
||||
try{
|
||||
if(opts.replaceAll){
|
||||
result=result.replaceAll(match[0],callback(varname,formatters,match[0]));
|
||||
}else {
|
||||
result=result.replace(match[0],callback(varname,formatters,match[0]));
|
||||
}
|
||||
}catch{// callback函数可能会抛出异常,如果抛出异常,则中断匹配过程
|
||||
break
|
||||
}
|
||||
}
|
||||
varWithPipeRegexp.lastIndex=0;
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function resetScopeCache(scope,activeLanguage=null){
|
||||
scope.$cache = {activeLanguage,typedFormatters:{},formatters:{}};
|
||||
}
|
||||
/**
|
||||
* 取得指定数据类型的默认格式化器
|
||||
*
|
||||
* 可以为每一个数据类型指定一个默认的格式化器,当传入插值变量时,
|
||||
* 会自动调用该格式化器来对值进行格式化转换
|
||||
|
||||
const formatters = {
|
||||
"*":{
|
||||
$types:{...} // 在所有语言下只作用于特定数据类型的格式化器
|
||||
}, // 在所有语言下生效的格式化器
|
||||
cn:{
|
||||
$types:{
|
||||
[数据类型]:(value)=>{...},
|
||||
},
|
||||
[格式化器名称]:(value)=>{...},
|
||||
[格式化器名称]:(value)=>{...},
|
||||
[格式化器名称]:(value)=>{...},
|
||||
},
|
||||
}
|
||||
* @param {*} scope
|
||||
* @param {*} activeLanguage
|
||||
* @param {*} dataType 数字类型
|
||||
* @returns {Function} 格式化函数
|
||||
*/
|
||||
function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){
|
||||
if(!scope.$cache) resetScopeCache(scope);
|
||||
if(scope.$cache.activeLanguage === activeLanguage) {
|
||||
if(dataType in scope.$cache.typedFormatters) return scope.$cache.typedFormatters[dataType]
|
||||
}else {// 当语言切换时清空缓存
|
||||
resetScopeCache(scope,activeLanguage);
|
||||
}
|
||||
|
||||
// 先在当前作用域中查找,再在全局查找
|
||||
const targets = [scope.formatters,scope.global.formatters];
|
||||
for(const target of targets){
|
||||
if(!target) continue
|
||||
// 优先在当前语言的$types中查找
|
||||
if((activeLanguage in target) && isPlainObject(target[activeLanguage].$types)){
|
||||
let formatters = target[activeLanguage].$types;
|
||||
if(dataType in formatters && typeof(formatters[dataType])==="function"){
|
||||
return scope.$cache.typedFormatters[dataType] = formatters[dataType]
|
||||
}
|
||||
}
|
||||
// 在所有语言的$types中查找
|
||||
if(("*" in target) && isPlainObject(target["*"].$types)){
|
||||
let formatters = target["*"].$types;
|
||||
if(dataType in formatters && typeof(formatters[dataType])==="function"){
|
||||
return scope.$cache.typedFormatters[dataType] = formatters[dataType]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定名称的格式化器函数
|
||||
* @param {*} scope
|
||||
* @param {*} activeLanguage
|
||||
* @param {*} name 格式化器名称
|
||||
* @returns {Function} 格式化函数
|
||||
*/
|
||||
function getFormatter(scope,activeLanguage,name){
|
||||
// 缓存格式化器引用,避免重复检索
|
||||
if(!scope.$cache) resetScopeCache(scope);
|
||||
if(scope.$cache.activeLanguage === activeLanguage) {
|
||||
if(name in scope.$cache.formatters) return scope.$cache.formatters[name]
|
||||
}else {// 当语言切换时清空缓存
|
||||
resetScopeCache(scope,activeLanguage);
|
||||
}
|
||||
// 先在当前作用域中查找,再在全局查找
|
||||
const targets = [scope.formatters,scope.global.formatters];
|
||||
for(const target of targets){
|
||||
// 优先在当前语言查找
|
||||
if(activeLanguage in target){
|
||||
let formatters = target[activeLanguage] || {};
|
||||
if((name in formatters) && typeof(formatters[name])==="function") return scope.$cache.formatters[name] = formatters[name]
|
||||
}
|
||||
// 在所有语言的$types中查找
|
||||
let formatters = target["*"] || {};
|
||||
if((name in formatters) && typeof(formatters[name])==="function") return scope.$cache.formatters[name] = formatters[name]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行格式化器并返回结果
|
||||
* @param {*} value
|
||||
* @param {*} formatters 多个格式化器顺序执行,前一个输出作为下一个格式化器的输入
|
||||
*/
|
||||
function executeFormatter(value,formatters){
|
||||
if(formatters.length===0) return value
|
||||
let result = value;
|
||||
try{
|
||||
for(let formatter of formatters){
|
||||
if(typeof(formatter) === "function") {
|
||||
result = formatter(result);
|
||||
}else {// 如果碰到无效的格式化器,则跳过过续的格式化器
|
||||
return result
|
||||
}
|
||||
}
|
||||
}catch(e){
|
||||
console.error(`Error while execute i18n formatter for ${value}: ${e.message} ` );
|
||||
}
|
||||
return result
|
||||
}
|
||||
/**
|
||||
* 将 [[格式化器名称,[参数,参数,...]],[格式化器名称,[参数,参数,...]]]格式化器转化为
|
||||
*
|
||||
*
|
||||
*
|
||||
* @param {*} scope
|
||||
* @param {*} activeLanguage
|
||||
* @param {*} formatters
|
||||
*/
|
||||
function buildFormatters(scope,activeLanguage,formatters){
|
||||
let results = [];
|
||||
for(let formatter of formatters){
|
||||
if(formatter[0]){
|
||||
const func = getFormatter(scope,activeLanguage,formatter[0]);
|
||||
if(typeof(func)==="function"){
|
||||
results.push((v)=>{
|
||||
return func(v,...formatter[1])
|
||||
});
|
||||
}else {
|
||||
// 格式化器无效或者没有定义时,查看当前值是否具有同名的原型方法,如果有则执行调用
|
||||
// 比如padStart格式化器是String的原型方法,不需要配置就可以直接作为格式化器调用
|
||||
results.push((v)=>{
|
||||
if(typeof(v[formatter[0]])==="function"){
|
||||
return v[formatter[0]].call(v,...formatter[1])
|
||||
}else {
|
||||
return v
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 将value经过格式化器处理后返回
|
||||
* @param {*} scope
|
||||
* @param {*} activeLanguage
|
||||
* @param {*} formatters
|
||||
* @param {*} value
|
||||
* @returns
|
||||
*/
|
||||
function getFormattedValue(scope,activeLanguage,formatters,value){
|
||||
// 1. 取得格式化器函数列表
|
||||
const formatterFuncs = buildFormatters(scope,activeLanguage,formatters);
|
||||
// 2. 查找每种数据类型默认格式化器,并添加到formatters最前面,默认数据类型格式化器优先级最高
|
||||
const defaultFormatter = getDataTypeDefaultFormatter(scope,activeLanguage,getDataTypeName(value));
|
||||
if(defaultFormatter){
|
||||
formatterFuncs.splice(0,0,defaultFormatter);
|
||||
}
|
||||
// 3. 执行格式化器
|
||||
value = executeFormatter(value,formatterFuncs);
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串可以进行变量插值替换,
|
||||
* replaceInterpolatedVars("<模板字符串>",{变量名称:变量值,变量名称:变量值,...})
|
||||
* replaceInterpolatedVars("<模板字符串>",[变量值,变量值,...])
|
||||
* replaceInterpolatedVars("<模板字符串>",变量值,变量值,...])
|
||||
*
|
||||
- 当只有两个参数并且第2个参数是{}时,将第2个参数视为命名变量的字典
|
||||
replaceInterpolatedVars("this is {a}+{b},{a:1,b:2}) --> this is 1+2
|
||||
- 当只有两个参数并且第2个参数是[]时,将第2个参数视为位置参数
|
||||
replaceInterpolatedVars"this is {}+{}",[1,2]) --> this is 1+2
|
||||
- 普通位置参数替换
|
||||
replaceInterpolatedVars("this is {a}+{b}",1,2) --> this is 1+2
|
||||
-
|
||||
this == scope == { formatters: {}, ... }
|
||||
* @param {*} template
|
||||
* @returns
|
||||
*/
|
||||
function replaceInterpolatedVars(template,...args) {
|
||||
const scope = this;
|
||||
// 当前激活语言
|
||||
const activeLanguage = scope.global.activeLanguage;
|
||||
|
||||
// 没有变量插值则的返回原字符串
|
||||
if(args.length===0 || !hasInterpolation(template)) return template
|
||||
|
||||
// ****************************变量插值****************************
|
||||
if(args.length===1 && isPlainObject(args[0])){
|
||||
// 读取模板字符串中的插值变量列表
|
||||
// [[var1,[formatter,formatter,...],match],[var2,[formatter,formatter,...],match],...}
|
||||
let varValues = args[0];
|
||||
return forEachInterpolatedVars(template,(varname,formatters)=>{
|
||||
let value = (varname in varValues) ? varValues[varname] : '';
|
||||
return getFormattedValue(scope,activeLanguage,formatters,value)
|
||||
})
|
||||
}else {
|
||||
// ****************************位置插值****************************
|
||||
// 如果只有一个Array参数,则认为是位置变量列表,进行展开
|
||||
const params=(args.length===1 && Array.isArray(args[0])) ? [...args[0]] : args;
|
||||
if(params.length===0) return template // 没有变量则不需要进行插值处理,返回原字符串
|
||||
let i = 0;
|
||||
return forEachInterpolatedVars(template,(varname,formatters)=>{
|
||||
if(params.length>i){
|
||||
return getFormattedValue(scope,activeLanguage,formatters,params[i++])
|
||||
}else {
|
||||
throw new Error() // 抛出异常,停止插值处理
|
||||
}
|
||||
},{replaceAll:false})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 默认语言配置
|
||||
const defaultLanguageSettings = {
|
||||
defaultLanguage: "cn",
|
||||
activeLanguage: "cn",
|
||||
languages:[
|
||||
{name:"cn",title:"中文",default:true},
|
||||
{name:"en",title:"英文"}
|
||||
],
|
||||
formatters
|
||||
};
|
||||
|
||||
function isMessageId(content){
|
||||
return parseInt(content)>0
|
||||
}
|
||||
/**
|
||||
* 根据值的单数和复数形式,从messages中取得相应的消息
|
||||
*
|
||||
* @param {*} messages 复数形式的文本内容 = [<=0时的内容>,<=1时的内容>,<=2时的内容>,...]
|
||||
* @param {*} value
|
||||
*/
|
||||
function getPluraMessage(messages,value){
|
||||
try{
|
||||
if(Array.isArray(messages)){
|
||||
return messages.length > value ? messages[value] : messages[messages.length-1]
|
||||
}else {
|
||||
return messages
|
||||
}
|
||||
}catch{
|
||||
return Array.isArray(messages) ? messages[0] : messages
|
||||
}
|
||||
}
|
||||
function escape(str){
|
||||
return str.replaceAll(/\\(?![trnbvf'"]{1})/g,"\\\\")
|
||||
.replaceAll("\t","\\t")
|
||||
.replaceAll("\n","\\n")
|
||||
.replaceAll("\b","\\b")
|
||||
.replaceAll("\r","\\r")
|
||||
.replaceAll("\f","\\f")
|
||||
.replaceAll("\'","\\'")
|
||||
.replaceAll('\"','\\"')
|
||||
.replaceAll('\v','\\v')
|
||||
}
|
||||
function unescape(str){
|
||||
return str
|
||||
.replaceAll("\\t","\t")
|
||||
.replaceAll("\\n","\n")
|
||||
.replaceAll("\\b","\b")
|
||||
.replaceAll("\\r","\r")
|
||||
.replaceAll("\\f","\f")
|
||||
.replaceAll("\\'","\'")
|
||||
.replaceAll('\\"','\"')
|
||||
.replaceAll('\\v','\v')
|
||||
.replaceAll(/\\\\(?![trnbvf'"]{1})/g,"\\")
|
||||
}
|
||||
/**
|
||||
* 翻译函数
|
||||
*
|
||||
* translate("要翻译的文本内容") 如果默认语言是中文,则不会进行翻译直接返回
|
||||
* translate("I am {} {}","man") == I am man 位置插值
|
||||
* translate("I am {p}",{p:"man"}) 字典插值
|
||||
* translate("total {$count} items", {$count:1}) //复数形式
|
||||
* translate("total {} {} {} items",a,b,c) // 位置变量插值
|
||||
*
|
||||
* this===scope 当前绑定的scope
|
||||
*
|
||||
*/
|
||||
function translate(message) {
|
||||
const scope = this;
|
||||
const activeLanguage = scope.global.activeLanguage;
|
||||
let content = message;
|
||||
let vars=[]; // 插值变量列表
|
||||
let pluralVars= []; // 复数变量
|
||||
let pluraValue = null; // 复数值
|
||||
if(!typeof(message)==="string") return message
|
||||
try{
|
||||
// 1. 预处理变量: 复数变量保存至pluralVars中 , 变量如果是Function则调用
|
||||
if(arguments.length === 2 && isPlainObject(arguments[1])){
|
||||
Object.entries(arguments[1]).forEach(([name,value])=>{
|
||||
if(typeof(value)==="function"){
|
||||
try{
|
||||
vars[name] = value();
|
||||
}catch(e){
|
||||
vars[name] = value;
|
||||
}
|
||||
}
|
||||
// 以$开头的视为复数变量
|
||||
if(name.startsWith("$") && typeof(vars[name])==="number") pluralVars.push(name);
|
||||
});
|
||||
vars = [arguments[1]];
|
||||
}else if(arguments.length >= 2){
|
||||
vars = [...arguments].splice(1).map((arg,index)=>{
|
||||
try{
|
||||
arg = typeof(arg)==="function" ? arg() : arg;
|
||||
// 位置参数中以第一个数值变量为复数变量
|
||||
if(isNumber(arg)) pluraValue = parseInt(arg);
|
||||
}catch(e){ }
|
||||
return arg
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// 3. 取得翻译文本模板字符串
|
||||
if(activeLanguage === scope.defaultLanguage){
|
||||
// 2.1 从默认语言中取得翻译文本模板字符串
|
||||
// 如果当前语言就是默认语言,不需要查询加载,只需要做插值变换即可
|
||||
// 当源文件运用了babel插件后会将原始文本内容转换为msgId
|
||||
// 如果是msgId则从scope.default中读取,scope.default=默认语言包={<id>:<message>}
|
||||
if(isMessageId(content)){
|
||||
content = scope.default[content] || message;
|
||||
}
|
||||
}else {
|
||||
// 2.2 从当前语言包中取得翻译文本模板字符串
|
||||
// 如果没有启用babel插件将源文本转换为msgId,需要先将文本内容转换为msgId
|
||||
// JSON.stringify在进行转换时会将\t\n\r转换为\\t\\n\\r,这样在进行匹配时就出错
|
||||
let msgId = isMessageId(content) ? content : scope.idMap[escape(content)];
|
||||
content = scope.messages[msgId] || content;
|
||||
content = Array.isArray(content) ? content.map(v=>unescape(v)) : unescape(content);
|
||||
}
|
||||
// 2. 处理复数
|
||||
// 经过上面的处理,content可能是字符串或者数组
|
||||
// content = "原始文本内容" || 复数形式["原始文本内容","原始文本内容"....]
|
||||
// 如果是数组说明要启用复数机制,需要根据插值变量中的某个变量来判断复数形式
|
||||
if(Array.isArray(content) && content.length>0){
|
||||
// 如果存在复数命名变量,只取第一个复数变量
|
||||
if(pluraValue!==null){ // 启用的是位置插值,pluraIndex=第一个数字变量的位置
|
||||
content = getPluraMessage(content,pluraValue);
|
||||
}else if(pluralVar.length>0){
|
||||
content = getPluraMessage(content,parseInt(vars(pluralVar[0])));
|
||||
}else { // 如果找不到复数变量,则使用第一个内容
|
||||
content = content[0];
|
||||
}
|
||||
}
|
||||
|
||||
// 进行插值处理
|
||||
if(vars.length==0){
|
||||
return content
|
||||
}else {
|
||||
return replaceInterpolatedVars.call(scope,content,...vars)
|
||||
}
|
||||
}catch(e){
|
||||
return content // 出错则返回原始文本
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多语言管理类
|
||||
*
|
||||
* 当导入编译后的多语言文件时(import("./languages")),会自动生成全局实例VoerkaI18n
|
||||
*
|
||||
* VoerkaI18n.languages // 返回支持的语言列表
|
||||
* VoerkaI18n.defaultLanguage // 默认语言
|
||||
* VoerkaI18n.language // 当前语言
|
||||
* VoerkaI18n.change(language) // 切换到新的语言
|
||||
*
|
||||
*
|
||||
* VoerkaI18n.on("change",(language)=>{}) // 注册语言切换事件
|
||||
* VoerkaI18n.off("change",(language)=>{})
|
||||
*
|
||||
* */
|
||||
class I18nManager extends EventEmitter{
|
||||
constructor(settings={}){
|
||||
super();
|
||||
if(I18nManager.instance!=null){
|
||||
return I18nManager.instance;
|
||||
}
|
||||
I18nManager.instance = this;
|
||||
this._settings = deepMerge(defaultLanguageSettings,settings);
|
||||
this._scopes=[];
|
||||
return I18nManager.instance;
|
||||
}
|
||||
get settings(){ return this._settings }
|
||||
get scopes(){ return this._scopes }
|
||||
// 当前激活语言
|
||||
get activeLanguage(){ return this._settings.activeLanguage}
|
||||
// 默认语言
|
||||
get defaultLanguage(){ return this._settings.defaultLanguage}
|
||||
// 支持的语言列表
|
||||
get languages(){ return this._settings.languages}
|
||||
// 内置格式化器
|
||||
get formatters(){ return inlineFormatters }
|
||||
/**
|
||||
* 切换语言
|
||||
*/
|
||||
async change(value){
|
||||
value=value.trim();
|
||||
if(this.languages.findIndex(lang=>lang.name === value)!==-1){
|
||||
// 通知所有作用域刷新到对应的语言包
|
||||
await this._refreshScopes(value);
|
||||
this._settings.activeLanguage = value;
|
||||
/// 触发语言切换事件
|
||||
await this.emit(value);
|
||||
}else {
|
||||
throw new Error("Not supported language:"+value)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 当切换语言时调用此方法来加载更新语言包
|
||||
* @param {*} newLanguage
|
||||
*/
|
||||
async _refreshScopes(newLanguage){
|
||||
// 并发执行所有作用域语言包的加载
|
||||
try{
|
||||
const scopeRefreshers = this._scopes.map(scope=>{
|
||||
return scope.refresh(newLanguage)
|
||||
});
|
||||
if(Promise.allSettled){
|
||||
await Promise.allSettled(scopeRefreshers);
|
||||
}else {
|
||||
await Promise.all(scopeRefreshers);
|
||||
}
|
||||
}catch(e){
|
||||
console.warn("Error while refreshing i18n scopes:",e.message);
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* 注册一个新的作用域
|
||||
*
|
||||
* 每一个库均对应一个作用域,每个作用域可以有多个语言包,且对应一个翻译函数
|
||||
* 除了默认语言外,其他语言采用动态加载的方式
|
||||
*
|
||||
* @param {*} scope
|
||||
*/
|
||||
async register(scope){
|
||||
if(!(scope instanceof i18nScope)){
|
||||
throw new TypeError("Scope must be an instance of I18nScope")
|
||||
}
|
||||
this._scopes.push(scope);
|
||||
await scope.refresh(this.activeLanguage);
|
||||
}
|
||||
/**
|
||||
* 注册全局格式化器
|
||||
* 格式化器是一个简单的同步函数value=>{...},用来对输入进行格式化后返回结果
|
||||
*
|
||||
* registerFormatters(name,value=>{...}) // 适用于所有语言
|
||||
* registerFormatters(name,value=>{...},{langauge:"cn"}) // 适用于cn语言
|
||||
* registerFormatters(name,value=>{...},{langauge:"en"}) // 适用于en语言
|
||||
|
||||
* @param {*} formatters
|
||||
*/
|
||||
registerFormatter(name,formatter,{language="*"}={}){
|
||||
if(!typeof(formatter)==="function" || typeof(name)!=="string"){
|
||||
throw new TypeError("Formatter must be a function")
|
||||
}
|
||||
if(DataTypes$1.includes(name)){
|
||||
this.formatters[language].$types[name] = formatter;
|
||||
}else {
|
||||
this.formatters[language][name] = formatter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var runtime ={
|
||||
getInterpolatedVars,
|
||||
replaceInterpolatedVars,
|
||||
I18nManager,
|
||||
translate,
|
||||
languages,
|
||||
i18nScope,
|
||||
defaultLanguageSettings,
|
||||
getDataTypeName,
|
||||
isNumber,
|
||||
isPlainObject
|
||||
};
|
||||
|
||||
module.exports = runtime;
|
940
packages/runtime/dist/runtime.mjs
vendored
940
packages/runtime/dist/runtime.mjs
vendored
@ -1,940 +0,0 @@
|
||||
/**
|
||||
* 判断是否是JSON对象
|
||||
* @param {*} obj
|
||||
* @returns
|
||||
*/
|
||||
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))
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单进行对象合并
|
||||
*
|
||||
* options={
|
||||
* array:0 , // 数组合并策略,0-替换,1-合并,2-去重合并
|
||||
* }
|
||||
*
|
||||
* @param {*} toObj
|
||||
* @param {*} formObj
|
||||
* @returns 合并后的对象
|
||||
*/
|
||||
function deepMerge$1(toObj,formObj,options={}){
|
||||
let results = Object.assign({},toObj);
|
||||
Object.entries(formObj).forEach(([key,value])=>{
|
||||
if(key in results){
|
||||
if(typeof value === "object" && value !== null){
|
||||
if(Array.isArray(value)){
|
||||
if(options.array === 0){
|
||||
results[key] = value;
|
||||
}else if(options.array === 1){
|
||||
results[key] = [...results[key],...value];
|
||||
}else if(options.array === 2){
|
||||
results[key] = [...new Set([...results[key],...value])];
|
||||
}
|
||||
}else {
|
||||
results[key] = deepMerge$1(results[key],value,options);
|
||||
}
|
||||
}else {
|
||||
results[key] = value;
|
||||
}
|
||||
}else {
|
||||
results[key] = value;
|
||||
}
|
||||
});
|
||||
return results
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定变量类型名称
|
||||
* 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
var utils ={
|
||||
isPlainObject: isPlainObject$1,
|
||||
isNumber: isNumber$1,
|
||||
deepMerge: deepMerge$1,
|
||||
getDataTypeName: getDataTypeName$1
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* 简单的事件触发器
|
||||
*
|
||||
*/
|
||||
|
||||
var eventemitter = class EventEmitter{
|
||||
constructor(){
|
||||
this._callbacks = [];
|
||||
}
|
||||
on(callback){
|
||||
if(this._callbacks.includes(callback)) return
|
||||
this._callbacks.push(callback);
|
||||
}
|
||||
off(callback){
|
||||
for(let i=0;i<this._callbacks.length;i++){
|
||||
if(this._callbacks[i]===callback ){
|
||||
this._callbacks.splice(i,1);
|
||||
}
|
||||
}
|
||||
}
|
||||
offAll(){
|
||||
this._callbacks = [];
|
||||
}
|
||||
async emit(...args){
|
||||
if(Promise.allSettled){
|
||||
await Promise.allSettled(this._callbacks.map(cb=>cb(...args)));
|
||||
}else {
|
||||
await Promise.all(this._callbacks.map(cb=>cb(...args)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var scope = class i18nScope {
|
||||
constructor(options={},callback){
|
||||
// 每个作用域都有一个唯一的id
|
||||
this._id = options.id || (new Date().getTime().toString()+parseInt(Math.random()*1000));
|
||||
this._languages = options.languages; // 当前作用域的语言列表
|
||||
this._defaultLanguage = options.defaultLanguage || "cn"; // 默认语言名称
|
||||
this._activeLanguage = options.activeLanguage; // 当前语言名称
|
||||
this._default = options.default; // 默认语言包
|
||||
this._messages = options.messages; // 当前语言包
|
||||
this._idMap = options.idMap; // 消息id映射列表
|
||||
this._formatters = options.formatters; // 当前作用域的格式化函数列表
|
||||
this._loaders = options.loaders; // 异步加载语言文件的函数列表
|
||||
this._global = null; // 引用全局VoerkaI18n配置,注册后自动引用
|
||||
// 主要用来缓存格式化器的引用,当使用格式化器时可以直接引用,避免检索
|
||||
this.$cache={
|
||||
activeLanguage : null,
|
||||
typedFormatters: {},
|
||||
formatters : {},
|
||||
};
|
||||
// 如果不存在全局VoerkaI18n实例,说明当前Scope是唯一或第一个加载的作用域,
|
||||
// 则使用当前作用域来初始化全局VoerkaI18n实例
|
||||
if(!globalThis.VoerkaI18n){
|
||||
const { I18nManager } = runtime;
|
||||
globalThis.VoerkaI18n = new I18nManager({
|
||||
defaultLanguage: this.defaultLanguage,
|
||||
activeLanguage : this.activeLanguage,
|
||||
languages: options.languages,
|
||||
});
|
||||
}
|
||||
this.global = globalThis.VoerkaI18n;
|
||||
// 正在加载语言包标识
|
||||
this._loading=false;
|
||||
// 在全局注册作用域
|
||||
this.register(callback);
|
||||
}
|
||||
// 作用域
|
||||
get id(){return this._id}
|
||||
// 默认语言名称
|
||||
get defaultLanguage(){return this._defaultLanguage}
|
||||
// 默认语言名称
|
||||
get activeLanguage(){return this._activeLanguage}
|
||||
// 默认语言包
|
||||
get default(){return this._default}
|
||||
// 当前语言包
|
||||
get messages(){return this._messages}
|
||||
// 消息id映射列表
|
||||
get idMap(){return this._idMap}
|
||||
// 当前作用域的格式化函数列表
|
||||
get formatters(){return this._formatters}
|
||||
// 异步加载语言文件的函数列表
|
||||
get loaders(){return this._loaders}
|
||||
// 引用全局VoerkaI18n配置,注册后自动引用
|
||||
get global(){return this._global}
|
||||
set global(value){this._global = value;}
|
||||
/**
|
||||
* 在全局注册作用域
|
||||
* @param {*} callback 当注册
|
||||
*/
|
||||
register(callback){
|
||||
if(!typeof(callback)==="function") callback = ()=>{};
|
||||
this.global.register(this).then(callback).catch(callback);
|
||||
}
|
||||
registerFormatter(name,formatter,{language="*"}={}){
|
||||
if(!typeof(formatter)==="function" || typeof(name)!=="string"){
|
||||
throw new TypeError("Formatter must be a function")
|
||||
}
|
||||
if(DataTypes.includes(name)){
|
||||
this.formatters[language].$types[name] = formatter;
|
||||
}else {
|
||||
this.formatters[language][name] = formatter;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 回退到默认语言
|
||||
*/
|
||||
_fallback(){
|
||||
this._messages = this._default;
|
||||
this._activeLanguage = this.defaultLanguage;
|
||||
}
|
||||
/**
|
||||
* 刷新当前语言包
|
||||
* @param {*} newLanguage
|
||||
*/
|
||||
async refresh(newLanguage){
|
||||
this._loading = Promise.resolve();
|
||||
if(!newLanguage) newLanguage = this.activeLanguage;
|
||||
// 默认语言,默认语言采用静态加载方式,只需要简单的替换即可
|
||||
if(newLanguage === this.defaultLanguage){
|
||||
this._messages = this._default;
|
||||
return
|
||||
}
|
||||
// 非默认语言需要异步加载语言包文件,加载器是一个异步函数
|
||||
// 如果没有加载器,则无法加载语言包,因此回退到默认语言
|
||||
const loader = this.loaders[newLanguage];
|
||||
if(typeof(loader) === "function"){
|
||||
try{
|
||||
this._messages = (await loader()).default;
|
||||
this._activeLanguage = newLanguage;
|
||||
}catch(e){
|
||||
console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`);
|
||||
this._fallback();
|
||||
}
|
||||
}else {
|
||||
this._fallback();
|
||||
}
|
||||
}
|
||||
// 以下方法引用全局VoerkaI18n实例的方法
|
||||
get on(){return this.global.on.bind(this.global)}
|
||||
get off(){return this.global.off.bind(this.global)}
|
||||
get offAll(){return this.global.offAll.bind(this.global)}
|
||||
get change(){
|
||||
return this.global.change.bind(this.global)
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 内置的格式化器
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 字典格式化器
|
||||
* 根据输入data的值,返回后续参数匹配的结果
|
||||
* dict(data,<value1>,<result1>,<value2>,<result1>,<value3>,<result1>,...)
|
||||
*
|
||||
*
|
||||
* dict(1,1,"one",2,"two",3,"three",4,"four") == "one"
|
||||
* dict(2,1,"one",2,"two",3,"three",4,"four") == "two"
|
||||
* dict(3,1,"one",2,"two",3,"three",4,"four") == "three"
|
||||
* dict(4,1,"one",2,"two",3,"three",4,"four") == "four"
|
||||
* // 无匹配时返回原始值
|
||||
* dict(5,1,"one",2,"two",3,"three",4,"four") == 5
|
||||
* // 无匹配时并且后续参数个数是奇数,则返回最后一个参数
|
||||
* dict(5,1,"one",2,"two",3,"three",4,"four","more") == "more"
|
||||
*
|
||||
* 在翻译中使用
|
||||
* I have { value | dict(1,"one",2,"two",3,"three",4,"four")} apples
|
||||
*
|
||||
* @param {*} value
|
||||
* @param {...any} args
|
||||
* @returns
|
||||
*/
|
||||
function dict(value,...args){
|
||||
for(let i=0;i<args.length;i+=2){
|
||||
if(args[i]===value){
|
||||
return args[i+1]
|
||||
}
|
||||
}
|
||||
if(args.length >0 && (args.length % 2!==0)) return args[args.length-1]
|
||||
return value
|
||||
}
|
||||
|
||||
var formatters$1 = {
|
||||
"*":{
|
||||
$types:{
|
||||
Date:(value)=>value.toLocaleString()
|
||||
},
|
||||
time:(value)=> value.toLocaleTimeString(),
|
||||
shorttime:(value)=> value.toLocaleTimeString(),
|
||||
date: (value)=> value.toLocaleDateString(),
|
||||
dict, //字典格式化器
|
||||
},
|
||||
cn:{
|
||||
$types:{
|
||||
Date:(value)=> `${value.getFullYear()}年${value.getMonth()+1}月${value.getDate()}日 ${value.getHours()}点${value.getMinutes()}分${value.getSeconds()}秒`
|
||||
},
|
||||
shortime:(value)=> value.toLocaleTimeString(),
|
||||
time:(value)=>`${value.getHours()}点${value.getMinutes()}分${value.getSeconds()}秒`,
|
||||
date: (value)=> `${value.getFullYear()}年${value.getMonth()+1}月${value.getDate()}日`,
|
||||
shortdate: (value)=> `${value.getFullYear()}-${value.getMonth()+1}-${value.getDate()}`,
|
||||
currency:(value)=>`${value}元`,
|
||||
},
|
||||
en:{
|
||||
currency:(value)=>{
|
||||
return `$${value}`
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const { getDataTypeName,isNumber,isPlainObject,deepMerge } = utils;
|
||||
const EventEmitter = eventemitter;
|
||||
const i18nScope = scope;
|
||||
let inlineFormatters = formatters$1; // 内置格式化器
|
||||
|
||||
|
||||
|
||||
// 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter }
|
||||
// 不支持参数: let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*\s*)*)\s*\}/g
|
||||
|
||||
// 支持参数: { var | formatter(x,x,..) | formatter }
|
||||
let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*(\(.*\)){0,1}\s*)*)\s*\}/g;
|
||||
|
||||
// 有效的语言名称列表
|
||||
const languages = ["af","am","ar-dz","ar-iq","ar-kw","ar-ly","ar-ma","ar-sa","ar-tn","ar","az","be","bg","bi","bm","bn","bo","br","bs","ca","cs","cv","cy","da","de-at","de-ch","de","dv","el","en-au","en-ca","en-gb","en-ie","en-il","en-in","en-nz","en-sg","en-tt","en","eo","es-do","es-mx","es-pr","es-us","es","et","eu","fa","fi","fo","fr-ca","fr-ch","fr","fy","ga","gd","gl","gom-latn","gu","he","hi","hr","ht","hu","hy-am","id","is","it-ch","it","ja","jv","ka","kk","km","kn","ko","ku","ky","lb","lo","lt","lv","me","mi","mk","ml","mn","mr","ms-my","ms","mt","my","nb","ne","nl-be","nl","nn","oc-lnc","pa-in","pl","pt-br","pt","ro","ru","rw","sd","se","si","sk","sl","sq","sr-cyrl","sr","ss","sv-fi","sv","sw","ta","te","tet","tg","th","tk","tl-ph","tlh","tr","tzl","tzm-latn","tzm","ug-cn","uk","ur","uz-latn","uz","vi","x-pseudo","yo","zh-cn","zh-hk","zh-tw","zh"];
|
||||
|
||||
/**
|
||||
* 考虑到通过正则表达式进行插件的替换可能较慢,因此提供一个简单方法来过滤掉那些
|
||||
* 不需要进行插值处理的字符串
|
||||
* 原理很简单,就是判断一下是否同时具有{和}字符,如果有则认为可能有插值变量,如果没有则一定没有插件变量,则就不需要进行正则匹配
|
||||
* 从而可以减少不要的正则匹配
|
||||
* 注意:该方法只能快速判断一个字符串不包括插值变量
|
||||
* @param {*} str
|
||||
* @returns {boolean} true=可能包含插值变量,
|
||||
*/
|
||||
function hasInterpolation(str){
|
||||
return str.includes("{") && str.includes("}")
|
||||
}
|
||||
const DataTypes$1 = ["String","Number","Boolean","Object","Array","Function","Error","Symbol","RegExp","Date","Null","Undefined","Set","Map","WeakSet","WeakMap"];
|
||||
|
||||
|
||||
/**
|
||||
通过正则表达式对原始文本内容进行解析匹配后得到的
|
||||
formatters="| aaa(1,1) | bbb "
|
||||
|
||||
需要统一解析为
|
||||
|
||||
[
|
||||
[aaa,[1,1]], // [formatter'name,[args,...]]
|
||||
[bbb,[]],
|
||||
]
|
||||
|
||||
formatters="| aaa(1,1,"dddd") | bbb "
|
||||
|
||||
目前对参数采用简单的split(",")来解析,因为无法正确解析aaa(1,1,"dd,,dd")形式的参数
|
||||
在此场景下基本够用了,如果需要支持更复杂的参数解析,可以后续考虑使用正则表达式来解析
|
||||
|
||||
@returns [[<formatterName>,[<arg>,<arg>,...]]]
|
||||
*/
|
||||
function parseFormatters(formatters){
|
||||
if(!formatters) return []
|
||||
// 1. 先解析为 ["aaa()","bbb"]形式
|
||||
let result = formatters.trim().substr(1).trim().split("|").map(r=>r.trim());
|
||||
|
||||
// 2. 解析格式化器参数
|
||||
return result.map(formatter=>{
|
||||
let firstIndex = formatter.indexOf("(");
|
||||
let lastIndex = formatter.lastIndexOf(")");
|
||||
if(firstIndex!==-1 && lastIndex!==-1){ // 带参数的格式化器
|
||||
const argsContent = formatter.substr(firstIndex+1,lastIndex-firstIndex-1).trim();
|
||||
let args = argsContent=="" ? [] : argsContent.split(",").map(arg=>{
|
||||
arg = arg.trim();
|
||||
if(!isNaN(parseInt(arg))){
|
||||
return parseInt(arg) // 数字
|
||||
}else if((arg.startsWith('\"') && arg.endsWith('\"')) || (arg.startsWith('\'') && arg.endsWith('\'')) ){
|
||||
return arg.substr(1,arg.length-2) // 字符串
|
||||
}else if(arg.toLowerCase()==="true" || arg.toLowerCase()==="false"){
|
||||
return arg.toLowerCase()==="true" // 布尔值
|
||||
}else if((arg.startsWith('{') && arg.endsWith('}')) || (arg.startsWith('[') && arg.endsWith(']'))){
|
||||
try{
|
||||
return JSON.parse(arg)
|
||||
}catch(e){
|
||||
return String(arg)
|
||||
}
|
||||
}else {
|
||||
return String(arg)
|
||||
}
|
||||
});
|
||||
return [formatter.substr(0,firstIndex),args]
|
||||
}else {// 不带参数的格式化器
|
||||
return [formatter,[]]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取字符串中的插值变量
|
||||
* // [
|
||||
// {
|
||||
name:<变量名称>,formatters:[{name:<格式化器名称>,args:[<参数>,<参数>,....]]}],<匹配字符串>],
|
||||
// ....
|
||||
//
|
||||
* @param {*} str
|
||||
* @param {*} isFull =true 保留所有插值变量 =false 进行去重
|
||||
* @returns {Array}
|
||||
* [
|
||||
* {
|
||||
* name:"<变量名称>",
|
||||
* formatters:[
|
||||
* {name:"<格式化器名称>",args:[<参数>,<参数>,....]},
|
||||
* {name:"<格式化器名称>",args:[<参数>,<参数>,....]},
|
||||
* ],
|
||||
* match:"<匹配字符串>"
|
||||
* },
|
||||
* ...
|
||||
* ]
|
||||
*/
|
||||
function getInterpolatedVars(str){
|
||||
let vars = [];
|
||||
forEachInterpolatedVars(str,(varName,formatters,match)=>{
|
||||
let varItem = {
|
||||
name:varName,
|
||||
formatters:formatters.map(([formatter,args])=>{
|
||||
return {
|
||||
name:formatter,
|
||||
args:args
|
||||
}
|
||||
}),
|
||||
match:match
|
||||
};
|
||||
if(vars.findIndex(varDef=>((varDef.name===varItem.name) && (varItem.formatters.toString() == varDef.formatters.toString())))===-1){
|
||||
vars.push(varItem);
|
||||
}
|
||||
return ""
|
||||
});
|
||||
return vars
|
||||
}
|
||||
/**
|
||||
* 遍历str中的所有插值变量传递给callback,将callback返回的结果替换到str中对应的位置
|
||||
* @param {*} str
|
||||
* @param {Function(<变量名称>,[formatters],match[0])} callback
|
||||
* @returns 返回替换后的字符串
|
||||
*/
|
||||
function forEachInterpolatedVars(str,callback,options={}){
|
||||
let result=str, match;
|
||||
let opts = Object.assign({
|
||||
replaceAll:true, // 是否替换所有插值变量,当使用命名插值时应置为true,当使用位置插值时应置为false
|
||||
},options);
|
||||
varWithPipeRegexp.lastIndex=0;
|
||||
while ((match = varWithPipeRegexp.exec(result)) !== null) {
|
||||
const varname = match.groups.varname || "";
|
||||
// 解析格式化器和参数 = [<formatterName>,[<formatterName>,[<arg>,<arg>,...]]]
|
||||
const formatters = parseFormatters(match.groups.formatters);
|
||||
if(typeof(callback)==="function"){
|
||||
try{
|
||||
if(opts.replaceAll){
|
||||
result=result.replaceAll(match[0],callback(varname,formatters,match[0]));
|
||||
}else {
|
||||
result=result.replace(match[0],callback(varname,formatters,match[0]));
|
||||
}
|
||||
}catch{// callback函数可能会抛出异常,如果抛出异常,则中断匹配过程
|
||||
break
|
||||
}
|
||||
}
|
||||
varWithPipeRegexp.lastIndex=0;
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function resetScopeCache(scope,activeLanguage=null){
|
||||
scope.$cache = {activeLanguage,typedFormatters:{},formatters:{}};
|
||||
}
|
||||
/**
|
||||
* 取得指定数据类型的默认格式化器
|
||||
*
|
||||
* 可以为每一个数据类型指定一个默认的格式化器,当传入插值变量时,
|
||||
* 会自动调用该格式化器来对值进行格式化转换
|
||||
|
||||
const formatters = {
|
||||
"*":{
|
||||
$types:{...} // 在所有语言下只作用于特定数据类型的格式化器
|
||||
}, // 在所有语言下生效的格式化器
|
||||
cn:{
|
||||
$types:{
|
||||
[数据类型]:(value)=>{...},
|
||||
},
|
||||
[格式化器名称]:(value)=>{...},
|
||||
[格式化器名称]:(value)=>{...},
|
||||
[格式化器名称]:(value)=>{...},
|
||||
},
|
||||
}
|
||||
* @param {*} scope
|
||||
* @param {*} activeLanguage
|
||||
* @param {*} dataType 数字类型
|
||||
* @returns {Function} 格式化函数
|
||||
*/
|
||||
function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){
|
||||
if(!scope.$cache) resetScopeCache(scope);
|
||||
if(scope.$cache.activeLanguage === activeLanguage) {
|
||||
if(dataType in scope.$cache.typedFormatters) return scope.$cache.typedFormatters[dataType]
|
||||
}else {// 当语言切换时清空缓存
|
||||
resetScopeCache(scope,activeLanguage);
|
||||
}
|
||||
|
||||
// 先在当前作用域中查找,再在全局查找
|
||||
const targets = [scope.formatters,scope.global.formatters];
|
||||
for(const target of targets){
|
||||
if(!target) continue
|
||||
// 优先在当前语言的$types中查找
|
||||
if((activeLanguage in target) && isPlainObject(target[activeLanguage].$types)){
|
||||
let formatters = target[activeLanguage].$types;
|
||||
if(dataType in formatters && typeof(formatters[dataType])==="function"){
|
||||
return scope.$cache.typedFormatters[dataType] = formatters[dataType]
|
||||
}
|
||||
}
|
||||
// 在所有语言的$types中查找
|
||||
if(("*" in target) && isPlainObject(target["*"].$types)){
|
||||
let formatters = target["*"].$types;
|
||||
if(dataType in formatters && typeof(formatters[dataType])==="function"){
|
||||
return scope.$cache.typedFormatters[dataType] = formatters[dataType]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定名称的格式化器函数
|
||||
* @param {*} scope
|
||||
* @param {*} activeLanguage
|
||||
* @param {*} name 格式化器名称
|
||||
* @returns {Function} 格式化函数
|
||||
*/
|
||||
function getFormatter(scope,activeLanguage,name){
|
||||
// 缓存格式化器引用,避免重复检索
|
||||
if(!scope.$cache) resetScopeCache(scope);
|
||||
if(scope.$cache.activeLanguage === activeLanguage) {
|
||||
if(name in scope.$cache.formatters) return scope.$cache.formatters[name]
|
||||
}else {// 当语言切换时清空缓存
|
||||
resetScopeCache(scope,activeLanguage);
|
||||
}
|
||||
// 先在当前作用域中查找,再在全局查找
|
||||
const targets = [scope.formatters,scope.global.formatters];
|
||||
for(const target of targets){
|
||||
// 优先在当前语言查找
|
||||
if(activeLanguage in target){
|
||||
let formatters = target[activeLanguage] || {};
|
||||
if((name in formatters) && typeof(formatters[name])==="function") return scope.$cache.formatters[name] = formatters[name]
|
||||
}
|
||||
// 在所有语言的$types中查找
|
||||
let formatters = target["*"] || {};
|
||||
if((name in formatters) && typeof(formatters[name])==="function") return scope.$cache.formatters[name] = formatters[name]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行格式化器并返回结果
|
||||
* @param {*} value
|
||||
* @param {*} formatters 多个格式化器顺序执行,前一个输出作为下一个格式化器的输入
|
||||
*/
|
||||
function executeFormatter(value,formatters){
|
||||
if(formatters.length===0) return value
|
||||
let result = value;
|
||||
try{
|
||||
for(let formatter of formatters){
|
||||
if(typeof(formatter) === "function") {
|
||||
result = formatter(result);
|
||||
}else {// 如果碰到无效的格式化器,则跳过过续的格式化器
|
||||
return result
|
||||
}
|
||||
}
|
||||
}catch(e){
|
||||
console.error(`Error while execute i18n formatter for ${value}: ${e.message} ` );
|
||||
}
|
||||
return result
|
||||
}
|
||||
/**
|
||||
* 将 [[格式化器名称,[参数,参数,...]],[格式化器名称,[参数,参数,...]]]格式化器转化为
|
||||
*
|
||||
*
|
||||
*
|
||||
* @param {*} scope
|
||||
* @param {*} activeLanguage
|
||||
* @param {*} formatters
|
||||
*/
|
||||
function buildFormatters(scope,activeLanguage,formatters){
|
||||
let results = [];
|
||||
for(let formatter of formatters){
|
||||
if(formatter[0]){
|
||||
const func = getFormatter(scope,activeLanguage,formatter[0]);
|
||||
if(typeof(func)==="function"){
|
||||
results.push((v)=>{
|
||||
return func(v,...formatter[1])
|
||||
});
|
||||
}else {
|
||||
// 格式化器无效或者没有定义时,查看当前值是否具有同名的原型方法,如果有则执行调用
|
||||
// 比如padStart格式化器是String的原型方法,不需要配置就可以直接作为格式化器调用
|
||||
results.push((v)=>{
|
||||
if(typeof(v[formatter[0]])==="function"){
|
||||
return v[formatter[0]].call(v,...formatter[1])
|
||||
}else {
|
||||
return v
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 将value经过格式化器处理后返回
|
||||
* @param {*} scope
|
||||
* @param {*} activeLanguage
|
||||
* @param {*} formatters
|
||||
* @param {*} value
|
||||
* @returns
|
||||
*/
|
||||
function getFormattedValue(scope,activeLanguage,formatters,value){
|
||||
// 1. 取得格式化器函数列表
|
||||
const formatterFuncs = buildFormatters(scope,activeLanguage,formatters);
|
||||
// 2. 查找每种数据类型默认格式化器,并添加到formatters最前面,默认数据类型格式化器优先级最高
|
||||
const defaultFormatter = getDataTypeDefaultFormatter(scope,activeLanguage,getDataTypeName(value));
|
||||
if(defaultFormatter){
|
||||
formatterFuncs.splice(0,0,defaultFormatter);
|
||||
}
|
||||
// 3. 执行格式化器
|
||||
value = executeFormatter(value,formatterFuncs);
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串可以进行变量插值替换,
|
||||
* replaceInterpolatedVars("<模板字符串>",{变量名称:变量值,变量名称:变量值,...})
|
||||
* replaceInterpolatedVars("<模板字符串>",[变量值,变量值,...])
|
||||
* replaceInterpolatedVars("<模板字符串>",变量值,变量值,...])
|
||||
*
|
||||
- 当只有两个参数并且第2个参数是{}时,将第2个参数视为命名变量的字典
|
||||
replaceInterpolatedVars("this is {a}+{b},{a:1,b:2}) --> this is 1+2
|
||||
- 当只有两个参数并且第2个参数是[]时,将第2个参数视为位置参数
|
||||
replaceInterpolatedVars"this is {}+{}",[1,2]) --> this is 1+2
|
||||
- 普通位置参数替换
|
||||
replaceInterpolatedVars("this is {a}+{b}",1,2) --> this is 1+2
|
||||
-
|
||||
this == scope == { formatters: {}, ... }
|
||||
* @param {*} template
|
||||
* @returns
|
||||
*/
|
||||
function replaceInterpolatedVars(template,...args) {
|
||||
const scope = this;
|
||||
// 当前激活语言
|
||||
const activeLanguage = scope.global.activeLanguage;
|
||||
|
||||
// 没有变量插值则的返回原字符串
|
||||
if(args.length===0 || !hasInterpolation(template)) return template
|
||||
|
||||
// ****************************变量插值****************************
|
||||
if(args.length===1 && isPlainObject(args[0])){
|
||||
// 读取模板字符串中的插值变量列表
|
||||
// [[var1,[formatter,formatter,...],match],[var2,[formatter,formatter,...],match],...}
|
||||
let varValues = args[0];
|
||||
return forEachInterpolatedVars(template,(varname,formatters)=>{
|
||||
let value = (varname in varValues) ? varValues[varname] : '';
|
||||
return getFormattedValue(scope,activeLanguage,formatters,value)
|
||||
})
|
||||
}else {
|
||||
// ****************************位置插值****************************
|
||||
// 如果只有一个Array参数,则认为是位置变量列表,进行展开
|
||||
const params=(args.length===1 && Array.isArray(args[0])) ? [...args[0]] : args;
|
||||
if(params.length===0) return template // 没有变量则不需要进行插值处理,返回原字符串
|
||||
let i = 0;
|
||||
return forEachInterpolatedVars(template,(varname,formatters)=>{
|
||||
if(params.length>i){
|
||||
return getFormattedValue(scope,activeLanguage,formatters,params[i++])
|
||||
}else {
|
||||
throw new Error() // 抛出异常,停止插值处理
|
||||
}
|
||||
},{replaceAll:false})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 默认语言配置
|
||||
const defaultLanguageSettings = {
|
||||
defaultLanguage: "cn",
|
||||
activeLanguage: "cn",
|
||||
languages:[
|
||||
{name:"cn",title:"中文",default:true},
|
||||
{name:"en",title:"英文"}
|
||||
],
|
||||
formatters
|
||||
};
|
||||
|
||||
function isMessageId(content){
|
||||
return parseInt(content)>0
|
||||
}
|
||||
/**
|
||||
* 根据值的单数和复数形式,从messages中取得相应的消息
|
||||
*
|
||||
* @param {*} messages 复数形式的文本内容 = [<=0时的内容>,<=1时的内容>,<=2时的内容>,...]
|
||||
* @param {*} value
|
||||
*/
|
||||
function getPluraMessage(messages,value){
|
||||
try{
|
||||
if(Array.isArray(messages)){
|
||||
return messages.length > value ? messages[value] : messages[messages.length-1]
|
||||
}else {
|
||||
return messages
|
||||
}
|
||||
}catch{
|
||||
return Array.isArray(messages) ? messages[0] : messages
|
||||
}
|
||||
}
|
||||
function escape(str){
|
||||
return str.replaceAll(/\\(?![trnbvf'"]{1})/g,"\\\\")
|
||||
.replaceAll("\t","\\t")
|
||||
.replaceAll("\n","\\n")
|
||||
.replaceAll("\b","\\b")
|
||||
.replaceAll("\r","\\r")
|
||||
.replaceAll("\f","\\f")
|
||||
.replaceAll("\'","\\'")
|
||||
.replaceAll('\"','\\"')
|
||||
.replaceAll('\v','\\v')
|
||||
}
|
||||
function unescape(str){
|
||||
return str
|
||||
.replaceAll("\\t","\t")
|
||||
.replaceAll("\\n","\n")
|
||||
.replaceAll("\\b","\b")
|
||||
.replaceAll("\\r","\r")
|
||||
.replaceAll("\\f","\f")
|
||||
.replaceAll("\\'","\'")
|
||||
.replaceAll('\\"','\"')
|
||||
.replaceAll('\\v','\v')
|
||||
.replaceAll(/\\\\(?![trnbvf'"]{1})/g,"\\")
|
||||
}
|
||||
/**
|
||||
* 翻译函数
|
||||
*
|
||||
* translate("要翻译的文本内容") 如果默认语言是中文,则不会进行翻译直接返回
|
||||
* translate("I am {} {}","man") == I am man 位置插值
|
||||
* translate("I am {p}",{p:"man"}) 字典插值
|
||||
* translate("total {$count} items", {$count:1}) //复数形式
|
||||
* translate("total {} {} {} items",a,b,c) // 位置变量插值
|
||||
*
|
||||
* this===scope 当前绑定的scope
|
||||
*
|
||||
*/
|
||||
function translate(message) {
|
||||
const scope = this;
|
||||
const activeLanguage = scope.global.activeLanguage;
|
||||
let content = message;
|
||||
let vars=[]; // 插值变量列表
|
||||
let pluralVars= []; // 复数变量
|
||||
let pluraValue = null; // 复数值
|
||||
if(!typeof(message)==="string") return message
|
||||
try{
|
||||
// 1. 预处理变量: 复数变量保存至pluralVars中 , 变量如果是Function则调用
|
||||
if(arguments.length === 2 && isPlainObject(arguments[1])){
|
||||
Object.entries(arguments[1]).forEach(([name,value])=>{
|
||||
if(typeof(value)==="function"){
|
||||
try{
|
||||
vars[name] = value();
|
||||
}catch(e){
|
||||
vars[name] = value;
|
||||
}
|
||||
}
|
||||
// 以$开头的视为复数变量
|
||||
if(name.startsWith("$") && typeof(vars[name])==="number") pluralVars.push(name);
|
||||
});
|
||||
vars = [arguments[1]];
|
||||
}else if(arguments.length >= 2){
|
||||
vars = [...arguments].splice(1).map((arg,index)=>{
|
||||
try{
|
||||
arg = typeof(arg)==="function" ? arg() : arg;
|
||||
// 位置参数中以第一个数值变量为复数变量
|
||||
if(isNumber(arg)) pluraValue = parseInt(arg);
|
||||
}catch(e){ }
|
||||
return arg
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// 3. 取得翻译文本模板字符串
|
||||
if(activeLanguage === scope.defaultLanguage){
|
||||
// 2.1 从默认语言中取得翻译文本模板字符串
|
||||
// 如果当前语言就是默认语言,不需要查询加载,只需要做插值变换即可
|
||||
// 当源文件运用了babel插件后会将原始文本内容转换为msgId
|
||||
// 如果是msgId则从scope.default中读取,scope.default=默认语言包={<id>:<message>}
|
||||
if(isMessageId(content)){
|
||||
content = scope.default[content] || message;
|
||||
}
|
||||
}else {
|
||||
// 2.2 从当前语言包中取得翻译文本模板字符串
|
||||
// 如果没有启用babel插件将源文本转换为msgId,需要先将文本内容转换为msgId
|
||||
// JSON.stringify在进行转换时会将\t\n\r转换为\\t\\n\\r,这样在进行匹配时就出错
|
||||
let msgId = isMessageId(content) ? content : scope.idMap[escape(content)];
|
||||
content = scope.messages[msgId] || content;
|
||||
content = Array.isArray(content) ? content.map(v=>unescape(v)) : unescape(content);
|
||||
}
|
||||
// 2. 处理复数
|
||||
// 经过上面的处理,content可能是字符串或者数组
|
||||
// content = "原始文本内容" || 复数形式["原始文本内容","原始文本内容"....]
|
||||
// 如果是数组说明要启用复数机制,需要根据插值变量中的某个变量来判断复数形式
|
||||
if(Array.isArray(content) && content.length>0){
|
||||
// 如果存在复数命名变量,只取第一个复数变量
|
||||
if(pluraValue!==null){ // 启用的是位置插值,pluraIndex=第一个数字变量的位置
|
||||
content = getPluraMessage(content,pluraValue);
|
||||
}else if(pluralVar.length>0){
|
||||
content = getPluraMessage(content,parseInt(vars(pluralVar[0])));
|
||||
}else { // 如果找不到复数变量,则使用第一个内容
|
||||
content = content[0];
|
||||
}
|
||||
}
|
||||
|
||||
// 进行插值处理
|
||||
if(vars.length==0){
|
||||
return content
|
||||
}else {
|
||||
return replaceInterpolatedVars.call(scope,content,...vars)
|
||||
}
|
||||
}catch(e){
|
||||
return content // 出错则返回原始文本
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多语言管理类
|
||||
*
|
||||
* 当导入编译后的多语言文件时(import("./languages")),会自动生成全局实例VoerkaI18n
|
||||
*
|
||||
* VoerkaI18n.languages // 返回支持的语言列表
|
||||
* VoerkaI18n.defaultLanguage // 默认语言
|
||||
* VoerkaI18n.language // 当前语言
|
||||
* VoerkaI18n.change(language) // 切换到新的语言
|
||||
*
|
||||
*
|
||||
* VoerkaI18n.on("change",(language)=>{}) // 注册语言切换事件
|
||||
* VoerkaI18n.off("change",(language)=>{})
|
||||
*
|
||||
* */
|
||||
class I18nManager extends EventEmitter{
|
||||
constructor(settings={}){
|
||||
super();
|
||||
if(I18nManager.instance!=null){
|
||||
return I18nManager.instance;
|
||||
}
|
||||
I18nManager.instance = this;
|
||||
this._settings = deepMerge(defaultLanguageSettings,settings);
|
||||
this._scopes=[];
|
||||
return I18nManager.instance;
|
||||
}
|
||||
get settings(){ return this._settings }
|
||||
get scopes(){ return this._scopes }
|
||||
// 当前激活语言
|
||||
get activeLanguage(){ return this._settings.activeLanguage}
|
||||
// 默认语言
|
||||
get defaultLanguage(){ return this._settings.defaultLanguage}
|
||||
// 支持的语言列表
|
||||
get languages(){ return this._settings.languages}
|
||||
// 内置格式化器
|
||||
get formatters(){ return inlineFormatters }
|
||||
/**
|
||||
* 切换语言
|
||||
*/
|
||||
async change(value){
|
||||
value=value.trim();
|
||||
if(this.languages.findIndex(lang=>lang.name === value)!==-1){
|
||||
// 通知所有作用域刷新到对应的语言包
|
||||
await this._refreshScopes(value);
|
||||
this._settings.activeLanguage = value;
|
||||
/// 触发语言切换事件
|
||||
await this.emit(value);
|
||||
}else {
|
||||
throw new Error("Not supported language:"+value)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 当切换语言时调用此方法来加载更新语言包
|
||||
* @param {*} newLanguage
|
||||
*/
|
||||
async _refreshScopes(newLanguage){
|
||||
// 并发执行所有作用域语言包的加载
|
||||
try{
|
||||
const scopeRefreshers = this._scopes.map(scope=>{
|
||||
return scope.refresh(newLanguage)
|
||||
});
|
||||
if(Promise.allSettled){
|
||||
await Promise.allSettled(scopeRefreshers);
|
||||
}else {
|
||||
await Promise.all(scopeRefreshers);
|
||||
}
|
||||
}catch(e){
|
||||
console.warn("Error while refreshing i18n scopes:",e.message);
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* 注册一个新的作用域
|
||||
*
|
||||
* 每一个库均对应一个作用域,每个作用域可以有多个语言包,且对应一个翻译函数
|
||||
* 除了默认语言外,其他语言采用动态加载的方式
|
||||
*
|
||||
* @param {*} scope
|
||||
*/
|
||||
async register(scope){
|
||||
if(!(scope instanceof i18nScope)){
|
||||
throw new TypeError("Scope must be an instance of I18nScope")
|
||||
}
|
||||
this._scopes.push(scope);
|
||||
await scope.refresh(this.activeLanguage);
|
||||
}
|
||||
/**
|
||||
* 注册全局格式化器
|
||||
* 格式化器是一个简单的同步函数value=>{...},用来对输入进行格式化后返回结果
|
||||
*
|
||||
* registerFormatters(name,value=>{...}) // 适用于所有语言
|
||||
* registerFormatters(name,value=>{...},{langauge:"cn"}) // 适用于cn语言
|
||||
* registerFormatters(name,value=>{...},{langauge:"en"}) // 适用于en语言
|
||||
|
||||
* @param {*} formatters
|
||||
*/
|
||||
registerFormatter(name,formatter,{language="*"}={}){
|
||||
if(!typeof(formatter)==="function" || typeof(name)!=="string"){
|
||||
throw new TypeError("Formatter must be a function")
|
||||
}
|
||||
if(DataTypes$1.includes(name)){
|
||||
this.formatters[language].$types[name] = formatter;
|
||||
}else {
|
||||
this.formatters[language][name] = formatter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var runtime ={
|
||||
getInterpolatedVars,
|
||||
replaceInterpolatedVars,
|
||||
I18nManager,
|
||||
translate,
|
||||
languages,
|
||||
i18nScope,
|
||||
defaultLanguageSettings,
|
||||
getDataTypeName,
|
||||
isNumber,
|
||||
isPlainObject
|
||||
};
|
||||
|
||||
export { runtime as default };
|
@ -50,7 +50,7 @@ module.exports = {
|
||||
date: (value)=> value.toLocaleDateString(),
|
||||
dict, //字典格式化器
|
||||
},
|
||||
cn:{
|
||||
zh:{
|
||||
$types:{
|
||||
Date:(value)=> `${value.getFullYear()}年${value.getMonth()+1}月${value.getDate()}日 ${value.getHours()}点${value.getMinutes()}分${value.getSeconds()}秒`
|
||||
},
|
||||
|
@ -11,10 +11,6 @@ let inlineFormatters = require("./formatters") // 内置格式化器
|
||||
// 支持参数: { var | formatter(x,x,..) | formatter }
|
||||
let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*(\(.*\)){0,1}\s*)*)\s*\}/g
|
||||
|
||||
// 有效的语言名称列表
|
||||
const languages = ["af","am","ar-dz","ar-iq","ar-kw","ar-ly","ar-ma","ar-sa","ar-tn","ar","az","be","bg","bi","bm","bn","bo","br","bs","ca","cs","cv","cy","da","de-at","de-ch","de","dv","el","en-au","en-ca","en-gb","en-ie","en-il","en-in","en-nz","en-sg","en-tt","en","eo","es-do","es-mx","es-pr","es-us","es","et","eu","fa","fi","fo","fr-ca","fr-ch","fr","fy","ga","gd","gl","gom-latn","gu","he","hi","hr","ht","hu","hy-am","id","is","it-ch","it","ja","jv","ka","kk","km","kn","ko","ku","ky","lb","lo","lt","lv","me","mi","mk","ml","mn","mr","ms-my","ms","mt","my","nb","ne","nl-be","nl","nn","oc-lnc","pa-in","pl","pt-br","pt","ro","ru","rw","sd","se","si","sk","sl","sq","sr-cyrl","sr","ss","sv-fi","sv","sw","ta","te","tet","tg","th","tk","tl-ph","tlh","tr","tzl","tzm-latn","tzm","ug-cn","uk","ur","uz-latn","uz","vi","x-pseudo","yo","zh-cn","zh-hk","zh-tw","zh"]
|
||||
|
||||
|
||||
// 插值变量字符串替换正则
|
||||
|
||||
//let varReplaceRegexp =String.raw`\{\s*(?<var>{name}\.?\w*)\s*\}`
|
||||
@ -199,7 +195,7 @@ function resetScopeCache(scope,activeLanguage=null){
|
||||
"*":{
|
||||
$types:{...} // 在所有语言下只作用于特定数据类型的格式化器
|
||||
}, // 在所有语言下生效的格式化器
|
||||
cn:{
|
||||
zh:{
|
||||
$types:{
|
||||
[数据类型]:(value)=>{...},
|
||||
},
|
||||
@ -402,13 +398,13 @@ function replaceInterpolatedVars(template,...args) {
|
||||
|
||||
// 默认语言配置
|
||||
const defaultLanguageSettings = {
|
||||
defaultLanguage: "cn",
|
||||
activeLanguage: "cn",
|
||||
defaultLanguage: "zh",
|
||||
activeLanguage: "zh",
|
||||
languages:[
|
||||
{name:"cn",title:"中文",default:true},
|
||||
{name:"zh",title:"中文",default:true},
|
||||
{name:"en",title:"英文"}
|
||||
],
|
||||
formatters
|
||||
formatters:inlineFormatters
|
||||
}
|
||||
|
||||
function isMessageId(content){
|
||||
@ -638,7 +634,7 @@ function translate(message) {
|
||||
* 格式化器是一个简单的同步函数value=>{...},用来对输入进行格式化后返回结果
|
||||
*
|
||||
* registerFormatters(name,value=>{...}) // 适用于所有语言
|
||||
* registerFormatters(name,value=>{...},{langauge:"cn"}) // 适用于cn语言
|
||||
* registerFormatters(name,value=>{...},{langauge:"zh"}) // 适用于cn语言
|
||||
* registerFormatters(name,value=>{...},{langauge:"en"}) // 适用于en语言
|
||||
|
||||
* @param {*} formatters
|
||||
@ -659,8 +655,7 @@ module.exports ={
|
||||
getInterpolatedVars,
|
||||
replaceInterpolatedVars,
|
||||
I18nManager,
|
||||
translate,
|
||||
languages,
|
||||
translate
|
||||
i18nScope,
|
||||
defaultLanguageSettings,
|
||||
getDataTypeName,
|
||||
|
@ -1,37 +1,39 @@
|
||||
{
|
||||
"name": "@voerkai18n/runtime",
|
||||
"version": "1.0.9",
|
||||
"description": "Voerkai18n Runtime",
|
||||
"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",
|
||||
"release": "npm version patch && pnpm publish --no-git-checks --access public"
|
||||
},
|
||||
"exports": {
|
||||
"import": "./dist/index.esm.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"author": "wxzhang",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.17.6",
|
||||
"@babel/core": "^7.17.5",
|
||||
"@babel/plugin-transform-runtime": "^7.17.0",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/runtime": "^7.17.8",
|
||||
"@rollup/plugin-babel": "^5.3.1",
|
||||
"@rollup/plugin-commonjs": "^21.0.2",
|
||||
"@rollup/plugin-node-resolve": "^13.1.3",
|
||||
"deepmerge": "^4.2.2",
|
||||
"rollup": "^2.69.0",
|
||||
"rollup-plugin-clear": "^2.0.7",
|
||||
"rollup-plugin-terser": "^7.0.2"
|
||||
}
|
||||
}
|
||||
"name": "@voerkai18n/runtime",
|
||||
"version": "1.0.16",
|
||||
"description": "Voerkai18n Runtime",
|
||||
"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",
|
||||
"release": "pnpm autopublish"
|
||||
},
|
||||
"exports": {
|
||||
"import": "./dist/index.esm.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"author": "wxzhang",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.17.6",
|
||||
"@babel/core": "^7.17.5",
|
||||
"@babel/plugin-transform-runtime": "^7.17.0",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/runtime": "^7.17.8",
|
||||
"@rollup/plugin-babel": "^5.3.1",
|
||||
"@rollup/plugin-commonjs": "^21.0.2",
|
||||
"@rollup/plugin-node-resolve": "^13.1.3",
|
||||
"deepmerge": "^4.2.2",
|
||||
"rollup": "^2.69.0",
|
||||
"rollup-plugin-clear": "^2.0.7",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"@voerkai18n/autopublish": "workspace:^1.0.2"
|
||||
},
|
||||
"lastPublish": "2022-04-07T19:14:01+08:00"
|
||||
}
|
@ -4,7 +4,7 @@ module.exports = class i18nScope {
|
||||
// 每个作用域都有一个唯一的id
|
||||
this._id = options.id || (new Date().getTime().toString()+parseInt(Math.random()*1000))
|
||||
this._languages = options.languages // 当前作用域的语言列表
|
||||
this._defaultLanguage = options.defaultLanguage || "cn" // 默认语言名称
|
||||
this._defaultLanguage = options.defaultLanguage || "zh" // 默认语言名称
|
||||
this._activeLanguage = options.activeLanguage // 当前语言名称
|
||||
this._default = options.default // 默认语言包
|
||||
this._messages = options.messages // 当前语言包
|
||||
|
@ -121,9 +121,7 @@ function getProjectRootFolder(folder="./",exclueCurrent=false){
|
||||
|
||||
/**
|
||||
* 自动获取当前项目的languages
|
||||
*
|
||||
* 1.
|
||||
*
|
||||
*
|
||||
* @param {*} location
|
||||
*/
|
||||
function getProjectLanguageFolder(location="./"){
|
||||
@ -188,7 +186,7 @@ function getProjectRootFolder(folder="./",exclueCurrent=false){
|
||||
* @returns
|
||||
*/
|
||||
function getCurrentPackageJson(folder,exclueCurrent=true){
|
||||
let projectFolder = getCurrentProjectRootFolder(folder,exclueCurrent)
|
||||
let projectFolder = getProjectRootFolder(folder,exclueCurrent)
|
||||
if(projectFolder){
|
||||
return fs.readJSONSync(path.join(projectFolder,"package.json"))
|
||||
}
|
||||
@ -231,6 +229,28 @@ function isInstallDependent(url){
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前包的版本号
|
||||
*/
|
||||
function getInstalledPackages(){
|
||||
const packages = {
|
||||
"@voerkai18n/runtime":"未安装",
|
||||
"@voerkai18n/babel":"未安装",
|
||||
"@voerkai18n/vue":"未安装",
|
||||
"@voerkai18n/react":"未安装",
|
||||
"@voerkai18n/vite":"未安装",
|
||||
"@voerkai18n/formatters":"未安装"
|
||||
}
|
||||
for(let package of Object.keys(packages)){
|
||||
try{
|
||||
require(package)
|
||||
}catch{
|
||||
packages[package] = "已安装"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否是JSON对象
|
||||
* @param {*} obj
|
||||
@ -333,6 +353,21 @@ function deepMerge(toObj,formObj,options={}){
|
||||
shelljs.exec("npm install @voerkai18n/runtime")
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 在当前工程升级@voerkai18n/runtime
|
||||
* @param {*} srcPath
|
||||
* @param {*} opts
|
||||
*/
|
||||
function updateVoerkai18nRuntime(srcPath){
|
||||
const projectFolder = getCurrentProjectRootFolder(srcPath || process.cwd())
|
||||
if(fs.existsSync("pnpm-lock.yaml")){
|
||||
shelljs.exec("pnpm update --latest @voerkai18n/runtime")
|
||||
}else if(fs.existsSync("yarn.lock")){
|
||||
shelljs.exec("yarn upgrade @voerkai18n/runtime")
|
||||
}else{
|
||||
shelljs.exec("npm update --save @voerkai18n/runtime")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -368,6 +403,7 @@ module.exports = {
|
||||
findModuleType, // 获取当前项目的模块类型
|
||||
isInstallDependent, // 判断是否已经安装了依赖
|
||||
installVoerkai18nRuntime, // 在当前工程自动安装@voerkai18n/runtime
|
||||
updateVoerkai18nRuntime, // 在当前工程升级@voerkai18n/runtime
|
||||
isPlainObject, // 判断是否是普通对象
|
||||
isNumber, // 判断是否是数字
|
||||
deepMerge, // 深度合并对象
|
||||
|
@ -1,16 +1,20 @@
|
||||
{
|
||||
"name": "@voerkai18n/utils",
|
||||
"version": "1.0.2",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"release": "npm version patch && pnpm publish --no-git-checks --access public"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs-extra": "^10.0.1",
|
||||
"shelljs": "^0.8.5"
|
||||
}
|
||||
}
|
||||
"name": "@voerkai18n/utils",
|
||||
"version": "1.0.10",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"release": "pnpm autopublish"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs-extra": "^10.0.1",
|
||||
"shelljs": "^0.8.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@voerkai18n/autopublish": "workspace:^1.0.2"
|
||||
},
|
||||
"lastPublish": "2022-04-10T17:21:16+08:00"
|
||||
}
|
@ -1,15 +1,19 @@
|
||||
{
|
||||
"name": "@voerkai18n/vite",
|
||||
"version": "1.0.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"release": "npm version patch && pnpm publish --no-git-checks --access public"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@voerkai18n/utils": "workspace:^1.0.0"
|
||||
}
|
||||
}
|
||||
"name": "@voerkai18n/vite",
|
||||
"version": "1.0.7",
|
||||
"description": "VoerkaI18n plugin for Vite",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"release": "pnpm autopublish"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@voerkai18n/utils": "workspace:^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@voerkai18n/autopublish": "workspace:^1.0.2"
|
||||
},
|
||||
"lastPublish": "2022-04-10T17:21:48+08:00"
|
||||
}
|
@ -1,26 +1,16 @@
|
||||
{
|
||||
"name": "@voerkai18n/vue",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.4",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"release": "npm version patch && pnpm publish --no-git-checks --access public"
|
||||
},
|
||||
"exports": {
|
||||
".": "./index.js",
|
||||
"./vite-plugin-voerkai18n": "./vite-plugin-voerkai18n.js"
|
||||
"release": "pnpm autopublish"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"deepmerge": "^4.2.2",
|
||||
"gulp": "^4.0.2",
|
||||
"vinyl": "^2.2.1"
|
||||
"@voerkai18n/autopublish": "workspace:^1.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"minimatch": "^5.0.1",
|
||||
"vite": "^2.8.6",
|
||||
"vite-plugin-inspect": "^0.4.3"
|
||||
}
|
||||
}
|
||||
"lastPublish": "2022-04-06T20:55:01+08:00"
|
||||
}
|
1678
pnpm-lock.yaml
generated
1678
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
160
readme.md
160
readme.md
@ -1,6 +1,3 @@
|
||||
|
||||
# ** 测试阶段,有问题请issues **
|
||||
|
||||
[](https://gitee.com/zhangfisher/voerka-i18n/stargazers)
|
||||
|
||||
# 前言
|
||||
@ -17,15 +14,17 @@
|
||||
|
||||
|
||||
|
||||
基于此就开始造出`VoerkaI18n`这个全新的国际化多语言解决方案,主要特性包括:
|
||||
基于此就开始造出`VoerkaI18n`这个**全新的国际化多语言解决方案**,主要特性包括:
|
||||
|
||||
- 简单易用
|
||||
|
||||
|
||||
- 全面工程化解决方案,提供初始化、提取文本、自动翻译、编译等工具链支持。
|
||||
|
||||
- 符合直觉,不需要手动定义文本`Key`映射。
|
||||
|
||||
- 完整的自动化工具链支持,包括项目初始化、提取文本、编译语言等。
|
||||
- 强大的插值变量`格式化器`机制,可以扩展出强大的多语言特性。
|
||||
|
||||
- 支持`babel`插件自动导入t翻译函数。
|
||||
- 支持`babel`插件自动导入`t`翻译函数。
|
||||
|
||||
- 支持`nodejs`、浏览器(`vue`/`react`)前端环境。
|
||||
|
||||
@ -33,12 +32,12 @@
|
||||
|
||||
- 高度可扩展的`复数`、`货币`、`数字`等常用的多语言处理机制。
|
||||
|
||||
- 通过`格式化器`可以扩展出强大的多语言特性。
|
||||
|
||||
- 翻译过程内,提取文本可以自动进行同步,并保留已翻译的内容。
|
||||
|
||||
- 可以随时添加支持的语言
|
||||
|
||||
- 支持调用在线自动翻译对提取文本进行翻译。
|
||||
|
||||
|
||||
|
||||
# 安装
|
||||
@ -126,16 +125,16 @@ console.log(t("中华人民共和国成立于{}",1949))
|
||||
{
|
||||
"languages": [
|
||||
{
|
||||
"name": "cn",
|
||||
"title": "cn"
|
||||
"name": "zh",
|
||||
"title": "zh"
|
||||
},
|
||||
{
|
||||
"name": "en",
|
||||
"title": "en"
|
||||
}
|
||||
],
|
||||
"defaultLanguage": "cn",
|
||||
"activeLanguage": "cn",
|
||||
"defaultLanguage": "zh",
|
||||
"activeLanguage": "zh",
|
||||
"namespaces": {}
|
||||
}
|
||||
```
|
||||
@ -181,13 +180,13 @@ myapp
|
||||
**如果略过第一步中的`voerkai18n init`,也可以使用以下命令来为创建和更新`settinbgs.json`**
|
||||
|
||||
```javascript
|
||||
myapp>voerkai18n extract -D -lngs cn en de jp -d cn -a cn
|
||||
myapp>voerkai18n extract -D -lngs zh en de jp -d zh -a zh
|
||||
```
|
||||
|
||||
以上命令代表:
|
||||
|
||||
- 扫描当前文件夹下所有源码文件,默认是`js`、`jsx`、`html`、`vue`文件类型。
|
||||
- 计划支持`cn`、`en`、`de`、`jp`四种语言
|
||||
- 计划支持`zh`、`en`、`de`、`jp`四种语言
|
||||
- 默认语言是中文。(指在源码文件中我们直接使用中文即可)
|
||||
- 激活语言是中文(即默认切换到中文)
|
||||
- `-D`代表显示扫描调试信息
|
||||
@ -223,6 +222,14 @@ myapp>voerkai18n extract -D -lngs cn en de jp -d cn -a cn
|
||||
|
||||
因此,反复执行`voerkai18n extract`命令是安全的,不会导致进行了一半的翻译内容丢失,可以放心执行。
|
||||
|
||||
大部分国际化解决方案至此就需要交给人工进行翻译了,但是`voerkai18n`除了手动翻译外,通过`voerkai18n translate`命令来实现**调用在线翻译服务**进行自动翻译。
|
||||
|
||||
```javascript
|
||||
>voerkai18n translate --provider baidu --appkey <在百度翻译上申请的密钥> --appid <在百度翻译上申请的appid>
|
||||
```
|
||||
|
||||
在项目文件夹下执行上面的语句,将会自动调用百度的在线翻译API进行翻译,以现在的翻译水平而言,您只需要进行少量的微调即可。关于`voerkai18n translate`命令的使用请查阅后续介绍。
|
||||
|
||||
## 第五步:编译语言包
|
||||
|
||||
当我们完成`myapp/languages/translates`下的所有`JSON语言文件`的翻译后(如果配置了名称空间后,每一个名称空间会对应生成一个文件,详见后续`名称空间`介绍),接下来需要对翻译后的文件进行编译。
|
||||
@ -239,7 +246,7 @@ myapp> voerkai18n compile
|
||||
|-- idMap.js // 文本信息id映射表
|
||||
|-- runtime.js // 运行时源码
|
||||
|-- index.js // 包含该应用作用域下的翻译函数等
|
||||
|-- cn.js // 语言包
|
||||
|-- zh.js // 语言包
|
||||
|-- en.js
|
||||
|-- jp.js
|
||||
|-- de.js
|
||||
@ -258,7 +265,9 @@ myapp> voerkai18n compile
|
||||
import { t } from "./languages"
|
||||
```
|
||||
|
||||
因此,我们需要在需要进行翻译时导入该函数即可。但是如果源码文件很多,重次重复导入`t`函数也是比较麻烦的,所以我们也提供了一个`babel插件`来自动导入`t`函数。
|
||||
因此,我们需要在需要进行翻译时导入该函数即可。
|
||||
|
||||
但是如果源码文件很多,重次重复导入`t`函数也是比较麻烦的,所以我们也提供了一个`babel/vite`等插件来自动导入`t`函数。
|
||||
|
||||
## 第六步:切换语言
|
||||
|
||||
@ -362,8 +371,6 @@ t("我姓名叫{name},我今年{age}岁","tom",()=>12)
|
||||
|
||||
`voerka-i18n`支持强大的插值变量格式化机制,可以在插值变量中使用`{变量名称 | 格式化器名称 | 格式化器名称(...参数) | ... }`类似管道操作符的语法,将上一个输出作为下一个输入,从而实现对变量值的转换。此机制是`voerka-i18n`实现复数、货币、数字等多语言支持的基础。
|
||||
|
||||
### **格式化器语法**
|
||||
|
||||
我们假设定义以下格式化器(如果定义格式化器,详见后续)来进行示例。
|
||||
|
||||
- **UpperCase**:将字符转换为大写
|
||||
@ -408,8 +415,6 @@ t("My name is { name | UpperCase | mr }",{name:"tom"})
|
||||
|
||||
`{data | f1 | f2 | f3(1)}`等效于` f3(f2(f1(data)),1)`
|
||||
|
||||
|
||||
|
||||
## 日期时间
|
||||
|
||||
`@voerkai18n/runtime`内置了对日期时间进行处理的格式化器,可以直接使用,不需要额外的安装。
|
||||
@ -519,7 +524,7 @@ t("{name}有{$count}辆车",{name:"张三",$count:1})
|
||||
"Chapter Five","Chapter Six","Chapter Seven","Chapter Eight","Chapter Nine",
|
||||
"Chapter {}"
|
||||
],
|
||||
cn:["起始","第一章", "第二章", "第三章","第四章","第五章","第六章","第七章","第八章","第九章",“第{}章”]
|
||||
zh:["起始","第一章", "第二章", "第三章","第四章","第五章","第六章","第七章","第八章","第九章",“第{}章”]
|
||||
}
|
||||
}
|
||||
// 翻译函数
|
||||
@ -827,7 +832,7 @@ module.exports = {
|
||||
// [数据类型名称]:(value)=>{...},
|
||||
// [数据类型名称]:(value)=>{...},
|
||||
},
|
||||
cn:{
|
||||
zh:{
|
||||
$types:{
|
||||
// 所有类型的默认格式化器
|
||||
"*":{
|
||||
@ -890,7 +895,7 @@ t("灯状态:{status}",false) // === 灯状态:OFF
|
||||
```javascript
|
||||
//formatters.js
|
||||
module.exports = {
|
||||
cn:{
|
||||
zh:{
|
||||
$types:{
|
||||
Boolean:(value)=> value ? "开" : "关"
|
||||
}
|
||||
@ -918,7 +923,7 @@ t("灯状态:{status}",false) // === 灯状态:OFF
|
||||
"*":{
|
||||
$types:{...}
|
||||
},
|
||||
cn:{
|
||||
zh:{
|
||||
$types:{...}
|
||||
},
|
||||
en:{
|
||||
@ -947,7 +952,7 @@ module.exports = {
|
||||
$types:{...},
|
||||
[格式化名称]:(value)=>{.....},
|
||||
},
|
||||
cn:{
|
||||
zh:{
|
||||
$types:{...},
|
||||
[格式化名称]:(value)=>{.....},
|
||||
},
|
||||
@ -966,7 +971,7 @@ module.exports = {
|
||||
"*":{
|
||||
uppercase:(value)=>value
|
||||
},
|
||||
cn:{
|
||||
zh:{
|
||||
uppercase:(value)=>["一","二","三","四","五","六","七","八","九","十"][value-1]
|
||||
},
|
||||
en:{
|
||||
@ -1015,7 +1020,7 @@ t("{value | uppercase}",3) // == 3
|
||||
当使用`webpack`、`rollup`、`esbuild`进行项目打包时,默认语言包采用静态加载,会被打包进行源码中,而其他语言则采用异步打包方式。在`languages/index.js`中。
|
||||
|
||||
```javascript
|
||||
const defaultMessages = require("./cn.js")
|
||||
const defaultMessages = require("./zh.js")
|
||||
const activeMessages = defaultMessages
|
||||
|
||||
// 语言作用域
|
||||
@ -1031,6 +1036,18 @@ const scope = new i18nScope({
|
||||
})
|
||||
```
|
||||
|
||||
## 自动翻译
|
||||
|
||||
内置的`voerkai18n translate`命令能调用在线翻译服务完成对提取的文本的自动翻译。
|
||||
|
||||
目前支持访问百度在线API进行自动翻译。百度提供了免费的在线API,虽然只支持`QPS=1`,即每秒调用一次。但是`voerkai18n translate`命令会对要翻译的文本进行合并后再调用,因此大部分情况下,均足够使用了。
|
||||
|
||||
执行`voerkai18n translate`命令后,将大大提高您国际化的效率。
|
||||
|
||||
## 语言代码
|
||||
|
||||
请参阅https://fanyi-api.baidu.com/doc/21。
|
||||
|
||||
# 扩展工具
|
||||
|
||||
## babel插件
|
||||
@ -1191,7 +1208,6 @@ app.use(i18nPlugin,{
|
||||
```
|
||||
|
||||
- 当`forceUpdate=true`时,`@voerkai18n/vue`插件在切换语言时会调用`app._instance.update()`对整个应用进行强制重新渲染。大部分情况下,切换语言时强制对整个应用进行重新渲染的行为是符合预期的。您也可以能够通过设`forceUpdate=false`来禁用强制重新渲染,此时,界面就不会马上看到语言的切换,需要您自己控制进行重新渲染。
|
||||
-
|
||||
|
||||
## Vite插件
|
||||
|
||||
@ -1226,8 +1242,7 @@ export default defineConfig({
|
||||
|
||||
```
|
||||
|
||||
- ` vite-plugin-inspect`是开发`vite`插件时的调试插件,启用后就可以通过`localhost:3000/__inspect/ `查看Vue源码文件经过插件处理前后的内容,一般是Vite插件开发者使用。上例中安装后,就可以查看`Voerkai18nPlugin`对Vue文件干了什么事,可以加深理解,**正常使用不需要安装**。
|
||||
- `vite`插件
|
||||
- ` vite-plugin-inspect`是开发`vite`插件时的调试插件,启用后就可以通过`localhost:3000/__inspect/ `查看Vue源码文件经过插件处理前后的内容,一般是Vite插件开发者使用。上例中安装后,就可以查看`Voerkai18nPlugin`对`Vue`文件干了什么事,可以加深理解,**正常使用不需要安装**。
|
||||
|
||||
### 插件功能
|
||||
|
||||
@ -1284,10 +1299,10 @@ export default defineConfig({
|
||||
|
||||
- `正则表达式`比较容易理解,匹配上的就进行处理。
|
||||
- `正则表达式字符串`支持一些简单的语法扩展,包括:
|
||||
- 可以通过前置`!`符号来进行排除匹配。
|
||||
- 将`**`替换为`.*`,允许使用类似`"/code/apps/test/**/node_modules/**"`的形式来匹配连续路径。
|
||||
- 将`?`替换为`[^\/]?`
|
||||
- 将`*`替换为`[^\/]*`
|
||||
- `!`符号:添加在字符串前面来进行排除匹配。
|
||||
- `**`:将`**`替换为`.*`,允许使用类似`"/code/apps/test/**/node_modules/**"`的形式来匹配连续路径。
|
||||
- `?`:将`?`替换为`[^\/]?`,用来匹配单个字符
|
||||
- `*`:将`*`替换为`[^\/]*`,匹配路径名称
|
||||
|
||||
## React扩展
|
||||
|
||||
@ -1335,7 +1350,7 @@ Arguments:
|
||||
Options:
|
||||
-D, --debug 输出调试信息
|
||||
-r, --reset 重新生成当前项目的语言配置
|
||||
-lngs, --languages <languages...> 支持的语言列表 (default: ["cn","en"])
|
||||
-lngs, --languages <languages...> 支持的语言列表 (default: ["zh","en"])
|
||||
-d, --defaultLanguage 默认语言
|
||||
-a, --activeLanguage 激活语言
|
||||
-h, --help display help for command
|
||||
@ -1347,7 +1362,7 @@ Options:
|
||||
|
||||
```javascript
|
||||
//- `lngs`参数用来指定拟支持的语言名称列表
|
||||
> voerkai18n init . -lngs cn en jp de -d cn
|
||||
> voerkai18n init . -lngs zh en jp de -d zh
|
||||
```
|
||||
|
||||
运行`voerkai18n init`命令后,会在当前工程中创建相应配置文件。
|
||||
@ -1367,7 +1382,7 @@ module.exports = {
|
||||
// 拟支持的语言列表
|
||||
"languages": [
|
||||
{
|
||||
"name": "cn",
|
||||
"name": "zh",
|
||||
"title": "中文"
|
||||
},
|
||||
{
|
||||
@ -1376,9 +1391,9 @@ module.exports = {
|
||||
}
|
||||
],
|
||||
// 默认语言,即准备在源码中写的语言,一般我们可以直接使用中文
|
||||
"defaultLanguage": "cn",
|
||||
"defaultLanguage": "zh",
|
||||
// 激活语言,即默认要启用的语言,一般等于defaultLanguage
|
||||
"activeLanguage": "cn",
|
||||
"activeLanguage": "zh",
|
||||
// 翻译名称空间定义,详见后续介绍。
|
||||
"namespaces": {}
|
||||
}
|
||||
@ -1442,6 +1457,61 @@ Options:
|
||||
>
|
||||
>如果想添加新的语言支持,也`voerkai18n extract`也可以如预期的正常工作。
|
||||
|
||||
## translate
|
||||
|
||||
在工程文件夹下执行`voerkai18n translate`命令,该命令会读取`languages/settings.json`配置文件,并调用在线翻译服务(如百度在线翻译)对提取的文本(`languages/translates/*.json`)进行自动翻译。
|
||||
|
||||
```shell
|
||||
Usage: voerkai18n translate [options] [location]
|
||||
|
||||
调用在线翻译服务商的API翻译译指定项目的语言包,如使用百度云翻译服务
|
||||
|
||||
Arguments:
|
||||
location 工程项目所在目录
|
||||
|
||||
Options:
|
||||
-p, --provider <value> 在线翻译服务提供者名称或翻译脚本文件 (default: "baidu")
|
||||
-m, --max-package-size <value> 将多个文本合并提交的最大包字节数 (default: 3000)
|
||||
--appkey [key] API密钥
|
||||
--appid [id] API ID
|
||||
--no-backup 备份原始文件
|
||||
--mode 翻译模式,取值auto=仅翻译未翻译的,full=全部翻译
|
||||
-q, --qps <value> 翻译速度限制,即每秒可调用的API次数 (default: 1)
|
||||
-h, --help 显示帮助
|
||||
```
|
||||
|
||||
- 内置支持调用百度的在线翻译服务,您需要百度的网站上(http://api.fanyi.baidu.com/)申请开通服务,开通后可以得到`appid`和`appkey`(密钥)。
|
||||
|
||||
- `--provider`用来指定在线翻译服务提供者,内置支持的是百度在线翻译。也可以传入一个js脚本,如下:
|
||||
|
||||
```javascript
|
||||
// youdao.js
|
||||
module.exports = async function(options){
|
||||
let { appkey,appid } = options
|
||||
return {
|
||||
translate:async (texts,from,to){
|
||||
// texts是一个Array
|
||||
// from,to代表要从哪一种语言翻译到何种语言
|
||||
.....
|
||||
// 在此对texts内容调用在线翻译API
|
||||
// 翻译结果应该返回与texts对应的数组
|
||||
// 如果出错则应该throw new Error()
|
||||
return [...]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `qps`用来指定调用在线翻译API的速度,默认是1,代表每秒调用一次;此参数的引入是考虑到有些翻译平台的免费API有QPS限制。比如百度在线翻译免费版本限制`QPS`就是1,即每秒只能调用一次。如果您购买了服务,则可以将`QPS`调高。
|
||||
|
||||
- 默认情况下,每次运行时均会备份原始的翻译文件至`languages/translates/backup`,`--no-backup`可以禁止备份。
|
||||
|
||||
- 默认情况下,`voerkai18n translate`会在每次运行时跳过已经翻译过的内容,这样可以保留翻译成果。此特性在您对自动翻译的内容进行修改后,再多次运行`voerkai18n translate`命令时均能保留翻译内容,不会导致您修改调整过的内容丢失。`--mode full`参数可以完全覆盖翻译,请慎用。
|
||||
|
||||
- 为了提高在线翻译的速度,`voerkai18n translate`并不是一条文本调用一次API,而是将多条文本合并起来进行调用,但是单次调用也是有数据包大小的限制的,`--max-package-size`参数用来指定数据包的最大值。比如百度建议,为保证翻译质量,请将单次请求长度控制在 6000 bytes以内(汉字约为输入参数 2000 个)。
|
||||
|
||||
- 需要注意的是,自动翻译虽然准确性还不错,真实场景还是需要进行手工调整的,特别是自动翻译一般不能识别插值变量。
|
||||
|
||||
## compile
|
||||
|
||||
编译当前工程的语言包,编译结果输出在.`/langauges`文件夹。
|
||||
@ -1457,6 +1527,7 @@ Arguments:
|
||||
Options:
|
||||
-D, --debug 输出调试信息
|
||||
-m, --moduleType [types] 输出模块类型,取值auto,esm,cjs (default: "esm")
|
||||
--no-inline-runtime 不嵌入运行时源码
|
||||
-h, --help display help for command
|
||||
```
|
||||
|
||||
@ -1468,7 +1539,7 @@ myapp
|
||||
|-- index.js // 当前作用域的源码
|
||||
|-- idMap.js // 翻译文本与id的映射文件
|
||||
|-- formatters.js // 自定义格式化器
|
||||
|-- cn.js // 中文语言包
|
||||
|-- zh.js // 中文语言包
|
||||
|-- en.js // 英文语言包
|
||||
|-- xx.js // 其他语言包
|
||||
|-- ...
|
||||
@ -1478,7 +1549,8 @@ myapp
|
||||
|
||||
- 在当前工程目录下,一般不需要指定参数就可以反复多次进行编译。
|
||||
- 您每次修改了源码并`extract`后,均应该再次运行`compile`命令。
|
||||
- 如果您修改了`formatters.js`,执行compile命令不会修改该文件。
|
||||
- 如果您修改了`formatters.js`,执行`compile`命令不会重新生成和修改该文件。
|
||||
- `--no-inline-runtime `参数用来指示如何引用运行时。默认会将运行时代码生成保存在`languages/runtime.js`,应用以源码形式引用。当启用`--no-inline-runtime `参数时会采用`require("@voerkai18n/runtime")`的方式。
|
||||
|
||||
# API
|
||||
|
||||
@ -1496,12 +1568,12 @@ i18nScope.off(callback)
|
||||
// 当前作用域配置
|
||||
i18nScope.settings
|
||||
// 当前语言
|
||||
i18nScope.activeLanguage // 如cn
|
||||
i18nScope.activeLanguage // 如zh
|
||||
|
||||
// 默认语言
|
||||
i18nScope.defaultLanguage
|
||||
// 返回当前支持的语言列表,可以用来显示
|
||||
i18nScope.languages // [{name:"cn",title:"中文"},{name:"en",title:"英文"},...]
|
||||
i18nScope.languages // [{name:"zh",title:"中文"},{name:"en",title:"英文"},...]
|
||||
// 返回当前作用域的格式化器
|
||||
i18nScope.formatters
|
||||
// 当前作用id
|
||||
|
145
test/app.test.js
Normal file
145
test/app.test.js
Normal file
@ -0,0 +1,145 @@
|
||||
/**
|
||||
* 测试demp app的语言运行环境
|
||||
*
|
||||
* > pnpm test:app
|
||||
*
|
||||
* 执行本测试用例时需要确保packages/apps/test文件夹没有被占用
|
||||
*
|
||||
*/
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs-extra");
|
||||
const shelljs = require("shelljs");
|
||||
const os = require("os")
|
||||
|
||||
|
||||
|
||||
// 演示的项目名称
|
||||
|
||||
|
||||
const APP_FOLDER = path.join(__dirname, "../packages/apps/test");
|
||||
const LANGUAGE_FOLDER = path.join(APP_FOLDER, "languages");
|
||||
const TRANSLATES_FOLDER = path.join(LANGUAGE_FOLDER, "translates");
|
||||
|
||||
let SUPPORTED_LANGUAGES = ["zh", "en","de","jp","fr"];
|
||||
let DEFAULT_LANGUAGE = "zh"
|
||||
let ACTIVE_LANGUAGE = "zh"
|
||||
|
||||
const CN_TEXTS = ["一","二","三","四","五"];
|
||||
const EN_TEXTS = ["One","Two","Three","Four","Five"];
|
||||
|
||||
function createTestApp(){
|
||||
if(fs.existsSync(APP_FOLDER)) resetTestApp()
|
||||
fs.mkdirSync(APP_FOLDER);
|
||||
// 创建package.json
|
||||
const pkgFile = path.join(APP_FOLDER, "package.json");
|
||||
fs.writeFileSync(pkgFile, JSON.stringify({
|
||||
name: "@voerkai18n/testapp",
|
||||
main:"./index.js",
|
||||
scripts: {
|
||||
"release": "pnpm autopublish"
|
||||
}
|
||||
},null,4))
|
||||
// 创建index.js
|
||||
const indexFile = path.join(APP_FOLDER, "index.js");
|
||||
fs.writeFileSync(indexFile, `
|
||||
const { t,i18nScope } = require("./languages/index.js");
|
||||
let cn_messages, en_messages
|
||||
async function output(){
|
||||
cn_messages = t("一")+t("二")+t("三")+t("四")+t("五")
|
||||
console.log(cn_messages)
|
||||
await i18nScope.change("en")
|
||||
en_messages = t("一")+t("二")+t("三")+t("四")+t("五")
|
||||
console.log(en_messages)
|
||||
}
|
||||
output().then(()=>{})
|
||||
module.exports = {
|
||||
change:async (lang)=> await i18nScope.change(lang),
|
||||
getMessages:()=>{
|
||||
return t("一")+t("二")+t("三")+t("四")+t("五")
|
||||
}
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
||||
function resetTestApp(){
|
||||
fs.removeSync(APP_FOLDER);
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
shelljs.exec("pnpm update -g @voerkai18n/utils")
|
||||
shelljs.exec("pnpm update -g @voerkai18n/runtime ")
|
||||
shelljs.exec("pnpm update -g @voerkai18n/cli ")
|
||||
createTestApp()
|
||||
shelljs.cd(APP_FOLDER);
|
||||
})
|
||||
|
||||
|
||||
test("工程目录国际化",done=>{
|
||||
let { code } = shelljs.exec(`voerkai18n init -lngs ${SUPPORTED_LANGUAGES.join(" ")} -r -a ${ACTIVE_LANGUAGE} -d ${DEFAULT_LANGUAGE}`,{silent:true})
|
||||
expect(code).toBe(0)
|
||||
expect(fs.existsSync(LANGUAGE_FOLDER)).toBe(true)
|
||||
|
||||
const settingsFile = path.join(LANGUAGE_FOLDER, "settings.json");
|
||||
expect(fs.existsSync(settingsFile)).toBe(true)
|
||||
|
||||
const langSettings = fs.readJSONSync(settingsFile);
|
||||
expect(langSettings.languages.map(lng=>lng.name).join(",")).toEqual(SUPPORTED_LANGUAGES.join(","));
|
||||
expect(langSettings.defaultLanguage).toEqual(DEFAULT_LANGUAGE);
|
||||
expect(langSettings.activeLanguage).toEqual(ACTIVE_LANGUAGE);
|
||||
done()
|
||||
})
|
||||
|
||||
test("提取文本",(done) =>{
|
||||
|
||||
let code = shelljs.exec(`voerkai18n 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(CN_TEXTS.every(text=>text in messages)).toBeTruthy();
|
||||
|
||||
for(let [text,lngs] of Object.entries(messages)){
|
||||
expect(SUPPORTED_LANGUAGES.every(lng=>lng===DEFAULT_LANGUAGE || (lng in lngs))).toBeTruthy();
|
||||
}
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
|
||||
test("编译多语言",(done) =>{
|
||||
// 模拟翻译英文文件
|
||||
const msgFile = path.join(TRANSLATES_FOLDER,"default.json")
|
||||
let messages = fs.readJSONSync(msgFile)
|
||||
messages["一"]["en"] = "One"
|
||||
messages["二"]["en"] = "Two"
|
||||
messages["三"]["en"] = "Three"
|
||||
messages["四"]["en"] = "Four"
|
||||
messages["五"]["en"] = "Five"
|
||||
fs.writeFileSync(msgFile,JSON.stringify(messages,null,2))
|
||||
|
||||
const code = shelljs.exec(`voerkai18n compile -m cjs`).code
|
||||
expect(code).toEqual(0);
|
||||
// 是否生成所有的文件
|
||||
const filesIsGenerated = ["index.js","idMap.js","formatters.js","runtime.js"].every(filename=>fs.existsSync(path.join(LANGUAGE_FOLDER,filename)));
|
||||
expect(filesIsGenerated).toBeTruthy();
|
||||
expect(SUPPORTED_LANGUAGES.every(lng=>fs.existsSync(path.join(LANGUAGE_FOLDER,`${lng}.js`)))).toBe(true);
|
||||
done()
|
||||
})
|
||||
|
||||
|
||||
test("切换语言",async () =>{
|
||||
const { change,getMessages } = require(path.join(APP_FOLDER,"index.js"))
|
||||
expect(getMessages()).toEqual(CN_TEXTS.join(""));
|
||||
await change("en");
|
||||
expect(getMessages()).toEqual(EN_TEXTS.join(""));
|
||||
})
|
||||
|
||||
|
||||
|
163
test/cli.test.js
163
test/cli.test.js
@ -1,163 +0,0 @@
|
||||
/**
|
||||
* 测试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.json"))).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.json"));
|
||||
|
||||
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.json"))).toBe(true);
|
||||
expect(fs.readJSONSync(path.join(LANGUAGE_FOLDER,"package.json")).type || "commonjs").toEqual("commonjs");
|
||||
const langSettings = await importModule(path.join(LANGUAGE_FOLDER,"settings.json"));
|
||||
|
||||
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()
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -5,7 +5,7 @@ const Vinyl = require('vinyl');
|
||||
const { getTranslateTexts, normalizeLanguageOptions } = require("../packages/cli/extract.plugin");
|
||||
|
||||
|
||||
const languages = [{name:'en',title:"英文"},{name:'cn',title:"中文",default:true},{name:'de',title:"德语"},{name:'jp',title:"日本語"}]
|
||||
const languages = [{name:'en',title:"英文"},{name:'zh',title:"中文",default:true},{name:'de',title:"德语"},{name:'jp',title:"日本語"}]
|
||||
|
||||
test("扫描提取翻译文本",(done)=>{
|
||||
const file = new Vinyl({cwd: '/',base: '/test/',path: '/test/file.js',contents: Buffer.from("")});
|
||||
|
@ -2,7 +2,7 @@ const dayjs = require('dayjs');
|
||||
const { getInterpolatedVars, replaceInterpolatedVars , translate} = require('../packages/runtime/index.js')
|
||||
|
||||
const messages = {
|
||||
cn:{
|
||||
zh:{
|
||||
1:"你好",
|
||||
2:"现在是{}",
|
||||
3:"我出生于{year}年,今年{age}岁",
|
||||
@ -27,9 +27,9 @@ const idMap = {
|
||||
|
||||
|
||||
let scope1 ={
|
||||
defaultLanguage: "cn", // 默认语言名称
|
||||
default: messages.cn,
|
||||
messages :messages.cn,
|
||||
defaultLanguage: "zh", // 默认语言名称
|
||||
default: messages.zh,
|
||||
messages :messages.zh,
|
||||
idMap,
|
||||
formatters:{ // 当前作用域的格式化函数列表
|
||||
"*":{
|
||||
@ -42,7 +42,7 @@ let scope1 ={
|
||||
upper:(v)=>v.toUpperCase(),
|
||||
lower:(v)=>v.toLowerCase()
|
||||
},
|
||||
cn:{
|
||||
zh:{
|
||||
$types:{
|
||||
Date:(v)=>dayjs(v).format('YYYY年MM月DD日 HH点mm分ss秒'),
|
||||
Boolean:(v)=>v?"是":"否",
|
||||
@ -58,10 +58,10 @@ let scope1 ={
|
||||
},
|
||||
loaders:{}, // 异步加载语言文件的函数列表
|
||||
global:{// 引用全局VoerkaI18n配置
|
||||
defaultLanguage: "cn",
|
||||
activeLanguage: "cn",
|
||||
defaultLanguage: "zh",
|
||||
activeLanguage: "zh",
|
||||
languages:[
|
||||
{name:"cn",title:"中文",default:true},
|
||||
{name:"zh",title:"中文",default:true},
|
||||
{name:"en",title:"英文"},
|
||||
{name:"de",title:"德语"},
|
||||
{name:"jp",title:"日语"}
|
||||
@ -72,7 +72,7 @@ let scope1 ={
|
||||
|
||||
}
|
||||
},
|
||||
cn:{
|
||||
zh:{
|
||||
$types:{
|
||||
|
||||
}
|
||||
@ -97,7 +97,7 @@ function changeLanguage(language){
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
scope1.global.activeLanguage = "cn" // 切换到中文
|
||||
scope1.global.activeLanguage = "zh" // 切换到中文
|
||||
});
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user