This commit is contained in:
wxzhang 2022-08-16 18:46:34 +08:00
parent ba9f9bfd49
commit 8ae7b68083
15 changed files with 204 additions and 186 deletions

View File

@ -6,7 +6,7 @@
import { t } from "./languages"
```
由于默认情况下,`voerkai18 compile`命令会在当前工程的`/languages`文件夹下,这样我们为了导入`t`翻译函数不得不使用各种相对引用,这即容易出错,又不美观,如下:
由于默认情况下,`t`函数位于当前工程的`/languages`文件夹下,这样我们为了导入`t`翻译函数不得不使用各种相对引用,这即容易出错,又不美观,如下:
```javascript | pure
import { t } from "./languages"
@ -17,9 +17,9 @@ import { t } from "../../../languages"
作为国际化解决方案,一般工程的大部份源码中均会使用到翻译函数,这种使用体验比较差。
为此,我们提供了几个插件可以来自动完成翻译函数的自动导入,包括:
为此,我们提供了插件可以来自动完成翻译函数的自动导入,包括:
- `babel`插件
- `vite`插件
- (`babel`插件)[../tools/babel]
- (`vite`插件)[../tools/vite]
当启用了`babel/vite`插件后,就会在编译时自动导入`t`函数。关于插件如何使用请参阅文档。

View File

@ -20,7 +20,7 @@
```javascript | pure
//idMap.js
{
"1":"中华人民共和国万岁"
"中华人民共和国万岁":"1"
}
// en.js
{
@ -32,13 +32,13 @@
}
```
如此,就消除了在`en.js``jp.js`文件中的冗余。但是在源代码文件中还存在`t("中华人民共和国万岁")`,整个运行环境中存在两份副本,一份在源代码文件中,一份在`idMap.js`中。
如此,就消除了在`en.js``jp.js`文件中的冗余。但是在源代码文件中还存在`t("中华人民共和国万岁")`,整个运行环境中存在两份副本,一份在`源代码`文件中,一份在`idMap.js`中。
为了进一步减少重复内容,因此,我们需要将源代码文件中的`t("中华人民共和国万岁")`更改为`t("1")`,这样就能确保无重复冗余。但是,很显然,我们不可能手动来更改源代码文件,这就需要由`voerkai18n`提供的一个编译期插件来做这一件事了。
为了进一步减少冗余内容,因此,我们需要将源代码文件中的`t("中华人民共和国万岁")`更改为`t("1")`,这样就能确保无重复冗余。但是,很显然,我们不可能手动来更改源代码文件,这就需要由`voerkai18n`提供的一个编译期插件来做这一件事了。
`babel-plugin-voerkai18n`插件为例,该插件同时还完成一任务,就是自动读取`voerkai18n compile`生成的`idMap.js`文件,然后将`t("中华人民共和国万岁")`自动更改为`t("1")`,这样就完全消除了重复冗余信息。
`babel-plugin-voerkai18n`插件为例,该插件同时还完成一任务,就是自动读取`voerkai18n compile`生成的`idMap.js`文件,然后将`t("中华人民共和国万岁")`自动更改为`t("1")`,这样就完全消除了重复冗余信息。
所以在最终形成的代码中实际上每一个t函数均是`t("1")``t("2")``t("3")``...``t("n")`的形式,最终代码还是采用了用`key`来进行转换,只不过这个过程是自动完成的而已。
所以,在最终形成编译后的代码中实际上每一个t函数均是`t("1")``t("2")``t("3")``...``t("n")`的形式,最终代码还是采用了用`key`来进行转换,只不过这个过程是自动完成的而已。
**注意:**

View File

@ -2,7 +2,7 @@
| 包| 版本号| 最后更新|说明|
| --- | :---:| --- |---|
|**@voerkai18n/utils**|1.0.12|2022/08/05|公共工具库
|**@voerkai18n/runtime**|1.0.28|2022/08/07|核心运行时
|**@voerkai18n/runtime**|1.0.29|2022/08/16|核心运行时
|**@voerkai18n/formatters**|1.0.6|2022/04/15|格式化器,提供对要翻译文本的转换功能
|**@voerkai18n/react**|1.0.4|2022/04/16|React支持,提供语言切换等功能
|**@voerkai18n/cli**|1.0.33|2022/08/07|命令行工具,用来初始化/提取/编译/自动翻译等工具链

View File

