This commit is contained in:
wxzhang 2023-04-06 20:41:15 +08:00
parent 105a75bc59
commit e7318e2de4
6 changed files with 97 additions and 76 deletions

View File

@ -4,13 +4,20 @@ import {test,vi,describe,expect,afterAll,beforeAll} from 'vitest'
import { VoerkaI18nScope } from '../scope' import { VoerkaI18nScope } from '../scope'
import zhFormatters from '../formatters/zh'; import zhFormatters from '../formatters/zh';
import enFormatters from '../formatters/en'; import enFormatters from '../formatters/en';
import { VoerkaI18nManager } from '../manager';
import { VoerkaI18nFormatterRegistry } from '../formatterRegistry';
import { VoerkaI18nLanguageMessages } from '../types';
const zhMessages={ const zhMessages:VoerkaI18nLanguageMessages = {
$config:{
x:{a:1},
y:{b:1}
},
"1": "你好", "1": "你好",
"2": "你好,{name}", "2": "你好,{name}",
"3": "中国", "3": "中国",
"4": ["我有一部车","我有很多部车"] "4": ["我有一部车","我有很多部车"] ,
} }
const enMessages={ const enMessages={
"1": "hello", "1": "hello",
@ -37,7 +44,8 @@ const languages = [
const formatters ={ const formatters ={
zh:zhFormatters, zh:zhFormatters,
en:enFormatters en:enFormatters,
jp:()=>{}
} }
describe("VoerkaI18nScope", () => { describe("VoerkaI18nScope", () => {
@ -56,8 +64,19 @@ describe("VoerkaI18nScope", () => {
expect(scope.default).toEqual(zhMessages) expect(scope.default).toEqual(zhMessages)
expect(scope.current).toEqual(zhMessages) expect(scope.current).toEqual(zhMessages)
expect(scope.idMap).toEqual(idMap) expect(scope.idMap).toEqual(idMap)
// 格式化器配置
expect(scope.formatters).toBeInstanceOf(VoerkaI18nFormatterRegistry)
expect(scope.formatters.language).toBe("zh")
expect(scope.formatters.formatters).toEqual(formatters)
expect(scope.formatters.config).toBe(zhFormatters.$config)
expect(scope.formatters.types).toBe(zhFormatters.$types)
// 全局管理器
expect(scope.global).toBeInstanceOf(VoerkaI18nManager)
}) })
}) })
test('translate', () => {}) test('translate', () => {})

View File

@ -32,8 +32,8 @@ export class VoerkaI18nFormatterRegistry{
#ready:boolean = false #ready:boolean = false
#formatters:VoerkaI18nLanguageFormatters = {} #formatters:VoerkaI18nLanguageFormatters = {}
#language:string = 'zh' #language:string = 'zh'
constructor(formatters?:VoerkaI18nLanguageFormatters){ constructor(){
this.#formatters = formatters || {}
} }
/** /**
* *
@ -65,20 +65,16 @@ export class VoerkaI18nFormatterRegistry{
使, 使,
*/ */
register(name:string | SupportedDateTypes, formatter:VoerkaI18nFormatter, {language = "*"}:{ language: string | string[] } ) { register(name:string | SupportedDateTypes, formatter:VoerkaI18nFormatter, {language = "*"}:{ language: string | string[] } ) {
if (!isFunction(formatter) || typeof name !== "string") { if (!isFunction(formatter) || typeof name !== "string") {
throw new TypeError("Formatter must be a function"); throw new TypeError("Formatter must be a function");
} }
const languages = Array.isArray(language) ? language: language ? language.split(","): []; const languages = Array.isArray(language) ? language: language ? language.split(","): [];
languages.forEach((lngName:string) => { languages.forEach((lngName:string) => {
if(!(lngName in this.#formatters)) { if(!(lngName in this.#formatters)) this.#formatters[lngName] = {}
this.#formatters[lngName] = { }
}
if(typeof(this.#formatters[lngName])!="function"){ if(typeof(this.#formatters[lngName])!="function"){
let lngFormatters = this.#formatters[lngName] as any let lngFormatters = this.#formatters[lngName] as any
if (DataTypes.includes(name)) { if (DataTypes.includes(name)) {
if(!lngFormatters.$types){ if(!lngFormatters.$types) lngFormatters.$types = {}
lngFormatters.$types = {}
}
lngFormatters.$types![name] = formatter lngFormatters.$types![name] = formatter
} else { } else {
lngFormatters[name] = formatter; lngFormatters[name] = formatter;
@ -187,9 +183,5 @@ export class VoerkaI18nFormatterRegistry{
if(!this.#ready) throw new FormattersNotLoadedError(this.#language) if(!this.#ready) throw new FormattersNotLoadedError(this.#language)
return (this.#formatters[this.#language] as VoerkaI18nFormatters).$types as VoerkaI18nFormatterConfigs return (this.#formatters[this.#language] as VoerkaI18nFormatters).$types as VoerkaI18nFormatterConfigs
} }
get items(){
if(!this.#ready) throw new FormattersNotLoadedError(this.#language)
return this.#formatters[this.#language] as VoerkaI18nFormatterConfigs
}
} }

View File

@ -5,6 +5,7 @@ import inlineFormatters from "./formatters"
import type { VoerkaI18nScope } from "./scope" import type { VoerkaI18nScope } from "./scope"
import type { VoerkaI18nLanguageDefine, VoerkaI18nLanguageFormatters, VoerkaI18nDefaultMessageLoader, VoerkaI18nFormatter, VoerkaI18nTypesFormatters } from "./types" import type { VoerkaI18nLanguageDefine, VoerkaI18nLanguageFormatters, VoerkaI18nDefaultMessageLoader, VoerkaI18nFormatter, VoerkaI18nTypesFormatters } from "./types"
import { VoerkaI18nFormatterRegistry } from "./formatterRegistry" import { VoerkaI18nFormatterRegistry } from "./formatterRegistry"
import { DataTypes } from "./utils"
// 默认语言配置 // 默认语言配置
const defaultLanguageSettings = { const defaultLanguageSettings = {
@ -46,7 +47,7 @@ export class VoerkaI18nManager extends EventEmitter{
#options?:Required<VoerkaI18nManagerOptions> #options?:Required<VoerkaI18nManagerOptions>
#scopes:VoerkaI18nScope[] = [] #scopes:VoerkaI18nScope[] = []
#defaultMessageLoader?:VoerkaI18nDefaultMessageLoader #defaultMessageLoader?:VoerkaI18nDefaultMessageLoader
#formatterRegistry!:VoerkaI18nFormatterRegistry #formatters:VoerkaI18nLanguageFormatters = {}
constructor(options?:VoerkaI18nManagerOptions){ constructor(options?:VoerkaI18nManagerOptions){
super() super()
if(VoerkaI18nManager.instance){ if(VoerkaI18nManager.instance){
@ -54,7 +55,6 @@ export class VoerkaI18nManager extends EventEmitter{
} }
VoerkaI18nManager.instance = this; VoerkaI18nManager.instance = this;
this.#options = deepMerge(defaultLanguageSettings,options) as Required<VoerkaI18nManagerOptions> this.#options = deepMerge(defaultLanguageSettings,options) as Required<VoerkaI18nManagerOptions>
this.#formatterRegistry = new VoerkaI18nFormatterRegistry()
this.loadInitialFormatters() // 加载初始格式化器 this.loadInitialFormatters() // 加载初始格式化器
this.#scopes=[] // 保存VoerkaI18nScope实例 this.#scopes=[] // 保存VoerkaI18nScope实例
} }
@ -65,14 +65,14 @@ export class VoerkaI18nManager extends EventEmitter{
get defaultLanguage(){ return this.#options!.defaultLanguage} // 默认语言名称 get defaultLanguage(){ return this.#options!.defaultLanguage} // 默认语言名称
get languages(){ return this.#options!.languages} // 支持的语言列表 get languages(){ return this.#options!.languages} // 支持的语言列表
get defaultMessageLoader(){ return this.#defaultMessageLoader} // 默认语言包加载器 get defaultMessageLoader(){ return this.#defaultMessageLoader} // 默认语言包加载器
get formatters(){return this.#formatterRegistry!} get formatters(){return this.#formatters!}
/** /**
* *
*/ */
private loadInitialFormatters(){ private loadInitialFormatters(){
if(this.#options?.formatters){ if(this.#options?.formatters){
this.#formatterRegistry.loadInitials(this.#options!.formatters) this.#formatters = this.#options!.formatters
delete (this.#options as any).formatters delete (this.#options as any).formatters
} }
} }
@ -146,7 +146,22 @@ export class VoerkaI18nManager extends EventEmitter{
language : 声明该格式化器适用语言 language : 声明该格式化器适用语言
*/ */
registerFormatter(name:string,formatter:VoerkaI18nFormatter,{language="*"}:{language:string | string[] | '*'}){ registerFormatter(name:string,formatter:VoerkaI18nFormatter,{language="*"}:{language:string | string[] | '*'}){
this.#formatterRegistry.register(name,formatter,{language}) if (!isFunction(formatter) || typeof name !== "string") {
throw new TypeError("Formatter must be a function");
}
const languages = Array.isArray(language) ? language: language ? language.split(","): [];
languages.forEach((lngName:string) => {
if(!(lngName in this.#formatters)) this.#formatters[lngName] = {}
if(typeof(this.#formatters[lngName])!="function"){
let lngFormatters = this.#formatters[lngName] as any
if (DataTypes.includes(name)) {
if(!lngFormatters.$types) lngFormatters.$types = {}
lngFormatters.$types![name] = formatter
} else {
lngFormatters[name] = formatter;
}
}
});
} }
/** /**
* *

View File

@ -24,6 +24,7 @@ VoerkaI18nMessageLoader,
} from "./types" } from "./types"
import { VoerkaI18nFormatterRegistry } from './formatterRegistry'; import { VoerkaI18nFormatterRegistry } from './formatterRegistry';
import { InvalidLanguageError } from "./errors" import { InvalidLanguageError } from "./errors"
import { randomId } from "./utils"
export interface VoerkaI18nScopeOptions { export interface VoerkaI18nScopeOptions {
id?: string id?: string
@ -38,20 +39,20 @@ export interface VoerkaI18nScopeOptions {
export class VoerkaI18nScope { export class VoerkaI18nScope {
#options:Required<VoerkaI18nScopeOptions> #options:Required<VoerkaI18nScopeOptions>
#global:VoerkaI18nManager // 引用全局VoerkaI18nManager配置注册后自动引用 #global:VoerkaI18nManager // 引用全局VoerkaI18nManager配置注册后自动引用
#refreshing:boolean = false #refreshing:boolean = false
#patchMessages:VoerkaI18nLanguagePack = {}
#t:VoerkaI18nTranslate #t:VoerkaI18nTranslate
#activeFormatters:VoerkaI18nFormatters = {} #activeFormatters:VoerkaI18nFormatters = {}
#activeFormatterConfig: VoerkaI18nFormatterConfigs={} #activeFormatterConfig: VoerkaI18nFormatterConfigs={}
#cache:VoerkaI18nScopeCache #cache:VoerkaI18nScopeCache
#formatterRegistry:VoerkaI18nFormatterRegistry #formatterRegistry:VoerkaI18nFormatterRegistry = new VoerkaI18nFormatterRegistry()
#defaultLanguage:string ='zh' #defaultLanguage:string ='zh'
#activeLanguage:string='zh' #activeLanguage:string='zh'
#currentMessages:VoerkaI18nLanguageMessages = {} // 当前语言包 #currentMessages:VoerkaI18nLanguageMessages = {} // 当前语言包
#patchedMessages:VoerkaI18nLanguagePack = {} // 补丁语言包
constructor(options:VoerkaI18nScopeOptions, callback?:(e?:Error)=>void) { constructor(options:VoerkaI18nScopeOptions, callback?:(e?:Error)=>void) {
this.#options = assignObject({ this.#options = assignObject({
id : Date.now().toString() + parseInt(String(Math.random() * 1000)), id : randomId(), // 作用域唯一id
debug : false, debug : false,
languages : {}, // 当前作用域支持的语言列表 languages : {}, // 当前作用域支持的语言列表
messages : {}, // 所有语言包={[language]:VoerkaI18nLanguageMessages} messages : {}, // 所有语言包={[language]:VoerkaI18nLanguageMessages}
@ -63,24 +64,16 @@ export class VoerkaI18nScope {
activeLanguage : this.#options.activeLanguage, activeLanguage : this.#options.activeLanguage,
typedFormatters: {}, typedFormatters: {},
formatters : {}, formatters : {},
}; };
// 初始化 // 初始化
this._initiLanguages() this.init()
// 如果不存在全局VoerkaI18n实例说明当前Scope是唯一或第一个加载的作用域则自动创建全局VoerkaI18n实例 // 将当前实例注册到全局单例VoerkaI18nManager中
if (!globalThis.VoerkaI18n) { this.#global = this.registerToManager(callback)
globalThis.VoerkaI18n = new VoerkaI18nManager({ // 从本地缓存中读取并合并补丁语言包
debug : this.debug, this._mergePatchedMessages();
defaultLanguage: this.#defaultLanguage, // 延后执行补丁命令,该命令会向远程下载补丁包
activeLanguage : this.#activeLanguage, this._patch(this.#currentMessages, this.activeLanguage);
languages : options.languages,
});
}
this.#t = translate.bind(this) this.#t = translate.bind(this)
this.#global = globalThis.VoerkaI18n as unknown as VoerkaI18nManager;
this.#formatterRegistry = new VoerkaI18nFormatterRegistry()
this._mergePatchedMessages(); // 从本地缓存中读取并合并补丁语言包
this._patch(this.#currentMessages, this.activeLanguage); // 延后执行补丁命令,该命令会向远程下载补丁包
if(callback) this.register(callback); // 在全局注册作用域
} }
get id() {return this.#options.id;} // 作用域唯一id get id() {return this.#options.id;} // 作用域唯一id
get debug() {return this.#options.debug;} // 调试开关 get debug() {return this.#options.debug;} // 调试开关
@ -106,7 +99,7 @@ export class VoerkaI18nScope {
* - en配置为默认回退语言 * - en配置为默认回退语言
* - * -
*/ */
_initiLanguages(){ private init(){
if(!Array.isArray(this.languages)){ if(!Array.isArray(this.languages)){
console.warn("[VoerkaI18n] invalid languages config,use default languages config instead.") console.warn("[VoerkaI18n] invalid languages config,use default languages config instead.")
this.#options.languages = [ this.#options.languages = [
@ -138,16 +131,29 @@ export class VoerkaI18nScope {
throw new Error("[VoerkaI18n] invalid <defaultLanguage> config, messages must be static.") throw new Error("[VoerkaI18n] invalid <defaultLanguage> config, messages must be static.")
} }
this.#currentMessages = this.messages[this.#activeLanguage] as VoerkaI18nLanguageMessages this.#currentMessages = this.messages[this.#activeLanguage] as VoerkaI18nLanguageMessages
// 初始化格式化器
this.loadInitialFormatters()
} }
/**
/** *
* * @param callback
* @param {*} callback */
*/ private registerToManager(callback?:(e?:Error)=>void){
register(callback:(e?:Error)=>void) { // 如果不存在全局VoerkaI18n实例说明当前Scope是唯一或第一个加载的作用域则自动创建全局VoerkaI18n实例
if (!globalThis.VoerkaI18n) {
globalThis.VoerkaI18n = new VoerkaI18nManager({
debug : this.debug,
defaultLanguage: this.#defaultLanguage,
activeLanguage : this.#activeLanguage,
languages : this.#options.languages,
});
}
this.#global = globalThis.VoerkaI18n as unknown as VoerkaI18nManager;
if (!isFunction(callback)) callback = () => {}; if (!isFunction(callback)) callback = () => {};
this.global.register(this).then(()=>callback()).catch((e)=>callback(e)); this.#global.register(this).then(()=>(callback as any)()).catch((e)=>(callback as any)(e));
} return this.#global
}
/** /**
* *
* *
@ -199,10 +205,9 @@ export class VoerkaI18nScope {
* ... * ...
* } * }
*/ */
private loadFormatters(newLanguage:string){ private loadInitialFormatters(){
// 初始化格式化器 // 初始化格式化器
this.formatters.loadInitials(this.#options.formatters) this.formatters.loadInitials(this.#options.formatters)
this.#formatterRegistry.language = newLanguage
if(this.#options.formatters) if(this.#options.formatters)
// 将配置中的指定为全局的格式化器注册到全局 // 将配置中的指定为全局的格式化器注册到全局
Object.entries(this.#options.formatters).forEach(([langName,formatters])=>{ Object.entries(this.#options.formatters).forEach(([langName,formatters])=>{
@ -219,17 +224,8 @@ export class VoerkaI18nScope {
}) })
// 保存到Registry中就可以从options中删除了 // 保存到Registry中就可以从options中删除了
delete (this.#options as any).formatters delete (this.#options as any).formatters
// 初始化格式化器 // 切换到当前语言的格式化器上下文
try { this.changeFormatters(this.activeLanguage)
if (this.formatters.hasLanguage(newLanguage)) {
} else {
if (this.debug) console.warn(`Not initialize <${newLanguage}> formatters.`);
}
this._generateFormatterConfig(newLanguage)
} catch (e:any) {
if (this.debug) console.error(`Error while initialize ${newLanguage} formatters: ${e.message}`);
}
} }
/** /**
@ -244,7 +240,7 @@ export class VoerkaI18nScope {
* *
* @param {*} language * @param {*} language
*/ */
async _changeFormatters(newLanguage:string) { private async changeFormatters(newLanguage:string) {
try { try {
if (this.formatters.hasLanguage(newLanguage)) { if (this.formatters.hasLanguage(newLanguage)) {
this.formatters.language = newLanguage this.formatters.language = newLanguage
@ -260,16 +256,9 @@ export class VoerkaI18nScope {
} }
} }
/** /**
* *
*
* - global.formatters[*].$config
* - global.formatters[fallbackLanguage].$config
* - global.formatters[language].$config
* - this.formatters[*].$config
* - this.formatters[fallbackLanguage].$config
* - this.formatters[language].$config
*/ */
_generateFormatterConfig(language:string){ private _generateFormatterConfig(language:string){
try{ try{
const fallbackLanguage = this.getLanguage(language)?.fallback; const fallbackLanguage = this.getLanguage(language)?.fallback;
let configSources = [ let configSources = [
@ -375,7 +364,7 @@ export class VoerkaI18nScope {
if (newLanguage === this.defaultLanguage) { if (newLanguage === this.defaultLanguage) {
this.#currentMessages = this.default; this.#currentMessages = this.default;
await this._patch(this.#currentMessages, newLanguage); // 异步补丁 await this._patch(this.#currentMessages, newLanguage); // 异步补丁
await this._changeFormatters(newLanguage); await this.changeFormatters(newLanguage);
this.#refreshing = false this.#refreshing = false
return; return;
}else{ // 非默认语言可以是静态语言包也可以是异步加载语言包 }else{ // 非默认语言可以是静态语言包也可以是异步加载语言包
@ -390,7 +379,7 @@ export class VoerkaI18nScope {
await this._patch(this.#currentMessages, newLanguage); await this._patch(this.#currentMessages, newLanguage);
} }
// 切换到对应语言的格式化器 // 切换到对应语言的格式化器
await this._changeFormatters(newLanguage); await this.changeFormatters(newLanguage);
}else{ }else{
this._fallback(); this._fallback();
} }

View File

@ -9,8 +9,9 @@ declare global {
export type SupportedDateTypes = "String" | "Number" | "Boolean" | "Object" | "Array" | "Function" | "Error" | "Symbol" | "RegExp" | "Date" | "Null" | "Undefined" | "Set" | "Map" | "WeakSet" | "WeakMap" export type SupportedDateTypes = "String" | "Number" | "Boolean" | "Object" | "Array" | "Function" | "Error" | "Symbol" | "RegExp" | "Date" | "Null" | "Undefined" | "Set" | "Map" | "WeakSet" | "WeakMap"
// 语言包 // 语言包
export type VoerkaI18nLanguageMessages = Record<string, string | string[]> & { export type VoerkaI18nLanguageMessages = Record<string, string | string[]> | {
$config?: VoerkaI18nTypesFormatterConfigs $config?: VoerkaI18nTypesFormatterConfigs
$remote?: boolean
} }
export type VoerkaI18nLanguageMessagePack = Record<string, VoerkaI18nLanguageMessages | VoerkaI18nMessageLoader> export type VoerkaI18nLanguageMessagePack = Record<string, VoerkaI18nLanguageMessages | VoerkaI18nMessageLoader>

View File

@ -71,3 +71,8 @@ export function toDate(value:any) {
export function toBoolean(value:any){ export function toBoolean(value:any){
return !!value return !!value
} }
export function randomId():string{
return Date.now().toString() + parseInt(String(Math.random() * 1000))
}