Alpha版本发布,并完成文档

This commit is contained in:
wxzhang 2022-03-21 15:18:30 +08:00
parent 353234cd01
commit b7340a9989
27 changed files with 1886 additions and 567 deletions

BIN
images/arch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -1,4 +1,7 @@
{
"type": "commonjs",
"license": "MIT"
"license": "MIT",
"devDependencies": {
"@voerkai18n/tools": "workspace:^1.0.0"
}
}

View File

@ -10,13 +10,10 @@ module.exports = {
"title" : "标题",
"content": "内容"
},
"产品清单\t{}": [
"手机",
"电脑"
],
"产品清单\t{}": 2,
"产品价格" : {
"手机": 1299,
"电脑": 3999
"手机" : 1299,
"电脑\t:": 3999
},
"产品\\清单" : 1
"产品\\清\\单": 1
}

View File

@ -1,10 +1,16 @@
const objectToString = require("../tools/stringify")
const { objectStringify } = require("../tools/stringify")
const path = require("path");
const fs = require("fs");
const k1 = "产品清单\t{}"
const k2 = "产品\\清单"
const data = {
const k2 = "产品\\清\\单"
const newTexts = {
[k1]:1,
[k2]:2
}
let data = {
name:"张三丰",
age:12,
active:true,
@ -21,14 +27,16 @@ const data = {
"手机",
"电脑"
],
"产品清单\t{}":2,
"产品价格":{
"手机":1299,
"电脑":3999
"电脑\t:":3999
},
[k2]:1
"产品\\清\\单":1
}
const result = objectToString(data)
const result = objectStringify(data)
console.log(result)
@ -36,6 +44,15 @@ fs.writeFileSync(path.join(__dirname,"./data.js"),`module.exports = ${result}`)
const loaded = require("./data.js")
Object.entries(loaded).forEach(([key,value])=>{
if(key===k1){
console.log("k1=",value)
}else if(key===k2){
console.log("k2=",value)
console.log("k2 in data",k2 in data)
}
})
console.log(loaded[k1])
console.log(loaded[k2])

View File

@ -0,0 +1,31 @@
/**
*
* 简单的事件触发器
*
*/
module.exports = 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)))
}
}
}

View File

@ -26,18 +26,19 @@
* @param {...any} args
* @returns
*/
function dict(value,...args){
try{
for(let i=0;i<args.length;i+=2){
if(args[i]===value){
return args[i+1]
}
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]
}catch{}
}
if(args.length >0 && (args.length % 2!==0)) return args[args.length-1]
return value
}
function formatCurrency(value,symbol,retainDots){
}
module.exports = {
"*":{
@ -45,6 +46,7 @@ module.exports = {
Date:(value)=>value.toLocaleString()
},
time:(value)=> value.toLocaleTimeString(),
shorttime:(value)=> value.toLocaleTimeString(),
date: (value)=> value.toLocaleDateString(),
dict, //字典格式化器
},
@ -52,11 +54,15 @@ module.exports = {
$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)=>`$${value}`,
currency:(value)=>{
return `$${value}`
}
}
}

View File