@ -55,7 +55,7 @@ this.i18n = {
注入`i18n`实例后就可以在此基础上实现`激活语言``默认语言``切换语言`等功能。
```vue
```javascript | pure
<script>
import {reactive } from 'vue'
export default {

View File

@ -4,7 +4,7 @@
假设一个大型项目,其中源代码文件有上千个。默认情况下,`voerkai18n extract`会扫描所有源码文件将需要翻译的文本提取到`languages/translates/default.json`文件中。由于文件太多会导致以下问题:
- 内容太多导致`default.json`文件太大,利于管理
- 内容太多导致`default.json`文件太大,利于管理
- 有些翻译往往需要联系上下文才可以作出更准确的翻译,没有适当分类,不容易联系上下文。
因此,引入`名称空间`就是目的就是为了解决此问题。

View File

@ -51,7 +51,7 @@ import { t } from "../../../languages"
导入`t`函数后就可以直接使用了。
```vue
```javascript | pure
<Script setup>
// 如果没有在vite.config.js中配置`@voerkai18n/vite`插件则需要手工导入t函数
// import { t } from "./languages"
@ -101,17 +101,17 @@ export default {
引入`@voerkai18n/vue`插件来实现切换语言和自动重新渲染的功能。
```javascript | pure
import { createApp } from 'vue'
import Root from './App.vue'
import i18nPlugin from '@voerkai18n/vue'
import { i18nScope } from './languages'
const app = createApp(Root)
app.use(i18nPlugin,{ i18nScope }) // 重点需要引入i18nScope
app.mount('#app')
import { createApp } from 'vue'
import Root from './App.vue'
import i18nPlugin from '@voerkai18n/vue'
import { i18nScope } from './languages'
const app = createApp(Root)
app.use(i18nPlugin,{ i18nScope }) // 重点需要引入i18nScope
app.mount('#app')
```
`@voerkai18n/vue`插件安装后,提供了一个`i18n`实例,可以在组件中进行`inject`。就可以按如下方式使用:
```vue
```javascript | pure
<script>
export default {
inject: ['i18n'] // 此值由`@voerkai18n/vue`插件提供

View File

@ -55,12 +55,12 @@ const { toNumber,isFunction } = require("../utils")
* @param {*} value
* @param {String} escapeValue
* @paran {String} next 下一步行为取值true/false,break,skip,默认是break
* @param {*} options
* @param {*} config
*/
function empty(value,escapeValue,next,options) {
function empty(value,escapeValue,next,config) {
if(next===false) next = 'break'
if(next===true) next = 'skip'
let opts = Object.assign({escape:"",next:'break',values:[]},options.empty || {})
let opts = Object.assign({escape:"",next:'break',values:[]},config.empty || {})
if(escapeValue!=undefined) opts.escape = escapeValue
let emptyValues = [undefined,null]
if(Array.isArray(opts.values)) emptyValues.push(...opts.values)
@ -86,15 +86,15 @@ empty.paramCount = 2
* @param {*} value
* @param {*} escapeValue
* @param {*} next 下一步的行为取值break,ignore
* @param {*} options 格式化器的全局配置参数
* @param {*} config 格式化器的全局配置参数
* @returns
*/
function error(value,escapeValue,next,options) {
function error(value,escapeValue,next,config) {
if(value instanceof Error){
if(scope.debug) console.error(`Error while execute formatter<${value.formatter}>:`,e)
const scope = this
try{
let opts = Object.assign({escape:null,next:'break'},options.error || {})
let opts = Object.assign({escape:null,next:'break'},config.error || {})
if(escapeValue!=undefined) opts.escape = escapeValue
if(next!=undefined) opts.next = next
return {

View File

@ -7,7 +7,7 @@
// 日期格式化器
// format取字符串"long","short","local","iso","gmt","utc"或者日期模块字符串
// { value | date } == '2022/8/15'
// { value | date } == '2022/8/15' 默认
// { value | date('long') } == '2022/8/15 12:08:32'
// { value | date('short') } == '8/15'
// { value | date('GMT') } == 'Mon, 15 Aug 2022 06:39:38 GMT'
@ -15,6 +15,7 @@
// { value | date('YYYY-MM-DD HH:mm:ss') } == '2022-8-15 12:08:32'
const dateFormatter = Formatter((value,format,$config)=>{
const optionals = ["long","short","local","iso","gmt","utc"]
// 处理参数支持大小写和数字0-long,1-short,2-local,3-iso,4-gmt,5-utc
const optionIndex = optionals.findIndex((v,i)=>{
if(typeof(format)=="string"){
return v==format || v== format.toUpperCase()
@ -132,7 +133,7 @@ module.exports = {
date :{
long : 'YYYY/MM/DD HH:mm:ss',
short : "MM/DD",
format : "local"
format : "long"
},
quarter : {
names : ["Q1","Q2","Q3","Q4"],
@ -183,7 +184,8 @@ module.exports = {
},
// 默认数据类型的格式化器
$types: {
Date : value => { const d = toDate(value); return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()} ${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}` },
Date : dateFormatter,
//value => { const d = toDate(value); return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()} ${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}` },
Null : value =>"",
Undefined: value =>"",
Error : value => "ERROR",

View File

@ -14,7 +14,7 @@ module.exports = {
date :{
long : 'YYYY年MM月DD日 HH点mm分ss秒',
short : "MM/DD",
format : 'YYYY年MM月DD日 HH点mm分ss秒'
format : 'long'
},
quarter : {
names : ["一季度","二季度","三季度","四季度"],
@ -50,12 +50,10 @@ module.exports = {
}
},
$types: {
Date: value => {const d = toDate(value);return `${d.getFullYear()}${d.getMonth() + 1}${d.getDate()}${d.getHours()}${d.getMinutes()}${d.getSeconds()}`},
Boolean : value =>value ? "是":"否"
},
// 中文货币big=true代表大写形式
capitalizeCurrency:(value,big,unit="元",prefix,suffix)=>toChineseCurrency(value,{big,prefix,suffix,unit}),
// 中文货币big=true代表大写形式
rmb : (value,big,unit="元",prefix,suffix)=>toChineseCurrency(value,{big,prefix,suffix,unit}),
// 中文数字,如一千二百三十一
number:(value,isBig)=>toChineseNumber(value,isBig)
number :(value,isBig)=>toChineseNumber(value,isBig)
}

View File

@ -1,4 +1,4 @@
const {createFormatter,getDataTypeName,isNumber,isPlainObject,deepMerge,isFunction,isNothing,deepMixin,replaceAll} = require("./utils")
const {createFormatter,Formatter,getDataTypeName,isNumber,isPlainObject,deepMerge,isFunction,isNothing,deepMixin,replaceAll} = require("./utils")
const EventEmitter = require("./eventemitter")
const inlineFormatters = require("./formatters")
const i18nScope = require("./scope")
@ -153,7 +153,7 @@ function forEachInterpolatedVars(str,callback,options={}){
if(opts.replaceAll){ // 在某此版本上可能没有
result=result.replaceAll(match[0],finalValue)
}else{
result=replaceAll(result,match[0],finalValue)
result=result.replace(match[0],finalValue)
}
}catch{// callback函数可能会抛出异常如果抛出异常则中断匹配过程
break
@ -229,23 +229,18 @@ function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){
}else{// 当语言切换时清空缓存
resetScopeCache(scope,activeLanguage)
}
const fallbackLanguage = scope.getLanguage(activeLanguage).fallback;
// 先在当前作用域中查找,再在全局查找
const targets = [scope.formatters,scope.global.formatters]
const targets = [
scope.activeFormatters,
scope.formatters[fallbackLanguage], // 如果指定了回退语言时,也在该回退语言中查找
scope.global.formatters[activeLanguage],
scope.global.formatters["*"]
]
for(const target of targets){
if(!target) continue
// 1. 在全局$types中查找
if(("*" in target) && isPlainObject(target["*"].$types)){
let formatters = target["*"].$types
if(dataType in formatters && isFunction(formatters[dataType])){
return scope.$cache.typedFormatters[dataType] = formatters[dataType]
}
}
// 2. 当前语言的$types中查找
if((activeLanguage in target) && isPlainObject(target[activeLanguage].$types)){
let formatters = target[activeLanguage].$types
if(dataType in formatters && isFunction(formatters[dataType])){
return scope.$cache.typedFormatters[dataType] = formatters[dataType]
}
if(isPlainObject(target.$types) && isFunction(target.$types[dataType])){
return scope.$cache.typedFormatters[dataType] = target.$types[dataType]
}
}
}
@ -285,7 +280,16 @@ function getFormatter(scope,activeLanguage,name){
}
}
}
/**
* Checker是一种特殊的格式化器会在特定的时间执行
*
* Checker应该返回{value,next}用来决定如何执行下一个格式化器函数
*
*
* @param {*} checker
* @param {*} value
* @returns
*/
function executeChecker(checker,value){
let result ={ value, next:"skip"}
if(!isFunction(checker)) return result
@ -341,7 +345,7 @@ function executeFormatter(value,formatters,scope,template){
// 3. 分别执行格式化器函数
for(let formatter of formatters){
try{
result = formatter(result)
result = formatter(result,scope.activeFormatterConfig)
}catch(e){
e.formatter = formatter.$name
if(scope.debug) console.error(`Error while execute i18n formatter<${formatter.$name}> for ${template}: ${e.message} ` )
@ -361,6 +365,9 @@ function executeFormatter(value,formatters,scope,template){
/**
* 添加默认的empty和error格式化器用来提供默认的空值和错误处理逻辑
*
* empty和error格式化器有且只能有一个其他无效
*
* @param {*} formatters
*/
function addDefaultFormatters(formatters){
@ -376,8 +383,13 @@ function addDefaultFormatters(formatters){
/**
*
* [[格式化器名称,[参数,参数,...]][格式化器名称,[参数,参数,...]]]格式化器包装转化为
* 格式化器的调用函数链
* 经parseFormatters解析t('{}')中的插值表达式中的格式化器后会得到
* [[<格式化器名称>,[参数,参数,...]][<格式化器名称>,[参数,参数,...]]]数组
*
* 本函数将之传换为转化为调用函数链形式如下
* [(v)=>{...},(v)=>{...},(v)=>{...}]
*
* 并且会自动将当前激活语言的格式化器配置作为最后一个参数配置传入,这样格式化器函数就可以读取
*
* @param {*} scope
* @param {*} activeLanguage
@ -387,31 +399,23 @@ function addDefaultFormatters(formatters){
*/
function wrapperFormatters(scope,activeLanguage,formatters){
let wrappedFormatters = []
addDefaultFormatters(formatters)
for(let [name,args] of formatters){
if(name){
const func = getFormatter(scope,activeLanguage,name)
if(isFunction(func)){
const fn = (value) => {
if(func.configurable){ // 如果格式化器函数是使用createFormatter创建的
return func.call(scope,value,...args,scope.activeFormatterConfig)
}else{ // 不可配置的格式化器不会传入格式化器配置
return func.call(scope,value,...args)
}
}
fn.$name = name
wrappedFormatters.push(fn)
}else{
// 格式化器无效或者没有定义时,查看当前值是否具有同名的原型方法,如果有则执行调用
// 比如padStart格式化器是String的原型方法不需要配置就可以直接作为格式化器调用
wrappedFormatters.push((value)=>{
let fn = getFormatter(scope,activeLanguage,name)
// 格式化器无效或者没有定义时,查看当前值是否具有同名的原型方法,如果有则执行调用
// 比如padStart格式化器是String的原型方法不需要配置就可以直接作为格式化器调用
if(!isFunction(fn)){
fn = (value,...args) =>{
if(isFunction(value[name])){
// 最后一个参数是当前作用域的格式化器配置参数
return value[name](value,...args)
return value[name](...args)
}else{
return value
}
})
}
}
fn.$name = name
wrappedFormatters.push(fn)
}
}
return wrappedFormatters
@ -426,10 +430,11 @@ function wrapperFormatters(scope,activeLanguage,formatters){
* @returns
*/
function getFormattedValue(scope,activeLanguage,formatters,value,template){
// 1. 取得格式化器函数列表
// 1. 取得格式化器函数列表,然后经过包装以传入当前格式化器的配置参数
const formatterFuncs = wrapperFormatters(scope,activeLanguage,formatters)
// 3. 执行格式化器
if(formatterFuncs.length==0){
// EMPTY和ERROR是默认两个格式化器如果只有两个则说明在t(...)中没有指定格式化器
if(formatterFuncs.length==2){
// 当没有格式化器时,查询是否指定了默认数据类型的格式化器,如果有则执行
const defaultFormatter = getDataTypeDefaultFormatter(scope,activeLanguage,getDataTypeName(value))
if(defaultFormatter){
@ -661,14 +666,13 @@ function translate(message) {
/**
* 切换语言
*/
async change(value){
value=value.trim()
if(this.languages.findIndex(lang=>lang.name === value)!==-1 || isFunction(this._defaultMessageLoader)){
await this._refreshScopes(value) // 通知所有作用域刷新到对应的语言包
this._settings.activeLanguage = value
await this.emit(value) // 触发语言切换事件
async change(language){
if(this.languages.findIndex(lang=>lang.name === language)!==-1 || isFunction(this._defaultMessageLoader)){
await this._refreshScopes(language) // 通知所有作用域刷新到对应的语言包
this._settings.activeLanguage = language
await this.emit(language) // 触发语言切换事件
}else{
throw new Error("Not supported language:"+value)
throw new Error("Not supported language:"+language)
}
}
/**
@ -681,7 +685,7 @@ function translate(message) {
return scope.refresh(newLanguage)
})
if(Promise.allSettled){
await Promise.allSettled(scopeRefreshers)
await Promise.allSettled(scopeRefreshers)
}else{
await Promise.all(scopeRefreshers)
}
@ -764,6 +768,7 @@ module.exports ={
translate,
i18nScope,
createFormatter,
Formatter,
defaultLanguageSettings,
getDataTypeName,
isNumber,

View File

@ -1,6 +1,6 @@
{
"name": "@voerkai18n/runtime",
"version": "1.0.28",
"version": "1.0.29",
"description": "核心运行时",
"main": "./dist/index.cjs",
"module": "dist/index.esm.js",
@ -35,5 +35,5 @@
"rollup-plugin-terser": "^7.0.2",
"@voerkai18n/autopublish": "workspace:^1.0.2"
},
"lastPublish": "2022-08-07T19:16:27+08:00"
"lastPublish": "2022-08-16T09:47:13+08:00"
}

View File

@ -39,12 +39,14 @@ export default [
{
file: 'dist/runtime.cjs',
exports:"auto",
format:"cjs"
format:"cjs",
sourcemap:true
},
{
file: 'dist/runtime.mjs',
exports:"default",
format:"esm"
format:"esm",
sourcemap:true
}
],
plugins:[

View File

@ -1,4 +1,4 @@
const { isPlainObject, isFunction, getByPath, deepMixin } = require("./utils");
const { isPlainObject, isFunction, getByPath, deepMixin,deepClone } = require("./utils");
const DataTypes = [
"String",
@ -41,8 +41,8 @@ module.exports = class i18nScope {
const { I18nManager } = require("./");
globalThis.VoerkaI18n = new I18nManager({
debug : this._debug,
defaultLanguage: this.defaultLanguage,
activeLanguage : this.activeLanguage,
defaultLanguage: this._defaultLanguage,
activeLanguage : this._activeLanguage,
languages : options.languages,
});
}
@ -55,7 +55,7 @@ module.exports = class i18nScope {
get id() {return this._id;} // 作用域唯一id
get debug() {return this._debug;} // 调试开关
get defaultLanguage() {return this._defaultLanguage;} // 默认语言名称
get activeLanguage() {return this._activeLanguage;} // 默认语言名称
get activeLanguage() {return this._global.activeLanguage;} // 默认语言名称
get default() {return this._default;} // 默认语言包
get messages() {return this._messages; } // 当前语言包
get idMap() {return this._idMap;} // 消息id映射列表
@ -64,7 +64,7 @@ module.exports = class i18nScope {
get global() { return this._global;} // 引用全局VoerkaI18n配置注册后自动引用
get formatters() { return this._formatters;} // 当前作用域的所有格式化器定义 {<语言名称>: {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () => {}}}
get activeFormatters() {return this._activeFormatters} // 当前作用域激活的格式化器定义 {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () = >{}}
get activeFormatterConfig(){return this._activeFormatterConfig} // 当前格式化器合并后的配置参数,参数已经合并了全局格式化器中的参数
get activeFormatterConfig(){return this._activeFormatterConfig} // 当前格式化器合并后的配置参数,参数已经合并了全局格式化器中的参数
/**
* 在全局注册作用域当前作用域
@ -159,7 +159,7 @@ module.exports = class i18nScope {
}
/**
* 初始化格式化器
* 激活和默认语言的格式化器采用静态导入的形式而没有采用异步块的形式这是为了确保首次加载时的能马上读取减少延迟加载
* 激活和默认语言的格式化器采用静态导入的形式而没有采用异步块的形式这是为了确保首次加载时的能马上读取不能采用延迟加载方式
* _activeFormatters={$config:{...},$types:{...},[格式化器名称]:()=>{...},[格式化器名称]:()=>{...},...}}
*/
_initFormatters(newLanguage){
@ -170,7 +170,7 @@ module.exports = class i18nScope {
} else {
if (this._debug) console.warn(`Not initialize <${newLanguage}> formatters.`);
}
this._generateFormatterOptions(newLanguage)
this._generateFormatterConfig(newLanguage)
} catch (e) {
if (this._debug) console.error(`Error while initialize ${newLanguage} formatters: ${e.message}`);
}
@ -183,7 +183,7 @@ module.exports = class i18nScope {
* 当切换语言时格式化器应该切换到对应语言的格式化器
*
* 重要需要处理
* $config参数采用合并继承机制
* $config参数采用合并继承机制,从全局读取
*
*
* @param {*} language
@ -198,7 +198,7 @@ module.exports = class i18nScope {
this._activeFormatters = (await loader()).default;
}
// 合并生成格式化器的配置参数,当执行格式化器时该参数将被传递给格式化器
this._generateFormatterOptions(newLanguage)
this._generateFormatterConfig(newLanguage)
} else {
if (this._debug) console.warn(`Not configured <${newLanguage}> formatters.`);
}
@ -212,10 +212,10 @@ module.exports = class i18nScope {
* - global.formatters[language].$config
* - scope.activeFormatters.$config 当前优先
*/
_generateFormatterOptions(language){
_generateFormatterConfig(language){
let options
try{
options = Object.assign({},getByPath(this._global.formatters,`*.$config`,{}))
options = deepClone(getByPath(this._global.formatters,`*.$config`,{}))
deepMixin(options,getByPath(this._global.formatters,`${language}.$config`,{}))
deepMixin(options,getByPath(this._activeFormatters,"$config",{}))
}catch(e){
@ -235,38 +235,45 @@ module.exports = class i18nScope {
if (newLanguage === this.defaultLanguage) {
this._messages = this._default;
await this._patch(this._messages, newLanguage); // 异步补丁
this._changeFormatters(newLanguage);
await this._changeFormatters(newLanguage);
return;
}
// 非默认语言需要异步加载语言包文件,加载器是一个异步函数
// 如果没有加载器,则无法加载语言包,因此回退到默认语言
let loader = this.loaders[newLanguage];
try {
if (isPlainObject(loader)) {
this._messages = loader;
await this._patch(this._messages, newLanguage);
} else if (isFunction(loader)) {
this._messages = (await loader()).default;
this._activeLanguage = newLanguage;
await this._patch(this._messages, newLanguage);
} else if (isFunction(this.global.defaultMessageLoader)) {
// 如果该语言没有指定加载器,则使用全局配置的默认加载器
const loadedMessages =
await this.global.loadMessagesFromDefaultLoader(
newLanguage,
this
);
this._messages = Object.assign(
{},
this._default,
loadedMessages
);
this._activeLanguage = newLanguage;
} else {
this._fallback();
let newMessages, useRemote =false;
if (isPlainObject(loader)) { // 静态语言包
newMessages = loader;
} else if (isFunction(loader)) { // 语言包异步chunk
newMessages = (await loader()).default;
} else if (isFunction(this.global.defaultMessageLoader)) { // 从远程加载语言包:如果该语言没有指定加载器,则使用全局配置的默认加载器
const loadedMessages = await this.global.loadMessagesFromDefaultLoader(newLanguage,this);
if(isPlainObject(loadedMessages)){
useRemote = true
// 需要保存动态语言包中的$config合并到对应语言的格式化器配置
if(isPlainObject(loadedMessages.$config)){
this._formatters[newLanguage] = {
$config : loadedMessages.$config
}
delete loadedMessages.$config
}
newMessages = Object.assign({},this._default,loadedMessages);
}
}
// 应该切换到对应语言的格式化器
this._changeFormatters(newLanguage);
if(newMessages){
this._messages = newMessages
this._activeLanguage = newLanguage;
// 打语言包补丁, 如果是从远程加载语言包则不需要再打补丁了
if(!useRemote) {
await this._patch(this._messages, newLanguage);
}
// 切换到对应语言的格式化器
await this._changeFormatters(newLanguage);
}else{
this._fallback();
}
} catch (e) {
if (this._debug) console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`);
this._fallback();
@ -344,16 +351,10 @@ module.exports = class i18nScope {
}
}
// 以下方法引用全局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);
}
on() {return this._global.on(...arguments); }
off() {return this._global.off(...arguments); }
offAll() {return this._global.offAll(...arguments);}
async change(language) {
await this._global.change(language);
}
};

View File

@ -97,7 +97,6 @@ function deepMerge(toObj,formObj,options={}){
return results
}
function deepMixin(toObj,formObj,options={}){
return deepMerge(toObj,formObj,{...options,mixin:true})
}
@ -174,7 +173,7 @@ function toNumber(value,defualt=0) {
/**
* 根据路径获取指定值
*
* 只支持简单的.分割路径
* getByPath({a:{b:1}},"a.b") == 1
* getByPath({a:{b:1}},"a.c",2) == 2
*
@ -184,11 +183,11 @@ function toNumber(value,defualt=0) {
* @returns
*/
function getByPath(obj,path,defaultValue){
if(typeof(obj)!="object") return defaultValue
if(typeof(obj)!="object" || typeof(path)!="string") return defaultValue
let paths = path.split(".")
let cur = obj
for(let key of paths){
if(typeof(cur)=="object" && key in cur ){
if(typeof(cur)=="object" && (key in cur) ){
cur = cur[key]
}else{
return defaultValue
@ -196,6 +195,22 @@ function getByPath(obj,path,defaultValue){
}
return cur
}
function deepClone(obj){
if(obj==undefined) return obj
if (['string',"number","boolean","function","undefined"].includes(typeof(obj))){
return obj
}else if(Array.isArray(obj)){
return obj.map(item => deepClone(item))
}else if(typeof(obj)=="object"){
let results = {}
Object.entries(obj).forEach(([key,value])=>{
results[key] = deepClone(value)
})
return results
}else{
return obj
}
}
// YY 18 年,两位数
@ -226,8 +241,7 @@ function formatDatetime(value,templ="YYYY/MM/DD HH:mm:ss"){
const v = toDate(value)
const year =String(v.getFullYear()),month = String(v.getMonth()+1),weekday=String(v.getDay()),day=String(v.getDate())
const hourNum = v.getHours()
const hour = String(hourNum).minute = String(v.getMinutes()),second = String(v.getSeconds()),millisecond=String(v.getMilliseconds())
const timezone = v.getTimezoneOffset()
const hour = String(hourNum), minute = String(v.getMinutes()),second = String(v.getSeconds()),millisecond=String(v.getMilliseconds())
const vars = [
["YYYY", year], // 2018 年,四位数
["YY", year.substring(year.length - 2, year.length)], // 18 年,两位数
@ -250,14 +264,15 @@ function formatDatetime(value,templ="YYYY/MM/DD HH:mm:ss"){
["A", hour > 12 ? "PM" : "AM"], // AM / PM 上/下午,大写
["a", hour > 12 ? "pm" : "am"], // am / pm 上/下午,小写
]
vars.forEach(([key,value])=>result = replaceAll(result,key,value))
let result = templ
vars.forEach(([k,v])=>result = replaceAll(result,k,v))
return result
}
function formatTime(value,templ="HH:mm:ss"){
const v = toDate(value)
const hourNum = v.getHours()
const hour = String(hourNum).minute = String(v.getMinutes()),second = String(v.getSeconds()),millisecond=String(v.getMilliseconds())
const hour = String(hourNum),minute = String(v.getMinutes()),second = String(v.getSeconds()),millisecond=String(v.getMilliseconds())
let result = templ
const vars = [
["HH", hour.padStart(2, "0")], // 00-23 24小时两位数
@ -274,7 +289,7 @@ function formatTime(value,templ="HH:mm:ss"){
["A", hour > 12 ? "PM" : "AM"], // AM / PM 上/下午,大写
["a", hour > 12 ? "pm" : "am"] // am / pm 上/下午,小写
]
vars.forEach(([key,value])=>result = replaceAll(result,key,value))
vars.forEach(([k,v])=>result = replaceAll(result,k,v))
return result
}
/**
@ -310,6 +325,7 @@ function replaceAll(str,findValue,replaceValue){
* "currency":createFormatter((value,prefix,suffix, division ,precision,options)=>{
* // 无论在格式化入参数是多少个经过处理后在此得到prefix,suffix, division ,precision参数已经是经过处理后的参数
* 依次读取格式化器的参数合并
* - 创建格式化时的defaultParams参数
* - 从当前激活格式化器的$config中读取配置参数
* - 在t函数后传入参数
* 比如currency格式化器支持4参数其入参顺序是prefix,suffix, division ,precision
@ -338,19 +354,19 @@ function replaceAll(str,findValue,replaceValue){
* )
*
* @param {*} fn
* @param {*} defaultParams 默认参数
* @param {*} meta
* @param {*} options 配置参数
* @param {*} defaultParams 可选默认值
* @returns
*/
function createFormatter(fn,defaultParams={},meta={}){
function createFormatter(fn,options={},defaultParams={}){
let opts = Object.assign({
normalize : null, // 对输入值进行规范化处理,如进行时间格式化时,为了提高更好的兼容性,支持数字时间戳/字符串/Date等需要对输入值进行处理如强制类型转换等
params : [], // 声明参数顺序
configKey : null // 声明该格式化器在$config中的路径支持简单的使用.的路径语法
})
},options)
// 最后一个参数是传入activeFormatterConfig参数
const wrappedFn = function(value,...args){
const $formatter = function(value,...args){
let finalValue = value
// 1. 输入值规范处理,主要是进行类型转换,确保输入的数据类型及相关格式的正确性,提高数据容错性
if(isFunction(opts.normalize)){
@ -369,10 +385,10 @@ function replaceAll(str,findValue,replaceValue){
if(i>=args.length-1) break // 最后一参数是配置
if(args[i]!==undefined) finalArgs[i] = args[i]
}
return fn(finalValue,...finalArgs,activeFormatterConfigs)
return fn(finalValue,...finalArgs,formatterConfig)
}
wrappedFn.configurable = true // 当函数是可配置时才在最后一个参数中传入$config
return wrappedFn
$formatter.configurable = true // 当函数是可配置时才在最后一个参数中传入$config
return $formatter
}
const Formatter = createFormatter
@ -382,6 +398,7 @@ module.exports ={
isFunction,
isNumber,
isNothing,
deepClone,
deepMerge,
deepMixin,
Formatter,

View File

@ -1,4 +1,4 @@
const {i18nScope, translate, getInterpolatedVars } = require('../packages/runtime/index')
const {i18nScope, translate, getInterpolatedVars } = require('../packages/runtime/dist/runtime.cjs')
const dayjs = require('dayjs');
const loaders = {
@ -31,10 +31,10 @@ const formatters = {
}
const idMap = {
1:"你好",
2:"现在是{}",
3:"我出生于{year}年,今年{age}岁",
4:"我有{}个朋友"
"你好":1,
"现在是{}":2,
"我出生于{year}年,今年{age}岁":3,
"我有{}个朋友":4
}
const languages = [
{ name: "zh", title: "中文" },
@ -127,7 +127,7 @@ test("替换翻译内容的位置插值变量",done=>{
expect(t("{ a|}{b|dd}{c|}{}",1,2,3)).toBe("123{}");
// 中文状态下true和false被转换成中文的"是"和"否"
expect(t("{}{}{}",1,"2",true)).toBe("12是");
expect(trim("{|double}{}{}",1,"2",true)).toBe("22是");
expect(t("{|double}{}{}",1,"2",true)).toBe("22是");
done()
})
@ -141,9 +141,7 @@ test("命名插值变量使用格式化器",done=>{
// 提供无效的格式化器,直接忽略
expect(t("{a|x}{b|x|y}{c|}",{a:1,b:2,c:3})).toBe("123");
expect(t("{a|x}{b|x|y}{c|double}",{a:1,b:2,c:3})).toBe("126");
// 默认的字符串格式化器,不需要定义使用字符串方法
expect(t("{a|x}{b|x|y}{c|double}",{a:1,b:2,c:3})).toBe("126");
// padStart格式化器是字符串的方法不需要额外定义可以直接使用
// padStart和trim格式化器只是字符串的原型方法不需要额外定义可以直接使用
expect(t("{a|padStart(10)}",{a:"123"})).toBe(" 123");
expect(t("{a|padStart(10)|trim}",{a:"123"})).toBe("123");
done()
@ -163,18 +161,16 @@ test("命名插值变量使用格式化器",done=>{
test("切换到其他语言时的自动匹配同名格式化器",done=>{
// 默认的字符串类型的格式化器
test("切换到其他语言时的自动匹配同名格式化器",async ()=>{
expect(t("{a}",{a:true})).toBe("是");
expect(t("{name|book}是毛泽东思想的重要载体","毛泽东选集")).toBe("《毛泽东选集》是毛泽东思想的重要载体");
changeLanguage("en")
await scope.change("en")
expect(t("{a}",{a:false})).toBe("False");
expect(t("{name|book}是毛泽东思想的重要载体","毛泽东选集")).toBe("<毛泽东选集>是毛泽东思想的重要载体");
done()
})
test("位置插值翻译文本内容",done=>{
test("位置插值翻译文本内容",async ()=>{
const now = new Date()
expect(t("你好")).toBe("你好");
expect(t("现在是{}",now)).toBe(`现在是${dayjs(now).format('YYYY年MM月DD日 HH点mm分ss秒')}`);
@ -183,21 +179,20 @@ test("位置插值翻译文本内容",done=>{
expect(t("1")).toBe("你好");
expect(t("2",now)).toBe(`现在是${dayjs(now).format('YYYY年MM月DD日 HH点mm分ss秒')}`);
changeLanguage("en")
await scope.change("en")
.eixt
expect(t("你好")).toBe("hello");
expect(t("现在是{}",now)).toBe(`Now is ${dayjs(now).format('YYYY-MM-DD HH:mm:ss')}`);
expect(t("1")).toBe("hello");
expect(t("2",now)).toBe(`Now is ${dayjs(now).format('YYYY-MM-DD HH:mm:ss')}`);
done()
})
test("命名插值翻译文本内容",done=>{
test("命名插值翻译文本内容",async ()=>{
const now = new Date()
expect(t("你好")).toBe("你好");
expect(t("现在是{}",now)).toBe(`现在是${dayjs(now).format('YYYY年MM月DD日 HH点mm分ss秒')}`);
changeLanguage("en")
await scope.change("en")
expect(t("你好")).toBe("hello");
expect(t("现在是{}",now)).toBe(`Now is ${dayjs(now).format('YYYY-MM-DD HH:mm:ss')}`);
expect(t("1")).toBe("hello");
@ -206,24 +201,22 @@ test("命名插值翻译文本内容",done=>{
})
test("当没有对应的语言翻译时",done=>{
test("当没有对应的语言翻译时,保持原始输出",async ()=>{
expect(t("我是中国人")).toBe("我是中国人");
scope.change("en")
await scope.change("en")
expect(t("我是中国人")).toBe("我是中国人");
done()
})
test("切换到未知语言时回退到默认语言",done=>{
test("切换到未知语言时回退到默认语言",async ()=>{
expect(t("我是中国人")).toBe("我是中国人");
scope.change("xn")
expect(async ()=>await scope.change("xn")).rejects.toThrow(Error);
expect(t("我是中国人")).toBe("我是中国人");
done()
})
test("翻译复数支持",done=>{
scope.change("en")
test("翻译复数支持",async ()=>{
await scope.change("en")
expect(t("我有{}个朋友",0)).toBe("I have no friends");
expect(t("我有{}个朋友",1)).toBe("I have one friends");
expect(t("我有{}个朋友",2)).toBe("I have two friends");