增加自动翻译机制

This commit is contained in:
fisher 2022-04-11 09:00:25 +08:00
parent 834ae4cbfd
commit af2384683d
54 changed files with 5878 additions and 3093 deletions

12
.gitignore vendored
View File

@ -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/

View File

@ -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"
}
}

View File

@ -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"
}

View File

@ -47,7 +47,7 @@
*
*/
module.exports = {
export default {
// 在所有语言下生效的格式化器
"*":{
//[格式化名称]:(value)=>{...},

View File

@ -1,4 +1,4 @@
module.exports = {
export default {
"a": 1,
"b": 2,
"c": 3,

View File

@ -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
}

View File

@ -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": {}
}

View File

@ -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": "写真"
}
}

View File

@ -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);

View File

@ -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"
}

View File

@ -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)
[![fisher/voerka-i18n](https://gitee.com/zhangfisher/voerka-i18n/widgets/widget_card.svg?colors=4183c4,ffffff,ffffff,e3e9ed,666666,9b9b9b)](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`列出当前工程的所有包,并显示当前包最近更新和最近发布时间。

View File

@ -11,9 +11,15 @@
*
* {
* plugins:[
* ["voerkai18n",{}]
* ["voerkai18n",{
* location:"./languages",
* autoImport:"./languages",
* moduleType:"esm"
* }]
* ]
*
*
*
* }
*
*

View File

@ -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"
}

File diff suppressed because it is too large Load Diff

View 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);
})
});
}
}
}

View File

@ -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 = {

View File

@ -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")

View File

@ -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

View File

@ -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);

View File

@ -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:{}

View File

@ -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": " - 翻译 -> {}"
}

View File

@ -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": " - 翻译 -> {}"
}

View File

@ -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:{
// 所有类型的默认格式化器
// "*":{

View File

@ -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
}

View File

@ -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

View File

@ -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;

View File

@ -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": {}
}

View File

@ -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"
]
}
}

View 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": " - 翻译 -> {}"
}

View File

@ -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"
}

View File

@ -12,7 +12,7 @@
* {
* "保存{}\\n": "Save{}\n",
* }
* 这个转义方式比较不符合我们的预期更关键的是在require("cn.js")
* 这个转义方式比较不符合我们的预期更关键的是在require("zh.js")
* 得到的是
* {
* "保存{}\\n": "Save{}\n",

View File

@ -10,7 +10,7 @@
$types:{...}, // 只作用于特定数据类型的默认格式化器
.... // 全局格式化器
},
cn:{
zh:{
// 只作用于特定数据类型的格式化器
$types:{
Date:(value)=>dayjs(value).format("YYYY年MM月DD日 HH:mm:ss"),

View 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)
}
}

View File

@ -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"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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;

View File

@ -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 };

View File

@ -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()}`
},

View File

@ -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,

View File

@ -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"
}

View File

@ -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 // 当前语言包

View File

@ -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, // 深度合并对象

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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

File diff suppressed because it is too large Load Diff

160
readme.md
View File

@ -1,6 +1,3 @@
# ** 测试阶段有问题请issues **
[![star](https://gitee.com/zhangfisher/voerka-i18n/badge/star.svg?theme=white)](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
View 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(""));
})

View File

@ -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()
})

View File

@ -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("")});

View File

@ -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" // 切换到中文
});