From 8f7cc049277afbaf3f27fbc3e756d965fcf59ba1 Mon Sep 17 00:00:00 2001 From: wxzhang Date: Thu, 2 Jan 2025 18:01:51 +0800 Subject: [PATCH] update --- packages/runtime/src/__tests__/_utils.ts | 25 ++++++ packages/runtime/src/__tests__/change.test.ts | 13 +++- packages/runtime/src/__tests__/patch.test.ts | 34 +++++++++ .../src/__tests__/save.restore.test.ts | 68 +++++++++++++++++ packages/runtime/src/__tests__/scope.test.ts | 11 +-- packages/runtime/src/manager/index.ts | 48 ++++-------- packages/runtime/src/scope/index.ts | 76 ++++++++++++------- .../src/scope/mixins/{loader.ts => change.ts} | 16 ++-- packages/runtime/src/scope/mixins/patch.ts | 12 +-- packages/runtime/src/scope/mixins/restore.ts | 36 +++++++-- packages/runtime/src/types.ts | 21 ++--- 11 files changed, 257 insertions(+), 103 deletions(-) create mode 100644 packages/runtime/src/__tests__/patch.test.ts create mode 100644 packages/runtime/src/__tests__/save.restore.test.ts rename packages/runtime/src/scope/mixins/{loader.ts => change.ts} (90%) diff --git a/packages/runtime/src/__tests__/_utils.ts b/packages/runtime/src/__tests__/_utils.ts index f0dfc77..f51bfb6 100644 --- a/packages/runtime/src/__tests__/_utils.ts +++ b/packages/runtime/src/__tests__/_utils.ts @@ -1,3 +1,4 @@ +import { VoerkaI18nLanguageLoader } from '@/types' import { VoerkaI18nScope, VoerkaI18nScopeOptions } from '../scope' import { VoerkaI18nManager } from "@/manager" @@ -26,10 +27,34 @@ export function createVoerkaI18nScope(opts?:Partial): Vo export function resetVoerkaI18n() { try{ + + if(globalThis.VoerkaI18n){ + globalThis.VoerkaI18n.clearLanguage() + } + // @ts-ignore delete globalThis.__VoerkaI18nScopes__ // @ts-ignore delete globalThis.VoerkaI18n VoerkaI18nManager.instance = undefined + }catch{} +} + + +export function getTestStorage(init?:string){ + let saveLanguage:string | undefined= init + return { + get() { return saveLanguage}, + set(_:string,value:string){saveLanguage = value}, + remove(){ saveLanguage = undefined } + } + +} + + +export function getTestLanguageLoader(patchMsgs?:Record):VoerkaI18nLanguageLoader{ + return async (language:string,scope:VoerkaI18nScope)=>{ + return Object.assign({},patchMsgs) + } } \ No newline at end of file diff --git a/packages/runtime/src/__tests__/change.test.ts b/packages/runtime/src/__tests__/change.test.ts index 0e7ad0b..a9eea0c 100644 --- a/packages/runtime/src/__tests__/change.test.ts +++ b/packages/runtime/src/__tests__/change.test.ts @@ -5,7 +5,7 @@ -import { test, vi, describe, expect, beforeEach } from 'vitest' +import { test, describe, expect, beforeEach } from 'vitest' import { createVoerkaI18nScope, resetVoerkaI18n } from './_utils'; describe('语言切换', () => { @@ -117,7 +117,7 @@ describe('语言切换', () => { expect(appScope.activeLanguage).toBe("en") expect(appScope.activeMessages).toEqual({ message: 'Hello' }) expect(detachedScope.activeLanguage).toBe("zh") - expect(detachedScope.activeMessages).toEqual({ message: 'Hello' }) + expect(detachedScope.activeMessages).toEqual({ message: '你好' }) }) test("独立切换detachedScope语言",async ()=>{ @@ -130,6 +130,15 @@ describe('语言切换', () => { expect(detachedScope.activeMessages).toEqual({ message: 'Hello' }) }) + + test("切换到不存在的语言时进行回退",async ()=>{ + const appScope = createVoerkaI18nScope({ id: "a" }); + await appScope.change("de") + // 回退到defaultLanguage + expect(appScope.activeLanguage).toBe("zh") + expect(appScope.activeMessages).toEqual({ message: 'Hello' }) + + }) }); diff --git a/packages/runtime/src/__tests__/patch.test.ts b/packages/runtime/src/__tests__/patch.test.ts new file mode 100644 index 0000000..f2e7ca1 --- /dev/null +++ b/packages/runtime/src/__tests__/patch.test.ts @@ -0,0 +1,34 @@ +/** + * + * 语言补丁 + * + * 语言补丁功能需要配置LnaguageLoader + * + * + */ + + + +import { test, vi, describe, expect, beforeEach } from 'vitest' +import { VoerkaI18nScope } from '../scope' +import { VoerkaI18nManager } from '../manager'; +import { createVoerkaI18nScope, getTestLanguageLoader, getTestStorage, resetVoerkaI18n } from './_utils'; +import { VoerkaI18nOnlyOneAppScopeError } from '@/errors'; +import { VoerkaI18nLanguageLoader } from '@/types'; + + +describe('语言包补丁功能', () => { + beforeEach(() => { + resetVoerkaI18n() + }); + test('appScope加载时加载补丁', async () => { + const storage = getTestStorage() + const languageLoader = getTestLanguageLoader() + + const appScope = createVoerkaI18nScope({ + storage + }) + }); +}); + + diff --git a/packages/runtime/src/__tests__/save.restore.test.ts b/packages/runtime/src/__tests__/save.restore.test.ts new file mode 100644 index 0000000..babab27 --- /dev/null +++ b/packages/runtime/src/__tests__/save.restore.test.ts @@ -0,0 +1,68 @@ + + +import { test, vi, describe, expect, beforeEach } from 'vitest' +import { createVoerkaI18nScope, getTestStorage, resetVoerkaI18n } from './_utils'; + + + +describe('保存与恢复语言', () => { + beforeEach(() => { + resetVoerkaI18n() + }); + test('保存与恢复语言配置到存储', async () => { + const storage = getTestStorage() + const scope = createVoerkaI18nScope({storage}); + expect(storage.get()).toBe(undefined); + await scope.change('en') + expect(storage.get()).toBe('en'); + await scope.change('zh') + expect(storage.get()).toBe('zh'); + }); + test('从存储中恢复语言', async () => { + const storage = getTestStorage('en') + const scope = createVoerkaI18nScope({storage}); + await scope.refreshing() + expect(scope.activeLanguage).toBe('en'); + expect(scope.activeMessages).toEqual({ message: 'Hello' }); + }); + test('多个scope从存储中恢复语言', async () => { + const storage = getTestStorage('en') + const libScope1 = createVoerkaI18nScope({id:"a", library:true}); + const libScope2 = createVoerkaI18nScope({id:"b", library:true}); + const libScope3 = createVoerkaI18nScope({id:"c", library:true}); + const appScope = createVoerkaI18nScope({id:"app",storage}); + await appScope.refreshing() + await libScope1.refreshing() + await libScope2.refreshing() + await libScope3.refreshing() + expect(appScope.activeLanguage).toBe('en'); + expect(appScope.activeMessages).toEqual({ message: 'Hello' }); + expect(libScope1.activeLanguage).toBe('en'); + expect(libScope1.activeMessages).toEqual({ message: 'Hello' }); + expect(libScope2.activeLanguage).toBe('en'); + expect(libScope2.activeMessages).toEqual({ message: 'Hello' }); + expect(libScope3.activeLanguage).toBe('en'); + expect(libScope3.activeMessages).toEqual({ message: 'Hello' }); + }); + test('多个libScope在appScope后面注册时从存储中恢复语言', async () => { + const storage = getTestStorage('en') + const appScope = createVoerkaI18nScope({id:"app",storage}); + await appScope.refreshing() + const libScope1 = createVoerkaI18nScope({id:"a", library:true}); + const libScope2 = createVoerkaI18nScope({id:"b", library:true}); + const libScope3 = createVoerkaI18nScope({id:"c", library:true}); + await libScope1.refreshing() + await libScope2.refreshing() + await libScope3.refreshing() + expect(appScope.activeLanguage).toBe('en'); + expect(appScope.activeMessages).toEqual({ message: 'Hello' }); + expect(libScope1.activeLanguage).toBe('en'); + expect(libScope1.activeMessages).toEqual({ message: 'Hello' }); + expect(libScope2.activeLanguage).toBe('en'); + expect(libScope2.activeMessages).toEqual({ message: 'Hello' }); + expect(libScope3.activeLanguage).toBe('en'); + expect(libScope3.activeMessages).toEqual({ message: 'Hello' }); + }); +}); + + diff --git a/packages/runtime/src/__tests__/scope.test.ts b/packages/runtime/src/__tests__/scope.test.ts index 65636e7..add2ccd 100644 --- a/packages/runtime/src/__tests__/scope.test.ts +++ b/packages/runtime/src/__tests__/scope.test.ts @@ -7,8 +7,7 @@ import { createVoerkaI18nScope, resetVoerkaI18n } from './_utils'; import { VoerkaI18nOnlyOneAppScopeError } from '@/errors'; -describe('VoerkaI18nScope', () => { - describe('创建VoerkaI18nScope实例', () => { +describe('创建VoerkaI18nScope实例', () => { beforeEach(() => { resetVoerkaI18n() }); @@ -34,22 +33,20 @@ describe('VoerkaI18nScope', () => { } }); - test('创建多个VoerkaI18nScope实例', () => { + test('先创建appScope再创建多个VoerkaI18nScope实例', () => { createVoerkaI18nScope({ id: "a" }); createVoerkaI18nScope({ id: "b", library: true }); createVoerkaI18nScope({ id: "c", library: true }); createVoerkaI18nScope({ id: "d", library: true }); expect(globalThis.VoerkaI18n.scopes.length).toBe(4); }) - test('延后创建多个VoerkaI18nScope实例', () => { + test('先创建多个VoerkaI18nScope实例再创建appScope', () => { createVoerkaI18nScope({ id: "b", library: true }); createVoerkaI18nScope({ id: "c", library: true }); createVoerkaI18nScope({ id: "d", library: true }); createVoerkaI18nScope({ id: "a" }); expect(globalThis.VoerkaI18n.scopes.length).toBe(4); - }) - - }) + }) }); diff --git a/packages/runtime/src/manager/index.ts b/packages/runtime/src/manager/index.ts index 003ae70..daff514 100644 --- a/packages/runtime/src/manager/index.ts +++ b/packages/runtime/src/manager/index.ts @@ -1,6 +1,6 @@ import { isFunction } from "flex-tools/typecheck/isFunction" import type { VoerkaI18nScope } from "../scope" -import type { VoerkaI18nLanguageDefine, VoerkaI18nMessageLoader, VoerkaI18nEvents, IVoerkaI18nStorage } from "../types" +import type { VoerkaI18nLanguageDefine, VoerkaI18nLanguageLoader, VoerkaI18nEvents, IVoerkaI18nStorage } from "../types" import { VoerkaI18nInvalidLanguageError } from '../errors'; import { LiteEvent } from "flex-tools/events/liteEvent" import { execAsyncs, isI18nScope, isStorage } from "../utils" @@ -26,7 +26,6 @@ export class VoerkaI18nManager extends LiteEvent{ static instance? : VoerkaI18nManager private _scopes : VoerkaI18nScope[] = [] private _appScope! : VoerkaI18nScope - private _activeLanguage : string = 'zh' constructor(appScope:VoerkaI18nScope){ super() @@ -43,7 +42,7 @@ export class VoerkaI18nManager extends LiteEvent{ get logger(){ return this.scope.logger! } // 日志记录器 get scopes(){ return this._scopes } // 注册VoerkaI18nScope实例 get activeLanguage(){ return this._appScope.activeLanguage } // 当前激活语言名称 - get messageLoader(){ return this._appScope.messageLoader} // 默认语言包加载器 + get languageLoader(){ return this._appScope.languageLoader} // 默认语言包加载器 get storage(){return this.scope!.storage} get languages(){return this.scope.languages} get scope(){return this._appScope!} @@ -70,8 +69,6 @@ export class VoerkaI18nManager extends LiteEvent{ private _registerAppScope(scope:VoerkaI18nScope){ this._scopes.push(scope) this._appScope = scope - this._activeLanguage = scope.activeLanguage - this._getLanguageFromStorage() // 从存储器加载语言包配置 this.logger.debug("注册应用I18nScope: "+scope.id) this.emitAsync("init",undefined,true) .then(()=>this.emitAsync("ready",this.activeLanguage,true)) @@ -90,38 +87,14 @@ export class VoerkaI18nManager extends LiteEvent{ this._scopes.push(scope) scope.bind(this) this.logger.debug(`VoerkaI18nScope<${scope.id}>已注册`) - } - private _getStorage():IVoerkaI18nStorage | undefined{ - const storage = this.scope.storage - return isStorage(storage) ? storage: undefined - } - /** - * 从存储器加载语言包配置 - */ - private _getLanguageFromStorage(){ - const storage = this._getStorage() - if(storage){ - const savedLanguage = storage.get("language") - if(!savedLanguage || !this.hasLanguage(savedLanguage)) return - this._activeLanguage = savedLanguage - this.logger.debug("从存储中读取到当前语言:",savedLanguage) - } - } - private _setLanguageToStorage(){ - const storage = this._getStorage() - if(storage){ - if(!this._activeLanguage) return - storage.set("language",this.activeLanguage) - this.logger.debug("当前语言设置已保存到存储:",this.activeLanguage) - } - } + } /** * 切换语言 */ async change(language:string){ - if(this.hasLanguage(language) || isFunction(this.messageLoader)){ - await this._refreshScopes(language) // 刷新所有作用域 - this._setLanguageToStorage() // 保存语言配置到存储器 + if(this.hasLanguage(language) || isFunction(this.languageLoader)){ + await this._refreshScopes(language) // 刷新所有作用域 + this.scope.saveLanguage() // 保存语言配置到存储器 this.emit("change",language,true) this.logger.info("语言已切换为:"+ language) return language @@ -164,6 +137,15 @@ export class VoerkaI18nManager extends LiteEvent{ hasLanguage(language:string) { return this.languages.findIndex((lang:VoerkaI18nLanguageDefine) => lang.name == language) != -1; } + clearLanguage(){ + this.scope.clearLanguage() + } + saveLanguage(){ + this.scope.saveLanguage() + } + restoreLanguage(){ + this.scope.restoreLanguage() + } } \ No newline at end of file diff --git a/packages/runtime/src/scope/index.ts b/packages/runtime/src/scope/index.ts index 6dad757..d5c19fe 100644 --- a/packages/runtime/src/scope/index.ts +++ b/packages/runtime/src/scope/index.ts @@ -6,13 +6,13 @@ import type { IVoerkaI18nStorage, Dict, VoerkaI18nLanguagePack, - VoerkaI18nMessageLoader + VoerkaI18nLanguageLoader } from "@/types" import { DefaultLanguageSettings, DefaultFallbackLanguage } from '../consts'; import { Mixin } from "ts-mixer" import { EventEmitterMixin } from "./mixins/eventEmitter" import { PatchMessageMixin } from "./mixins/patch" -import { MessageLoaderMixin } from "./mixins/loader" +import { ChangeLanguageMixin } from "./mixins/change" import { VoerkaI18nLogger } from "../logger"; import { VoerkaI18nFormatters } from "../formatter/types" import { getId } from "@/utils/getId" @@ -29,32 +29,32 @@ import { assignObject } from "flex-tools/object/assignObject" import { VoerkaI18nManager } from "../manager" export interface VoerkaI18nScopeOptions { - id? : string // 作用域唯一id,一般可以使用package.json中的name字段 - debug? : boolean // 是否开启调试模式,开启后会输出调试信息 - library? : boolean // 当使用在库中时应该置为true - languages : VoerkaI18nLanguageDefine[] // 当前作用域支持的语言列表 - fallback? : string // 默认回退语言 - messages : VoerkaI18nLanguageMessagePack // 当前语言包 - idMap? : Voerkai18nIdMap // 消息id映射列表 - storage? : IVoerkaI18nStorage // 语言包存储器 - formatters? : VoerkaI18nFormatters // 当前作用域的格式化 - logger? : VoerkaI18nLogger // 日志记录器 - attached? : boolean // 是否挂接到appScope - messageLoader?: VoerkaI18nMessageLoader // 从远程加载语言包 + id? : string // 作用域唯一id,一般可以使用package.json中的name字段 + debug? : boolean // 是否开启调试模式,开启后会输出调试信息 + library? : boolean // 当使用在库中时应该置为true + languages : VoerkaI18nLanguageDefine[] // 当前作用域支持的语言列表 + fallback? : string // 默认回退语言 + messages : VoerkaI18nLanguageMessagePack // 当前语言包 + idMap? : Voerkai18nIdMap // 消息id映射列表 + storage? : IVoerkaI18nStorage // 语言包存储器 + formatters? : VoerkaI18nFormatters // 当前作用域的格式化 + logger? : VoerkaI18nLogger // 日志记录器 + attached? : boolean // 是否挂接到appScope + sorageKey? : string // 保存到Storeage时的Key + languageLoader?: VoerkaI18nLanguageLoader // 从远程加载语言包 } export type VoerkaI18nScopeFormatterContext = { getFormatterConfig: (configKey?:string )=>T } - export class VoerkaI18nScope extends Mixin( EventEmitterMixin, PatchMessageMixin, - MessageLoaderMixin, + ChangeLanguageMixin, LanguageMixin, TranslateMixin, InterpolatorMixin, @@ -86,12 +86,13 @@ export class VoerkaI18nScope this._init() } get id() { return this._options.id;} // 作用域唯一id - get options(){ return this._options} + get options(){ return this._options} // get attached() { return this._options.attached} // 作用域唯一id get debug(){return this._options.debug } // 是否开启调试模式 get library(){return this._options.library } // 是否是库 @@ -106,25 +107,26 @@ export class VoerkaI18nScope('storage')} - get messageLoader(){ return this.getScopeOption('messageLoader') } + get languageLoader(){ return this.getScopeOption('languageLoader') } /** * 有些配置项是以appScope为准 - * * @param name * @returns */ private getScopeOption(name:string):T | undefined{ const scopeOpts = this._options as any - return (this.attached ? scopeOpts[name] || this._manager.messageLoader : scopeOpts[name]) as T | undefined + // @ts-ignore + return (this.attached ? scopeOpts[name] || (this.library ? this._manager[name] : undefined) : scopeOpts[name]) as T | undefined } private _initOptions(){ @@ -169,12 +171,16 @@ export class VoerkaI18nScopelanguage.name); for(let lang of langs){ - this.manager.storage.remove(`voerkai18n_${this.id}_${lang}_patched_messages`); + this.storage.remove(`voerkai18n_${this.id}_${lang}_patched_messages`); } } } @@ -31,7 +31,7 @@ export class PatchMessageMixin{ * @returns {Promise} 返回补丁包的数量 */ protected async _patch(this:VoerkaI18nScope,messages:VoerkaI18nLanguageMessages, language:string):Promise { - if (!isFunction(this.manager.messageLoader)) return 0; + if (!isFunction(this.languageLoader)) return 0; try { const pachedMessages = (await this._loadMessagesFromLoader(language)) as unknown as VoerkaI18nLanguageMessages; if (isPlainObject(pachedMessages)) { @@ -77,7 +77,7 @@ export class PatchMessageMixin{ * @param {*} messages */ protected _savePatchedMessages(this:VoerkaI18nScope,messages:VoerkaI18nLanguageMessages, language:string) { - if(!this.attached && !this.manager.storage) return + if(!this.attached && !this.storage) return try { this.storage && this.storage.set(`voerkai18n_${this.id}_${language}_patched_messages`,JSON.stringify(messages)); } catch (e:any) { @@ -91,8 +91,8 @@ export class PatchMessageMixin{ */ protected _getPatchedMessages(this:VoerkaI18nScope,language:string) { try { - if(!this.attached && this.manager.storage){ - return this.manager.storage.get(`voerkai18n_${this.id}_${language}_patched_messages`) || {}; + if(this.storage){ + return this.storage.get(`voerkai18n_${this.id}_${language}_patched_messages`) || {}; }else{ return {}; } diff --git a/packages/runtime/src/scope/mixins/restore.ts b/packages/runtime/src/scope/mixins/restore.ts index 7173e9f..7c06e38 100644 --- a/packages/runtime/src/scope/mixins/restore.ts +++ b/packages/runtime/src/scope/mixins/restore.ts @@ -13,24 +13,46 @@ export class RestoreMixin{ const storage = this.storage return isStorage(storage) ? storage: undefined } + /** + * + * @param this + */ + private _getStorageKey(this:VoerkaI18nScope){ + const storageKey = this.options.sorageKey + return storageKey.replace("{scope}",this.id) + } /** * 从存储器加载语言包配置 */ - private _getLanguageFromStorage(this:VoerkaI18nScope){ + restoreLanguage(this:VoerkaI18nScope){ const storage = this._getStorage() - if(storage){ - const savedLanguage = storage.get("language") + if(storage){ + const storageKey = this._getStorageKey() + const savedLanguage = storage.get(storageKey) if(!savedLanguage || !this.hasLanguage(savedLanguage)) return this._activeLanguage = savedLanguage - this.logger.debug("从存储中读取到当前语言:",savedLanguage) + this.logger.debug(`从存储<${storageKey}>中恢复保存的语言:${savedLanguage}`) } } - private _setLanguageToStorage(this:VoerkaI18nScope){ + /** + * + * 将当前语言保存到Storage + * + */ + saveLanguage(this:VoerkaI18nScope){ const storage = this._getStorage() if(storage){ if(!this._activeLanguage) return - storage.set("language",this.activeLanguage) - this.logger.debug("当前语言设置已保存到存储:",this.activeLanguage) + const storageKey = this._getStorageKey() + storage.set(storageKey,this.activeLanguage) + this.logger.debug(`当前语言已保存到存储${storageKey}=${this.activeLanguage}`) } } + clearLanguage(this:VoerkaI18nScope){ + const storage = this._getStorage() + if(storage){ + storage.remove(this._getStorageKey()) + } + } + } \ No newline at end of file diff --git a/packages/runtime/src/types.ts b/packages/runtime/src/types.ts index 5a58d4d..429deaf 100644 --- a/packages/runtime/src/types.ts +++ b/packages/runtime/src/types.ts @@ -12,7 +12,7 @@ export type VoerkaI18nLanguageMessages = { [key in string]?: string | string[] } -export type VoerkaI18nLanguageMessagePack = Record +export type VoerkaI18nLanguageMessagePack = Record export type VoerkaI18nDynamicLanguageMessages = Record & { $config?: VoerkaI18nFormatterConfig @@ -41,7 +41,7 @@ export interface IVoerkaI18nStorage{ } -export type VoerkaI18nMessageLoader = (this:VoerkaI18nScope,newLanguage:string,scope:VoerkaI18nScope)=>Promise +export type VoerkaI18nLanguageLoader = (this:VoerkaI18nScope,newLanguage:string,scope:VoerkaI18nScope)=>Promise export type TranslateMessageVars = number | boolean | string | Function | Date export interface VoerkaI18nTranslate { @@ -65,14 +65,15 @@ declare global { export type VoerkaI18nEvents = { - log : { level: string, message:string } // 当有日志输出时 - init : undefined // 当第一个应用Scope注册时触发 - ready : string // 当初始化切换完成后触发 - change : string // 当语言切换后时, payload=language - restore : { scope:string,language:string } // 当Scope加载并从本地存储中读取语言包合并到语言包时 ,data={language,scope} - patched : { scope:string,language:string } // 当Scope加载并从本地存储中读取语言包合并到语言包时 ,data={language,scope} - error : Error // 当有错误发生时 - "scope/change": { scope:string,language:string } // + log : { level: string, message:string } // 当有日志输出时 + init : undefined // 当第一个应用Scope注册时触发 + ready : string // 当初始化切换完成后触发 + change : string // 当语言切换后时, payload=language + restore : { scope:string,language:string } // 当Scope加载并从本地存储中读取语言包合并到语言包时 ,data={language,scope} + patched : { scope:string,language:string } // 当Scope加载并从本地存储中读取语言包合并到语言包时 ,data={language,scope} + error : Error // 当有错误发生时 + "scope/change" : { scope:string,language:string } // + "scope/fallback": { scope:string,from:string,to:string} // 当切换到无效的语言或者加载失败时,进行回退 } export type Dict = Record