@ -1,5 +1,8 @@
const deepMerge = require("deepmerge")
const formatters = require("./formatters")
const EventEmitter = require("./eventemitter")
const i18nScope = require("./scope.js")
let formatters = require("./formatters")
// 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter }
// 不支持参数: let varWithPipeRegexp = /\{\s*(?<varname>\w+)?(?<formatters>(\s*\|\s*\w*\s*)*)\s*\}/g
@ -30,6 +33,8 @@ let varReplaceRegexp =String.raw`\{\s*{varname}\s*\}`
function hasInterpolation(str){
return str.includes("{") && str.includes("}")
}
const DataTypes = ["String","Number","Boolean","Object","Array","Function","Error","Symbol","RegExp","Date","Null","Undefined","Set","Map","WeakSet","WeakMap"]
/**
* 获取指定变量类型名称
* getDataTypeName(1) == Number
@ -251,6 +256,7 @@ function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){
// 先在当前作用域中查找,再在全局查找
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
@ -457,7 +463,29 @@ function getPluraMessage(messages,value){
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,"\\")
}
/**
* 翻译函数
*
@ -517,8 +545,9 @@ function translate(message) {
// 2.2 从当前语言包中取得翻译文本模板字符串
// 如果没有启用babel插件将源文本转换为msgId需要先将文本内容转换为msgId
// JSON.stringify在进行转换时会将\t\n\r转换为\\t\\n\\r,这样在进行匹配时就出错
let msgId = isMessageId(content) ? content : scope.idMap[content]
let msgId = isMessageId(content) ? content : scope.idMap[escape(content)]
content = scope.messages[msgId] || content
content = unescape(content)
}
// 3. 处理复数
@ -561,10 +590,11 @@ function translate(message) {
* VoerkaI18n.off("change",(language)=>{})
*
* */
class I18nManager{
class I18nManager extends EventEmitter{
static instance = null; // 单例引用
callbacks = [] // 当切换语言时的回调事件
constructor(settings={}){
super()
if(I18nManager.instance!=null){
return I18nManager.instance;
}
@ -581,96 +611,39 @@ function translate(message) {
get defaultLanguage(){ return this.this._settings.defaultLanguage}
// 支持的语言列表
get languages(){ return this._settings.languages}
// 订阅语言切换事件
on(callback){
this.callbacks.push(callback)
}
off(callback){
for(let i=0;i<this.callbacks.length;i++){
if(this.callbacks[i]===callback ){
this.callbacks.splice(i,1)
return
}
}
}
offAll(){
this.callbacks=[]
}
/**
* 切换语言时触发语言切换事件回调
*/
async _triggerChangeEvents(newLanguage){
try{
await this._updateScopes(newLanguage)
if(Promise.allSettled){
await Promise.allSettled(this.callbacks.map(cb=>cb(newLanguage)))
}else{
await Promise.all(this.callbacks.map(cb=>cb(newLanguage)))
}
}catch(e){
console.warn("Error while executing language change events:",e.message)
}
}
// 全局格式化器
get formatters(){ return formatters }
/**
* 切换语言
*/
async change(value){
if(this.languages.findIndex(lang=>lang.name === value)!==-1){
await this._triggerChangeEvents(value)
// 通知所有作用域刷新到对应的语言包
await this._refreshScopes(value)
this._settings.activeLanguage = value
/// 触发语言切换事件
await this.emit(value)
}else{
throw new Error("Not supported language:"+value)
}
}
/**
* 获取指定作用域的下的语言包加载器
*
* 同时会进行语言兼容性处理
*
* 如scope里面定义了一个cn的语言包当切换到zh-cn时会自动加载cn语言包
*
*
* @param {*} scope
* @param {*} lang
*/
_getScopeLoader(scope,lang){
}
/**
* 当切换语言时调用此方法来加载更新语言包
* @param {*} newLanguage
*/
async _updateScopes(newLanguage){
async _refreshScopes(newLanguage){
// 并发执行所有作用域语言包的加载
try{
let scopeLoders = this._scopes.map(scope=>{
return async ()=>{
// 默认语言,所有均默认语言均采用静态加载方式,只需要简单的替换即可
if(newLanguage === scope.defaultLanguage){
scope.messages = scope.default
return
}
// 异步加载语言文件
const loader = scope.loaders[newLanguage]
if(typeof(loader) === "function"){
try{
scope.messages = (await loader() ).default
}catch(e){
console.warn(`Error loading language ${newLanguage} : ${e.message}`)
scope.messages = defaultMessages // 出错时回退到默认语言
}
}else{
scope.messages = defaultMessages
}
}
})
const scopeRefreshers = this._scopes.map(scope=>{
return scope.refresh(newLanguage)
})
if(Promise.allSettled){
await Promise.allSettled(scopeLoders.map((f)=>f()))
await Promise.allSettled(scopeRefreshers)
}else{
await Promise.all(scopeLoders.map((f)=>f()))
await Promise.all(scopeRefreshers)
}
}catch(e){
console.warn("Error while refreshing scope:",e.message)
console.warn("Error while refreshing i18n scopes:",e.message)
}
}
/**
@ -678,33 +651,36 @@ function translate(message) {
* 注册一个新的作用域
*
* 每一个库均对应一个作用域每个作用域可以有多个语言包且对应一个翻译函数
* scope={
* defaultLanguage:"cn",
default: defaultMessages, // 转换文本信息
messages : defaultMessages, // 当前语言的消息
idMap:messageIds,
formatters:{
...formatters,
...i18nSettings.formatters || {}
},
loaders:{}, // 异步加载语言文件的函数列表
settings:{} // 引用全局VoerkaI18n实例的配置
* }
*
* 除了默认语言外其他语言采用动态加载的方式
*
* @param {*} scope
*/
register(scope){
scope.global = this._settings
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
*/
registerFormatters(formatters){
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
}
}
}
@ -714,6 +690,7 @@ module.exports ={
I18nManager,
translate,
languages,
i18nScope,
defaultLanguageSettings,
getDataTypeName,
isNumber,

115
packages/runtime/scope.js Normal file
View File

@ -0,0 +1,115 @@
module.exports = 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 } = require("./")
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)
}
}

View File

@ -1,2 +1,2 @@
# 命令行工具使用的语言
language=en
LANGUAGE=en

View File

@ -27,11 +27,11 @@ const readJson = require("readjson")
const glob = require("glob")
const createLogger = require("logsets")
const path = require("path")
const { importModule,findModuleType } = require("./utils")
const { t,importModule,findModuleType,getCurrentPackageJson} = require("./utils")
const fs = require("fs")
const logger = createLogger()
const artTemplate = require("art-template")
const { t } = require("./languages")
function normalizeCompileOptions(opts={}) {
let options = Object.assign({
moduleType:"auto" // 指定编译后的语言文件的模块类型取值common,cjs,esm,es
@ -49,7 +49,7 @@ module.exports =async function compile(langFolder,opts={}){
if(moduleType==="auto"){
moduleType = findModuleType(langFolder)
}
const projectPackageJson = getCurrentPackageJson(langFolder)
// 加载多语言配置文件
const settingsFile = path.join(langFolder,"settings.js")
try{
@ -58,13 +58,13 @@ module.exports =async function compile(langFolder,opts={}){
const langSettings = module.default;
let { languages,defaultLanguage,activeLanguage,namespaces } = langSettings
logger.log(t("支持的语言\t: {}",languages.map(item=>`${item.title}(${item.name})`).join(",")))
logger.log("默认语言\t: {}",defaultLanguage)
logger.log("激活语言\t: {}",activeLanguage)
logger.log("名称空间\t: {}",Object.keys(namespaces).join(","))
logger.log("模块类型\t: {}",moduleType)
logger.log(t("支持的语言\t: {}"),languages.map(item=>`${item.title}(${item.name})`).join(","))
logger.log(t("默认语言\t: {}"),defaultLanguage)
logger.log(t("激活语言\t: {}"),activeLanguage)
logger.log(t("名称空间\t: {}"),Object.keys(namespaces).join(","))
logger.log(t("模块类型\t: {}"),moduleType)
logger.log("")
logger.log("编译结果输出至:{}",langFolder)
logger.log(t("编译结果输出至:{}"),langFolder)
// 1. 合并生成最终的语言文件
let messages = {} ,msgId =1
@ -79,10 +79,10 @@ module.exports =async function compile(langFolder,opts={}){
}
})
}catch(e){
logger.log("读取语言文件{}失败:{}",file,e.message)
logger.log(t("读取语言文件{}失败:{}"),file,e.message)
}
})
logger.log(" - 共合成{}条语言包文本",Object.keys(messages).length)
logger.log(t(" - 共合成{}条语言包文本"),Object.keys(messages).length)
// 2. 为每一个文本内容生成一个唯一的id
let messageIds = {}
@ -103,7 +103,7 @@ module.exports =async function compile(langFolder,opts={}){
}else{
fs.writeFileSync(langFile,`module.exports = ${JSON.stringify(langMessages,null,4)}`)
}
logger.log(" - 语言包文件: {}",path.basename(langFile))
logger.log(t(" - 语言包文件: {}"),path.basename(langFile))
})
// 4. 生成id映射文件
@ -113,34 +113,46 @@ module.exports =async function compile(langFolder,opts={}){
}else{
fs.writeFileSync(idMapFile,`module.exports = ${JSON.stringify(messageIds,null,4)}`)
}
logger.log(" - idMap文件: {}",path.basename(idMapFile))
// 5. 生成编译后的访问入口文件
const entryFile = path.join(langFolder,"index.js")
const entryContent = artTemplate(path.join(__dirname,"templates","entry.js"), {languages,defaultLanguage,activeLanguage,namespaces,moduleType,JSON } )
fs.writeFileSync(entryFile,entryContent)
logger.log(" - 访问入口文件: {}",path.basename(entryFile))
logger.log(t(" - idMap文件: {}"),path.basename(idMapFile))
// 6 . 生成编译后的格式化函数文件
const templateContext = {
scopeId:projectPackageJson.name,
languages,
defaultLanguage,
activeLanguage,
namespaces,
moduleType,
JSON
}
// 5 . 生成编译后的格式化函数文件
const formattersFile = path.join(langFolder,"formatters.js")
if(!fs.existsSync(formattersFile)){
const formattersContent = artTemplate(path.join(__dirname,"templates","formatters.js"), {languages,defaultLanguage,activeLanguage,namespaces,moduleType } )
const formattersContent = artTemplate(path.join(__dirname,"templates","formatters.js"), templateContext )
fs.writeFileSync(formattersFile,formattersContent)
logger.log(" - 格式化器:{}",path.basename(formattersFile))
logger.log(t(" - 格式化器:{}"),path.basename(formattersFile))
}else{ // 格式化器如果存在,则需要更改对应的模块类型
let formattersContent = fs.readFileSync(formattersFile,"utf8").toString()
if(moduleType == "esm"){
formattersContent = formattersContent.replaceAll(/\s*module.exports\s*\=/g,"export default ")
formattersContent = formattersContent.replaceAll(/\s*module.exports\./g,"export ")
}else{
formattersContent = formattersContent.replaceAll(/\s*export\s*default\s*/g,"module.exports = ")
formattersContent = formattersContent.replaceAll(/\s*export\s*/g,"module.exports.")
formattersContent = formattersContent.replaceAll(/^\s*export\s*default\s*/g,"module.exports = ")
formattersContent = formattersContent.replaceAll(/^\s*export\s*/g,"module.exports.")
}
fs.writeFileSync(formattersFile,formattersContent)
logger.log(" - 更新格式化器:{}",path.basename(formattersFile))
logger.log(t(" - 更新格式化器:{}"),path.basename(formattersFile))
}
// 6. 生成编译后的访问入口文件
const entryFile = path.join(langFolder,"index.js")
const entryContent = artTemplate(path.join(__dirname,"templates","entry.js"), templateContext )
fs.writeFileSync(entryFile,entryContent)
logger.log(t(" - 访问入口文件: {}"),path.basename(entryFile))
// 7. 重新生成settings ,需要确保settings.js匹配模块类型
if(moduleType==="esm"){
fs.writeFileSync(settingsFile,`export default ${JSON.stringify(langSettings,null,4)}`)
@ -163,6 +175,6 @@ module.exports =async function compile(langFolder,opts={}){
}
fs.writeFileSync(packageJsonFile,JSON.stringify(packageJson,null,4))
}catch(e){
logger.log("加载多语言配置文件<{}>失败: {} ",settingsFile,e.message)
logger.log(t("加载多语言配置文件<{}>失败: {} "),settingsFile,e.message)
}
}

View File

@ -1,4 +1,4 @@
const { findModuleType } = require("./utils")
const { findModuleType,t } = require("./utils")
const path = require("path")
const fs = require("fs")
const gulp = require("gulp")
@ -9,7 +9,7 @@ const logger = createLogger()
module.exports = function(targetPath,options={}){
let { filetypes,exclude} = options
if(!filetypes) filetypes = ["*.js","*.json","*.jsx","*.ts","*.tsx","*.vue","*.html"]
if(!filetypes) filetypes = ["*.js","*.jsx","*.ts","*.tsx","*.vue","*.html"]
if(!Array.isArray(filetypes)) filetypes = filetypes.split(",")
const folders = filetypes.map(ftype=>{
if(ftype.startsWith(".")) ftype = "*"+ftype
@ -19,6 +19,12 @@ module.exports = function(targetPath,options={}){
folders.push(`!${path.join(targetPath,"languages","**")}`)
folders.push(`!${path.join(targetPath,"node_modules","**")}`)
folders.push(`!${path.join(targetPath,"**","node_modules","**")}`)
folders.push("!**/babel.config.js")
folders.push("!**/gulpfile.js")
folders.push("!**/*.test.js")
folders.push("!__test__/**/*.js")
if(!Array.isArray(exclude) && exclude){
exclude = exclude.split(",")
}
@ -28,11 +34,11 @@ module.exports = function(targetPath,options={}){
})
}
if(!fs.existsSync(targetPath)){
logger.log("目标文件夹<{}>不存在",targetPath)
logger.log(t("目标文件夹<{}>不存在"),targetPath)
return
}
if(options.debug){
logger.log("扫描提取范围:")
logger.log(t("扫描提取范围:"))
logger.format(folders)
}

View File

@ -12,10 +12,8 @@ const fs = require('fs')
const readJson = require("readjson")
const createLogger = require("logsets")
const { replaceInterpolateVars,getDataTypeName } = require("@voerkai18n/runtime")
const { findModuleType,createPackageJsonFile } = require("./utils")
const stringify = require("./stringify")
const { findModuleType,createPackageJsonFile,t } = require("./utils")
const logger = createLogger()
const { t } = require("./languages")
// 捕获翻译文本正则表达式一: 能匹配完整的t(xx,...)函数调用如果t函数调用不完整则不能匹配到
@ -311,35 +309,35 @@ function normalizeLanguageOptions(options){
*/
function updateLanguageFile(fromTexts,toLangFile,options){
function updateLanguageFile(newTexts,toLangFile,options){
const { output:{ updateMode } } = options
// 默认的overwrite
if(!["merge","sync"].includes(updateMode)){
fs.writeFileSync(toLangFile,stringify(targetTexts))
fs.writeFileSync(toLangFile,JSON.stringify(oldTexts,null,4))
return
}
let targetTexts = {}
let oldTexts = {}
// 读取原始翻译文件
try{
targetTexts = readJson.sync(toLangFile)
oldTexts =JSON.parse(fs.readFileSync(toLangFile))// readJson.sync(toLangFile)
}catch(e){
logger.log("Error while read language file <{}>: {}",toLangFile,e.message)
// 如果读取出错可能是语言文件不是有效的json文件则备份一下
}
// 同步模式下,如果原始文本在新扫描的内容中,则需要删除
if(updateMode==="sync"){
Object.keys(targetTexts).forEach((text)=>{
if(!(text in fromTexts)){
delete targetTexts[text]
Object.keys(oldTexts).forEach((text)=>{
if(!(text in newTexts)){
delete oldTexts[text]
}
})
}
Object.entries(fromTexts).forEach(([text,sourceLangs])=>{
if(text in targetTexts){ // 合并
let targetLangs = targetTexts[text] //{cn:'',en:''}
Object.entries(newTexts).forEach(([text,sourceLangs])=>{
if(text in oldTexts){ // 合并
let targetLangs = oldTexts[text] //{cn:'',en:''}
Object.entries(sourceLangs).forEach(([langName,sourceText])=>{
if(langName.startsWith("$")) return //
if(langName.startsWith("$")) return // 以$开头的为保留字段,不是翻译内容
const langExists = langName in targetLangs
const targetText = targetLangs[langName]
// 如果目标语言已经存在并且内容不为空,则不需要更新
@ -348,10 +346,10 @@ function updateLanguageFile(fromTexts,toLangFile,options){
}
})
}else{
targetTexts[text] = sourceLangs
oldTexts[text] = sourceLangs
}
})
fs.writeFileSync(toLangFile,stringify(targetTexts))
fs.writeFileSync(toLangFile,JSON.stringify(oldTexts,null,4))
}
@ -359,10 +357,10 @@ module.exports = function(options={}){
options = normalizeLanguageOptions(options)
let {debug,outputPath, updateMode,languages} = options
logger.log(t("支持的语言\t: {}",options.languages.map(item=>`${item.title}(${item.name})`).join(",")))
logger.log("默认语言\t: {}",options.defaultLanguage)
logger.log("激活语言\t: {}",options.activeLanguage)
logger.log("名称空间\t: {}",Object.keys(options.namespaces).join(","))
logger.log(t("支持的语言\t: {}"),options.languages.map(item=>`${item.title}(${item.name})`).join(","))
logger.log(t("默认语言\t: {}"),options.defaultLanguage)
logger.log(t("激活语言\t: {}"),options.activeLanguage)
logger.log(t("名称空间\t: {}"),Object.keys(options.namespaces).join(","))
logger.log("")
logger
// 保存提交提取的文本 = {}
@ -384,7 +382,9 @@ module.exports = function(options={}){
fileCount++
if(debug){
const textCount = Object.values(texts).reduce((sum,item)=>sum+Object.keys(item).length,0)
logger.log("提取<{}>, 发现 [{}] 名称空间,{} 条信息。",file.relative,Object.keys(texts).join(),textCount)
if(textCount>0){
logger.log("提取<{}>, 发现 [{}] 名称空间,{} 条信息。",file.relative,Object.keys(texts).join(),textCount)
}
}
}catch(err){
logger.log("从<{}>提取信息时出错 : {}",file.relative,err.message)
@ -411,7 +411,7 @@ module.exports = function(options={}){
updateLanguageFile(texts,langFile,options)
logger.log(" √ 更新语言文件 : {}",path.relative(outputPath,langFile))
}else{
fs.writeFileSync(langFile,stringify(texts))
fs.writeFileSync(langFile,JSON.stringify(texts,null,4))
logger.log(" √ 保存语言文件 : {}",path.relative(outputPath,langFile))
}
}
@ -430,7 +430,6 @@ module.exports = function(options={}){
logger.log(" - 已更新语言配置文件: {}",settingsFile)
}
// 生成package.json
createPackageJsonFile(outputPath)

View File

@ -2,29 +2,31 @@ const { Command } = require('commander');
const createLogger = require("logsets")
const path = require("path")
const fs = require("fs");
const { importModule } = require('./utils');
const { importModule,t } = require('./utils');
const deepmerge = require('deepmerge');
const logger = createLogger()
require('dotenv').config()
const { scope } = require('./languages');
const { t, changeLanguage } = require("./languages")
const program = new Command();
program
.command('init')
.argument('[location]', t('工程项目所在目录'))
.description('初始化项目国际化配置')
.option('-d, --debug', '输出调试信息')
.option('-r, --reset', '重新生成当前项目的语言配置')
.option('-m, --moduleType [type]', '生成的js模块类型,取值auto,esm,cjs',"auto")
.option('-lngs, --languages <languages...>', '支持的语言列表', ['cn','en'])
.description(t('初始化项目国际化配置'))
.option('-d, --debug', t('输出调试信息'))
.option('-r, --reset', t('重新生成当前项目的语言配置'))
.option('-m, --moduleType [type]', t('生成的js模块类型,取值auto,esm,cjs'),"auto")
.option('-lngs, --languages <languages...>', t('支持的语言列表'), ['cn','en'])
.hook("preAction",async function(location){
})
.action((location,options) => {
if(!location) {
location = process.cwd()
}else{
location = path.join(process.cwd(),location)
}
logger.log("工程目录:{}",location)
logger.log(t("工程目录:{}"),location)
//
if(options.debug){
logger.format(options,{compact:true})
@ -36,16 +38,16 @@ program
program
.command('extract')
.description('扫描并提取所有待翻译的字符串到<languages/translates>文件夹中')
.option('-d, --debug', '输出调试信息')
.option('-lngs, --languages', '支持的语言', 'cn,en')
.option('-d, --defaultLanguage', '默认语言', 'cn')
.option('-a, --activeLanguage', '激活语言', 'cn')
.option('-ns, --namespaces', '翻译名称空间')
.option('-e, --exclude <folders>', '排除要扫描的文件夹,多个用逗号分隔')
.option('-u, --updateMode', '本次提取内容与已存在内容的数据合并策略,默认取值sync=同步,overwrite=覆盖,merge=合并', 'sync')
.option('-f, --filetypes', '要扫描的文件类型', 'js,vue,html,jsx,ts')
.argument('[location]', '工程所在目录',"./")
.description(t('扫描并提取所有待翻译的字符串到<languages/translates>文件夹中'))
.option('-d, --debug', t('输出调试信息'))
.option('-lngs, --languages', t('支持的语言'), 'cn,en')
.option('-default, --defaultLanguage', t('默认语言'), 'cn')
.option('-active, --activeLanguage', t('激活语言'), 'cn')
.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')
.argument('[location]', t('工程项目所在目录'),"./")
.action(async (location,options) => {
if(!location) {
location = process.cwd()
@ -56,10 +58,10 @@ program
options.languages = options.languages.split(",").map(l=>({name:l,title:l}))
}
//options = Object.assign({},options)
logger.log("工程目录:{}",location)
logger.log(t("工程目录:{}"),location)
const langSettingsFile = path.join(location,"languages","settings.js")
if(fs.existsSync(langSettingsFile)){
logger.log("语言配置文件<{}>已存在,将优先使用此配置文件中参数来提取文本","./languages/settings.js")
logger.log(t("语言配置文件<{}>已存在,将优先使用此配置文件中参数来提取文本","./languages/settings.js"))
let lngOptions = (await importModule("file:///"+langSettingsFile)).default
options.languages = lngOptions.languages
options.defaultLanguage = lngOptions.defaultLanguage
@ -67,9 +69,9 @@ program
options.namespaces = lngOptions.namespaces
}
//
if(options.debug){
logger.format(options,{compact:true})
}
// if(options.debug){
// logger.format(options,{compact:true})
// }
const extractor = require('./extract.command');
extractor(location,options)
});
@ -77,13 +79,15 @@ program
program
.command('compile')
.description('编译语言包文件<languages>文件夹中')
.option('-d, --debug', '输出调试信息')
.option('-m, --moduleType [types]', '输出模块类型,取值auto,esm,cjs', 'auto')
.description(t('编译指定项目的语言包'))
.option('-d, --debug', t('输出调试信息'))
.option('-m, --moduleType [types]', t('输出模块类型,取值auto,esm,cjs'), 'auto')
.argument('[location]', t('工程项目所在目录'),"./")
.hook("preAction",async (location,options) => {
console.log("process.env.language",process.env.language)
await changeLanguage("en")
.hook("preAction",async function(location){
const lang= process.env.LANGUAGE
if(lang){
await scope.change(lang)
}
})
.action(async (location,options) => {
if(!location) {
@ -93,12 +97,12 @@ program
}
const langFolder = path.join(location,"languages")
if(!fs.existsSync(langFolder)){
logger.error("语言包文件夹<{}>不存在",langFolder)
logger.error(t("语言包文件夹<{}>不存在",langFolder))
return
}
if(options.debug){
logger.format(options,{compact:true})
}
// if(options.debug){
// logger.format(options,{compact:true})
// }
compile = require("./compile.command")
compile(langFolder,options)
});

View File

@ -4,38 +4,40 @@
*/
const { findModuleType,createPackageJsonFile } = require("./utils")
const { findModuleType,createPackageJsonFile,t } = require("./utils")
const path = require("path")
const fs = require("fs")
const createLogger = require("logsets")
const logger = createLogger()
module.exports = function(targetPath,{debug = true,languages=["cn","en"],defaultLanguage="cn",activeLanguage="cn",moduleType = "auto",reset=false}={}){
module.exports = function(srcPath,{debug = true,languages=["cn","en"],defaultLanguage="cn",activeLanguage="cn",moduleType = "auto",reset=false}={}){
// 语言文件夹名称
const langPath = "languages"
// 查找当前项目的语言包类型路径
const lngPath = path.join(targetPath,langPath)
const lngPath = path.join(srcPath,langPath)
if(!fs.existsSync(lngPath)){
fs.mkdirSync(lngPath)
if(debug) logger.log(t("创建语言包文件夹: {}"),lngPath)
}
moduleType = createPackageJsonFile(lngPath,moduleType)
if(moduleType==null) {
if(debug){
logger.log("找不到{}文件,{}只能在js项目工程中使用","package.json","voerkai18n")
logger.log(t("找不到{}文件,{}只能在js项目工程中使用"),"package.json","voerkai18n")
}else{
throw new Error("找不到package.json文件,voerkai18n只能在js项目工程中使用")
throw new Error(t("找不到package.json文件,voerkai18n只能在js项目工程中使用"))
}
}
if(!fs.existsSync(lngPath)){
fs.mkdirSync(lngPath)
if(debug) logger.log("创建语言包文件夹: {}",lngPath)
}
// 创建settings.js文件
const settingsFile = path.join(lngPath,"settings.js")
if(fs.existsSync(settingsFile) && !reset){
if(debug) logger.log("语言配置文件{}文件已存在,跳过创建。\n使用{}可以重新覆盖创建",settingsFile,"-r")
if(debug) logger.log(t("语言配置文件{}文件已存在,跳过创建。\n使用{}可以重新覆盖创建"),settingsFile,"-r")
return
}
const settings = {
@ -52,11 +54,11 @@ module.exports = function(targetPath,{debug = true,languages=["cn","en"],default
}
if(debug) {
logger.log("生成语言配置文件:{}","./languages/settings.js")
logger.log("拟支持的语言:{}",settings.languages.map(l=>l.name).join(","))
logger.log("初始化成功,下一步:")
logger.log(" - 编辑{}确定拟支持的语言种类等参数","languages/settings.js")
logger.log(" - 运行<{}>扫描提取要翻译的文本","voerkai18n extract")
logger.log(" - 运行<{}>编译语言包","voerkai18n compile")
logger.log(t("生成语言配置文件:{}"),"./languages/settings.js")
logger.log(t("拟支持的语言:{}"),settings.languages.map(l=>l.name).join(","))
logger.log(t("初始化成功,下一步:"))
logger.log(t(" - 编辑{}确定拟支持的语言种类等参数"),"languages/settings.js")
logger.log(t(" - 运行<{}>扫描提取要翻译的文本"),"voerkai18n extract")
logger.log(t(" - 运行<{}>编译语言包"),"voerkai18n compile")
}
}

View File

@ -1,4 +1,46 @@
module.exports = {
"1": "支持的语言\t: {}",
"2": "工程项目所在目录"
"1": "支持的语言\\t: {}",
"2": "默认语言\\t: {}",
"3": "激活语言\\t: {}",
"4": "名称空间\\t: {}",
"5": "模块类型\\t: {}",
"6": "编译结果输出至:{}",
"7": "读取语言文件{}失败:{}",
"8": " - 共合成{}条语言包文本",
"9": " - 语言包文件: {}",
"10": " - idMap文件: {}",
"11": " - 格式化器:{}",
"12": " - 更新格式化器:{}",
"13": " - 访问入口文件: {}",
"14": "加载多语言配置文件<{}>失败: {} ",
"15": "目标文件夹<{}>不存在",
"16": "扫描提取范围:",
"17": "工程项目所在目录",
"18": "初始化项目国际化配置",
"19": "输出调试信息",
"20": "重新生成当前项目的语言配置",
"21": "生成的js模块类型,取值auto,esm,cjs",
"22": "支持的语言列表",
"23": "工程目录:{}",
"24": "扫描并提取所有待翻译的字符串到<languages/translates>文件夹中",
"25": "支持的语言",
"26": "默认语言",
"27": "激活语言",
"28": "翻译名称空间",
"29": "排除要扫描的文件夹,多个用逗号分隔",
"30": "本次提取内容与已存在内容的数据合并策略,默认取值sync=同步,overwrite=覆盖,merge=合并",
"31": "要扫描的文件类型",
"32": "语言配置文件<{}>已存在,将优先使用此配置文件中参数来提取文本",
"33": "编译指定项目的语言包",
"34": "输出模块类型,取值auto,esm,cjs",
"35": "语言包文件夹<{}>不存在",
"36": "找不到{}文件,{}只能在js项目工程中使用",
"37": "找不到package.json文件,voerkai18n只能在js项目工程中使用",
"38": "语言配置文件{}文件已存在,跳过创建。\\n使用{}可以重新覆盖创建",
"39": "生成语言配置文件:{}",
"40": "拟支持的语言:{}",
"41": "初始化成功,下一步:",
"42": " - 编辑{}确定拟支持的语言种类等参数",
"43": " - 运行<{}>扫描提取要翻译的文本",
"44": " - 运行<{}>编译语言包"
}

View File

@ -1,4 +1,46 @@
module.exports = {
"1": "Supported Languages\t: {}",
"2": "Project Directory"
"1": "Supported languages\\t: {}",
"2": "Default language\\t: {}",
"3": "Active language\\t\\t: {}",
"4": "Namespaces\\t\\t: {}",
"5": "Module type\\t\\t: {}",
"6": "Compile to{}",
"7": "Error while read language of file<{}>:{}",
"8": " - Total {} messages",
"9": " - language messages: {}",
"10": " - idMap file: {}",
"11": " - Formatters:{}",
"12": " - Update formatters:{}",
"13": " - Entry of language: {}",
"14": "Failed to load multilingual configuration file <{}>: {}",
"15": "The destination folder < {} > does not exist",
"16": "Scan for",
"17": "Project directory",
"18": "Initialize project i18n configuration",
"19": "Output debug information",
"20": "Regenerate the language configuration of the current project",
"21": "Generated JS module type, with values of auto, esm, cjs",
"22": "Supported languages",
"23": "Folder of project{}",
"24": "Scan and extract all strings to be translated into the <languages/translations> folder",
"25": "Supported languages",
"26": "Default language",
"27": "Active language",
"28": "Namespaces",
"29": "Exclude folders to scan, multiple separated by commas",
"30": " strategy of messages merge,with value of sync(default),overwrite,merge",
"31": "Type of file to scan",
"32": "The language configuration file <{}> already exists. It will be used preferentially to extract messages",
"33": "Compiles the language messages for project",
"34": "Output module type, values auto, esm, cjs",
"35": "The language messages folder <{}> does not exist",
"36": "Cannot find {} file, {} can only be used in JS project",
"37": "Cannot find <package.json> file, voerkai18n can only be used in JS project",
"38": "Language configuration {} file already exists, skipping creation\\n use {} to overwrite the creation",
"39": "Generate language configuration: {}",
"40": "Languages to be supported{}",
"41": "Initialization succeeded, next step",
"42": " - Edit language parameters in {}",
"43": " - Run <{}> scan to extract the messages to be translated",
"44": " - Run <{}> compile language messages"
}

View File

@ -47,59 +47,83 @@
*
*/
const formatters = {
"*":{ }, // 在所有语言下生效的格式化器
$types:{ } // 在所有语言下只作用于特定数据类型的格式化器
module.exports = {
// 在所有语言下生效的格式化器
"*":{
//[格式化名称]:(value)=>{...},
//[格式化名称]:(value,arg)=>{...},
},
// 在所有语言下只作用于特定数据类型的格式化器
$types:{
},
cn:{
$types:{
"*":{
// 所有类型的默认格式化器
// "*":{
},
Date:{
// },
// Date:{
},
Number:{
// },
// Number:{
},
String:{
// },
// String:{
},
Array:{
// },
// Array:{
},
Object:{
// },
// Object:{
}
// }
}
}
},
en:{
$types:{
"*":{
// 所有类型的默认格式化器
// "*":{
},
Date:{
// },
// Date:{
},
Number:{
// },
// Number:{
},
String:{
// },
// String:{
},
Array:{
// },
// Array:{
},
Object:{
// },
// Object:{
}
// }
}
},
jp:{
$types:{
// 所有类型的默认格式化器
// "*":{
// },
// Date:{
// },
// Number:{
// },
// String:{
// },
// Array:{
// },
// Object:{
// }
}
}
}
module.module.module.module.module.module.module.module.module.module.module.module.module.module.module.module.module.module.module.module.module.module.module.module.module.module.module.exports.s.s.s.s.s.s.s.s.s.s.s.s.s.s.s.s.s.s.s.s.s.s.s.s.s.s = formatters
}

View File

@ -1,4 +1,46 @@
module.exports = {
"支持的语言\t: {}": 1,
"工程项目所在目录": 2
"支持的语言\\t: {}": 1,
"默认语言\\t: {}": 2,
"激活语言\\t: {}": 3,
"名称空间\\t: {}": 4,
"模块类型\\t: {}": 5,
"编译结果输出至:{}": 6,
"读取语言文件{}失败:{}": 7,
" - 共合成{}条语言包文本": 8,
" - 语言包文件: {}": 9,
" - idMap文件: {}": 10,
" - 格式化器:{}": 11,
" - 更新格式化器:{}": 12,
" - 访问入口文件: {}": 13,
"加载多语言配置文件<{}>失败: {} ": 14,
"目标文件夹<{}>不存在": 15,
"扫描提取范围:": 16,
"工程项目所在目录": 17,
"初始化项目国际化配置": 18,
"输出调试信息": 19,
"重新生成当前项目的语言配置": 20,
"生成的js模块类型,取值auto,esm,cjs": 21,
"支持的语言列表": 22,
"工程目录:{}": 23,
"扫描并提取所有待翻译的字符串到<languages/translates>文件夹中": 24,
"支持的语言": 25,
"默认语言": 26,
"激活语言": 27,
"翻译名称空间": 28,
"排除要扫描的文件夹,多个用逗号分隔": 29,
"本次提取内容与已存在内容的数据合并策略,默认取值sync=同步,overwrite=覆盖,merge=合并": 30,
"要扫描的文件类型": 31,
"语言配置文件<{}>已存在,将优先使用此配置文件中参数来提取文本": 32,
"编译指定项目的语言包": 33,
"输出模块类型,取值auto,esm,cjs": 34,
"语言包文件夹<{}>不存在": 35,
"找不到{}文件,{}只能在js项目工程中使用": 36,
"找不到package.json文件,voerkai18n只能在js项目工程中使用": 37,
"语言配置文件{}文件已存在,跳过创建。\\n使用{}可以重新覆盖创建": 38,
"生成语言配置文件:{}": 39,
"拟支持的语言:{}": 40,
"初始化成功,下一步:": 41,
" - 编辑{}确定拟支持的语言种类等参数": 42,
" - 运行<{}>扫描提取要翻译的文本": 43,
" - 运行<{}>编译语言包": 44
}

View File

@ -1,57 +1,28 @@
const messageIds = require("./idMap")
const { translate,I18nManager } = require("@voerkai18n/runtime")
const defaultMessages = require("./cn.js")
const { translate,I18nManager,i18nScope } = require("@voerkai18n/runtime")
const scopeSettings = require("./settings.js")
const formatters = require("./formatters.js")
const defaultMessages = require("./cn.js")
const activeMessages = defaultMessages
// 自动创建全局VoerkaI18n实例
if(!globalThis.VoerkaI18n){
globalThis.VoerkaI18n = new I18nManager(scopeSettings)
}
let scope = {
defaultLanguage: "cn", // 默认语言名称
// 语言作用域
const scope = new i18nScope({
...scopeSettings, // languages,defaultLanguage,activeLanguage,namespaces,formatters
id: "@voerkai18n/tools", // 当前作用域的id自动取当前工程的package.json的name
default: defaultMessages, // 默认语言包
messages : defaultMessages, // 当前语言包
messages : activeMessages, // 当前语言包
idMap:messageIds, // 消息id映射列表
formatters:{}, // 当前作用域的格式化函数列表
loaders:{}, // 异步加载语言文件的函数列表
global:{}, // 引用全局VoerkaI18n配置注册后自动引用
// 主要用来缓存格式化器的引用,当使用格式化器时可以直接引用,避免检索
$cache:{
activeLanguage:null,
typedFormatters:{},
formatters:{},
formatters, // 当前作用域的格式化函数列表
loaders:{
"en" : ()=>import("./en.js")
}
}
let supportedlanguages = {}
scope.loaders["en"] = ()=>import("./en.js")
})
// 翻译函数
const t = translate.bind(scope)
const languages = [
{
"name": "cn",
"title": "中文"
},
{
"name": "en",
"title": "英文"
}
]
// 注册当前作用域到全局VoerkaI18n实例
VoerkaI18n.register(scope)
module.exports.languages = languages
module.exports.scope = scope
module.exports.t = t
module.exports.changeLanguage = VoerkaI18n.change.bind(VoerkaI18n)
module.exports.addLanguageListener = VoerkaI18n.on.bind(VoerkaI18n)
module.exports.removeLanguageListener = VoerkaI18n.off.bind(VoerkaI18n)
module.exports.scope = scope
module.exports.i18nManager = VoerkaI18n

View File

@ -1,21 +1,270 @@
{
"工程项目所在目录" : {
"en" : "Project Directory",
"$file": [
"index.js"
]
},
"支持的语言\\t: {}": {
"en" : "支持的语言\\t: {}",
"en": "Supported languages\\t: {}",
"$file": [
"compile.command.js",
"extract.plugin.js"
]
},
"保存{}\\n" : {
"en" : "保存{}\\n",
"默认语言\\t: {}": {
"en": "Default language\\t: {}",
"$file": [
"stringify.js"
"compile.command.js",
"extract.plugin.js"
]
},
"激活语言\\t: {}": {
"en": "Active language\\t\\t: {}",
"$file": [
"compile.command.js",
"extract.plugin.js"
]
},
"名称空间\\t: {}": {
"en": "Namespaces\\t\\t: {}",
"$file": [
"compile.command.js",
"extract.plugin.js"
]
},
"模块类型\\t: {}": {
"en": "Module type\\t\\t: {}",
"$file": [
"compile.command.js"
]
},
"编译结果输出至:{}": {
"en": "Compile to{}",
"$file": [
"compile.command.js"
]
},
"读取语言文件{}失败:{}": {
"en": "Error while read language of file<{}>:{}",
"$file": [
"compile.command.js"
]
},
" - 共合成{}条语言包文本": {
"en": " - Total {} messages",
"$file": [
"compile.command.js"
]
},
" - 语言包文件: {}": {
"en": " - language messages: {}",
"$file": [
"compile.command.js"
]
},
" - idMap文件: {}": {
"en": " - idMap file: {}",
"$file": [
"compile.command.js"
]
},
" - 格式化器:{}": {
"en": " - Formatters:{}",
"$file": [
"compile.command.js"
]
},
" - 更新格式化器:{}": {
"en": " - Update formatters:{}",
"$file": [
"compile.command.js"
]
},
" - 访问入口文件: {}": {
"en": " - Entry of language: {}",
"$file": [
"compile.command.js"
]
},
"加载多语言配置文件<{}>失败: {} ": {
"en": "Failed to load multilingual configuration file <{}>: {}",
"$file": [
"compile.command.js"
]
},
"目标文件夹<{}>不存在": {
"en": "The destination folder < {} > does not exist",
"$file": [
"extract.command.js"
]
},
"扫描提取范围:": {
"en": "Scan for",
"$file": [
"extract.command.js"
]
},
"工程项目所在目录": {
"en": "Project directory",
"$file": [
"index.js"
]
},
"初始化项目国际化配置": {
"en": "Initialize project i18n configuration",
"$file": [
"index.js"
]
},
"输出调试信息": {
"en": "Output debug information",
"$file": [
"index.js"
]
},
"重新生成当前项目的语言配置": {
"en": "Regenerate the language configuration of the current project",
"$file": [
"index.js"
]
},
"生成的js模块类型,取值auto,esm,cjs": {
"en": "Generated JS module type, with values of auto, esm, cjs",
"$file": [
"index.js"
]
},
"支持的语言列表": {
"en": "Supported languages",
"$file": [
"index.js"
]
},
"工程目录:{}": {
"en": "Folder of project{}",
"$file": [
"index.js"
]
},
"扫描并提取所有待翻译的字符串到<languages/translates>文件夹中": {
"en": "Scan and extract all strings to be translated into the <languages/translations> folder",
"$file": [
"index.js"
]
},
"支持的语言": {
"en": "Supported languages",
"$file": [
"index.js"
]
},
"默认语言": {
"en": "Default language",
"$file": [
"index.js"
]
},
"激活语言": {
"en": "Active language",
"$file": [
"index.js"
]
},
"翻译名称空间": {
"en": "Namespaces",
"$file": [
"index.js"
]
},
"排除要扫描的文件夹,多个用逗号分隔": {
"en": "Exclude folders to scan, multiple separated by commas",
"$file": [
"index.js"
]
},
"本次提取内容与已存在内容的数据合并策略,默认取值sync=同步,overwrite=覆盖,merge=合并": {
"en": " strategy of messages merge,with value of sync(default),overwrite,merge",
"$file": [
"index.js"
]
},
"要扫描的文件类型": {
"en": "Type of file to scan",
"$file": [
"index.js"
]
},
"语言配置文件<{}>已存在,将优先使用此配置文件中参数来提取文本": {
"en": "The language configuration file <{}> already exists. It will be used preferentially to extract messages",
"$file": [
"index.js"
]
},
"编译指定项目的语言包": {
"en": "Compiles the language messages for project",
"$file": [
"index.js"
]
},
"输出模块类型,取值auto,esm,cjs": {
"en": "Output module type, values auto, esm, cjs",
"$file": [
"index.js"
]
},
"语言包文件夹<{}>不存在": {
"en": "The language messages folder <{}> does not exist",
"$file": [
"index.js"
]
},
"找不到{}文件,{}只能在js项目工程中使用": {
"en": "Cannot find {} file, {} can only be used in JS project",
"$file": [
"init.command.js"
]
},
"找不到package.json文件,voerkai18n只能在js项目工程中使用": {
"en": "Cannot find <package.json> file, voerkai18n can only be used in JS project",
"$file": [
"init.command.js"
]
},
"语言配置文件{}文件已存在,跳过创建。\\n使用{}可以重新覆盖创建": {
"en": "Language configuration {} file already exists, skipping creation\\n use {} to overwrite the creation",
"$file": [
"init.command.js"
]
},
"生成语言配置文件:{}": {
"en": "Generate language configuration: {}",
"$file": [
"init.command.js"
]
},
"拟支持的语言:{}": {
"en": "Languages to be supported{}",
"$file": [
"init.command.js"
]
},
"初始化成功,下一步:": {
"en": "Initialization succeeded, next step",
"$file": [
"init.command.js"
]
},
" - 编辑{}确定拟支持的语言种类等参数": {
"en": " - Edit language parameters in {}",
"$file": [
"init.command.js"
]
},
" - 运行<{}>扫描提取要翻译的文本": {
"en": " - Run <{}> scan to extract the messages to be translated",
"$file": [
"init.command.js"
]
},
" - 运行<{}>编译语言包": {
"en": " - Run <{}> compile language messages",
"$file": [
"init.command.js"
]
}
}

View File

@ -6,7 +6,8 @@
"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": "node ./index.js compile -d",
"compile:en": "cross-env LANGUAGE=en node ./index.js compile -d"
},
"author": "",
"bin": {
@ -20,12 +21,15 @@
"art-template": "^4.13.2",
"commander": "^9.0.0",
"deepmerge": "^4.2.2",
"dotenv": "^16.0.0",
"glob": "^7.2.0",
"gulp": "^4.0.2",
"logsets": "^1.0.8",
"readjson": "^2.2.2",
"through2": "^4.0.2",
"vinyl": "^2.2.1"
"vinyl": "^2.2.1",
"cross-env": "^7.0.3"
},
"devDependencies": {
}
}

View File

@ -40,7 +40,7 @@ const { isPlainObject } = require("./utils")
* @returns
*/
function escape(str){
return str.replaceAll('\\','\\\\')
return str.replaceAll(/\\\\(?![trnbvf'"]{1})/g,"\\\\")
.replaceAll("\t","\\t")
.replaceAll("\n","\\n")
.replaceAll("\b","\\b")
@ -48,9 +48,22 @@ const { isPlainObject } = require("./utils")
.replaceAll("\f","\\f")
.replaceAll("\'","\\'")
.replaceAll('\"','\\"')
.replaceAll('\v','\\v')
.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("\\\\",'\\')
}
/**
* 获取字符串的长度中文算两个字符
* @param {*} s
@ -67,7 +80,7 @@ function getStringWidth(s){
return realLength;
}
function objectToString(obj,{indent=4,alignKey=true}={}){
function objectStringify(obj,{indent=4,alignKey=true}={}){
function nodeToString(node,level,last=false){
let results = [],beginChar = "",endChar = ""
level++
@ -136,6 +149,10 @@ function objectToString(obj,{indent=4,alignKey=true}={}){
}
return nodeToString(obj,0,true)
}
module.exports = objectToString
module.exports = {
objectStringify,
escape,
escape2,
unescape
}

View File

@ -1,55 +1,42 @@
{{if moduleType === "esm"}}
import messageIds from "./idMap.js"
import { translate,I18nManager } from "@voerkai18n/runtime"
import defaultMessages from "./{{defaultLanguage}}.js"
import { translate,I18nManager,i18nScope } from "@voerkai18n/runtime"
import scopeSettings from "./settings.js"
import formatters from "./formatters.js"
import defaultMessages from "./{{defaultLanguage}}.js"
{{if defaultLanguage === activeLanguage}}const activeMessages = defaultMessages
{{else}}import activeMessages from "./{{activeLanguage}}.js"{{/if}}
{{else}}
const messageIds = require("./idMap")
const { translate,I18nManager } = require("@voerkai18n/runtime")
const defaultMessages = require("./{{defaultLanguage}}.js")
const { translate,I18nManager,i18nScope } = require("@voerkai18n/runtime")
const scopeSettings = require("./settings.js")
const formatters = require("./formatters.js")
const defaultMessages = require("./{{defaultLanguage}}.js")
{{if defaultLanguage === activeLanguage}}const activeMessages = defaultMessages
{{else}}const activeMessages = require("./{{activeLanguage}}.js"){{/if}}
{{/if}}
// 自动创建全局VoerkaI18n实例
if(!globalThis.VoerkaI18n){
globalThis.VoerkaI18n = new I18nManager(scopeSettings)
}
let scope = {
defaultLanguage: "{{defaultLanguage}}", // 默认语言名称
// 语言作用域
const scope = new i18nScope({
...scopeSettings, // languages,defaultLanguage,activeLanguage,namespaces,formatters
id: "{{scopeId}}", // 当前作用域的id自动取当前工程的package.json的name
default: defaultMessages, // 默认语言包
messages : defaultMessages, // 当前语言包
messages : activeMessages, // 当前语言包
idMap:messageIds, // 消息id映射列表
formatters:{}, // 当前作用域的格式化函数列表
loaders:{}, // 异步加载语言文件的函数列表
global:{}, // 引用全局VoerkaI18n配置注册后自动引用
// 主要用来缓存格式化器的引用,当使用格式化器时可以直接引用,避免检索
$cache:{
activeLanguage:null,
typedFormatters:{},
formatters:{},
formatters, // 当前作用域的格式化函数列表
loaders:{ {{each languages}}{{if $value.name !== defaultLanguage}}
{{if $value.name == activeLanguage}}"{{$value.name}}" : ()=>activeMessages{{else}}"{{$value.name}}" : ()=>import("./{{$value.name}}.js"){{/if}}{{if $index !== languages.length - 1}},{{/if}}{{/if}}{{/each}}
}
}
let supportedlanguages = {}
{{each languages}}{{if $value.name !== defaultLanguage}}
scope.loaders["{{$value.name}}"] = ()=>import("./{{$value.name}}.js")
{{/if}}{{/each}}
})
// 翻译函数
const t = translate.bind(scope)
const languages = {{@ JSON.stringify(languages,null,4) }}
// 注册当前作用域到全局VoerkaI18n实例
VoerkaI18n.register(scope)
{{if moduleType === "esm"}}
export { t, languages,scope,i18nManager:VoerkaI18n, changeLanguage:VoerkaI18n.change.bind(VoerkaI18n),addLanguageListener:VoerkaI18n.on.bind(VoerkaI18n),removeLanguageListener:VoerkaI18n.off.bind(VoerkaI18n) }
export {
t,
scope,
i18nManager:VoerkaI18n,
}
{{else}}
module.exports.languages = languages
module.exports.scope = scope
module.exports.t = t
module.exports.changeLanguage = VoerkaI18n.change.bind(VoerkaI18n)
module.exports.addLanguageListener = VoerkaI18n.on.bind(VoerkaI18n)
module.exports.removeLanguageListener = VoerkaI18n.off.bind(VoerkaI18n)
module.exports.scope = scope
module.exports.i18nManager = VoerkaI18n
{{/if}}

View File

@ -47,38 +47,37 @@
*
*/
const formatters = {
"*":{ }, // 在所有语言下生效的格式化器
$types:{ } // 在所有语言下只作用于特定数据类型的格式化器
{{each languages}}
{{$value.name}}:{
{{if moduleType === "esm"}}export default {{else}}module.exports = {{/if}}{
// 在所有语言下生效的格式化器
"*":{
//[格式化名称]:(value)=>{...},
//[格式化名称]:(value,arg)=>{...},
},
// 在所有语言下只作用于特定数据类型的格式化器
$types:{
},
{{each languages}} {{$value.name}}:{
$types:{
"*":{
// 所有类型的默认格式化器
// "*":{
},
Date:{
// },
// Date:{
},
Number:{
// },
// Number:{
},
String:{
// },
// String:{
},
Array:{
// },
// Array:{
},
Object:{
// },
// Object:{
}
// }
}
}
{{/each}}
}
{{if moduleType === "esm"}}
export default formatters
{{else}}
module.exports = formatters
{{/if}}
}{{if $index !== languages.length - 1}},{{/if}}
{{/each}}}

View File

@ -28,6 +28,31 @@ async function importModule(url){
}
}
/**
* 读取指定文件夹的package.json文件如果当前文件夹没有package.json文件则向上查找
* @param {*} folder
* @param {*} exclueSelf =true 排除folder从folder的父级开始查找
* @returns
*/
function getCurrentPackageJson(folder,exclueSelf=true){
try{
let pkgPath =exclueSelf ?
path.join(folder, "..", "package.json")
: path.join(folder, "package.json")
if(fs.existsSync(pkgPath)){
let pkg = readJson.sync(pkgPath)
return pkg
}
let parent = path.dirname(folder)
if(parent===folder) return null
return getCurrentPackageJson(parent,false)
}catch(e){
return null
}
}
function createPackageJsonFile(targetPath,moduleType="auto"){
if(moduleType==="auto"){
moduleType = findModuleType(targetPath)
@ -111,12 +136,39 @@ function createJsModuleFile(filename,defaultExports={},namedExports={},moduleTyp
}
}
function escape(str){
return str
.replaceAll("\\t","\t")
.replaceAll("\\n","\n")
.replaceAll("\\b","\b")
.replaceAll("\\r","\r")
.replaceAll("\\f","\f")
.replaceAll("\\'","\'")
.replaceAll('\\"','\"')
.replaceAll('\\v','\v')
.replaceAll("\\\\",'\\')
}
// 翻译函数
// @voerkai18n/tools工程本身使用了voerkai18n,即@voerkai18n/tools的extract和compile依赖于其自己生成的languages运行时
// 这样产生了鸡蛋问题因此在extract与compile调试阶段如果t函数无法使用(即编译的languages无法正常使用)则需要提供t函数
// 此函数的目的是提供一种容错方式
let t
try{
t = require("./languages").t
}catch(e){
console.warn(e.stack)
t = v=>v
}
module.exports = {
importModule,
findModuleType,
createPackageJsonFile,
isPlainObject
isPlainObject,
getCurrentPackageJson,
escape,
t
}

20
pnpm-lock.yaml generated
View File

@ -48,7 +48,7 @@ importers:
packages/demo/apps/app:
specifiers:
'@voerkai18n/tools': workspace:^1.0.0
dependencies:
devDependencies:
'@voerkai18n/tools': link:../../../tools
packages/demo/apps/app/languages:
@ -91,8 +91,8 @@ importers:
'@voerkai18n/runtime': workspace:^1.0.0
art-template: ^4.13.2
commander: ^9.0.0
cross-env: ^7.0.3
deepmerge: ^4.2.2
dotenv: ^16.0.0
glob: ^7.2.0
gulp: ^4.0.2
logsets: ^1.0.8
@ -106,13 +106,14 @@ importers:
art-template: 4.13.2
commander: 9.0.0
deepmerge: 4.2.2
dotenv: 16.0.0
glob: 7.2.0
gulp: 4.0.2
logsets: 1.0.8
readjson: 2.2.2
through2: 4.0.2
vinyl: 2.2.1
devDependencies:
cross-env: 7.0.3
packages/vue:
specifiers:
@ -2426,6 +2427,14 @@ packages:
/core-util-is/1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
/cross-env/7.0.3:
resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
hasBin: true
dependencies:
cross-spawn: 7.0.3
dev: true
/cross-spawn/7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
@ -2569,11 +2578,6 @@ packages:
webidl-conversions: 5.0.0
dev: true
/dotenv/16.0.0:
resolution: {integrity: sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==}
engines: {node: '>=12'}
dev: false
/duplexify/3.7.1:
resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==}
dependencies:

1045
readme.md

File diff suppressed because it is too large Load Diff