From 7e17ea53a6b0e8313485c3dee1b40bfc44124356 Mon Sep 17 00:00:00 2001 From: Bertrand Zuchuat Date: Tue, 22 Aug 2023 15:33:20 +0200 Subject: [PATCH] chore: add translations cache Use locale storage to cache translations. Co-Authored-by: Bertrand Zuchuat --- .../ng-core/src/lib/core-config.service.ts | 4 +- projects/rero/ng-core/src/lib/core.module.ts | 6 +- .../rero/ng-core/src/lib/interface/icache.ts | 22 ++++ .../translate/translate-cache.service.spec.ts | 81 ++++++++++++ .../lib/translate/translate-cache.service.ts | 120 ++++++++++++++++++ .../src/lib/translate/translate-loader.ts | 23 +++- projects/rero/ng-core/src/public-api.ts | 2 + 7 files changed, 248 insertions(+), 10 deletions(-) create mode 100644 projects/rero/ng-core/src/lib/interface/icache.ts create mode 100644 projects/rero/ng-core/src/lib/translate/translate-cache.service.spec.ts create mode 100644 projects/rero/ng-core/src/lib/translate/translate-cache.service.ts diff --git a/projects/rero/ng-core/src/lib/core-config.service.ts b/projects/rero/ng-core/src/lib/core-config.service.ts index 5808b71f..16d47306 100644 --- a/projects/rero/ng-core/src/lib/core-config.service.ts +++ b/projects/rero/ng-core/src/lib/core-config.service.ts @@ -1,6 +1,6 @@ /* * RERO angular core - * Copyright (C) 2020 RERO + * Copyright (C) 2020-2023 RERO * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -20,6 +20,7 @@ import { Injectable } from '@angular/core'; * Interface for configuration. */ export interface Config { + appVersion?: string; production?: boolean; prefixWindow?: string; apiBaseUrl?: string; @@ -39,6 +40,7 @@ export interface Config { providedIn: 'root' }) export class CoreConfigService implements Config { + appVersion = undefined; production = false; prefixWindow = undefined; apiBaseUrl = ''; diff --git a/projects/rero/ng-core/src/lib/core.module.ts b/projects/rero/ng-core/src/lib/core.module.ts index b1150677..0d749bbd 100644 --- a/projects/rero/ng-core/src/lib/core.module.ts +++ b/projects/rero/ng-core/src/lib/core.module.ts @@ -1,6 +1,6 @@ /* * RERO angular core - * Copyright (C) 2020 RERO + * Copyright (C) 2020-2023 RERO * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -25,8 +25,10 @@ import { NgxSpinnerModule } from 'ngx-spinner'; import { ToastrModule } from 'ngx-toastr'; import { CoreConfigService } from './core-config.service'; import { DialogComponent } from './dialog/dialog.component'; +import { AutofocusDirective } from './directives/autofocus.directive'; import { NgVarDirective } from './directives/ng-var.directive'; import { ErrorComponent } from './error/error.component'; +import { ComponentCanDeactivateGuard } from './guard/component-can-deactivate.guard'; import { MenuWidgetComponent } from './menu/menu-widget/menu-widget.component'; import { CallbackArrayFilterPipe } from './pipe/callback-array-filter.pipe'; import { DefaultPipe } from './pipe/default.pipe'; @@ -43,8 +45,6 @@ import { TranslateLanguagePipe } from './translate/translate-language.pipe'; import { TranslateLoader } from './translate/translate-loader'; import { MenuComponent } from './widget/menu/menu.component'; import { SortListComponent } from './widget/sort-list/sort-list.component'; -import { AutofocusDirective } from './directives/autofocus.directive'; -import { ComponentCanDeactivateGuard } from './guard/component-can-deactivate.guard'; @NgModule({ declarations: [ diff --git a/projects/rero/ng-core/src/lib/interface/icache.ts b/projects/rero/ng-core/src/lib/interface/icache.ts new file mode 100644 index 00000000..e9c54036 --- /dev/null +++ b/projects/rero/ng-core/src/lib/interface/icache.ts @@ -0,0 +1,22 @@ +import { Observable } from "rxjs"; + +/* + * RERO angular core + * Copyright (C) 2020-2023 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export interface ICache { + set(key: string, data: any): this; + get(key: string): Observable | undefined; +} diff --git a/projects/rero/ng-core/src/lib/translate/translate-cache.service.spec.ts b/projects/rero/ng-core/src/lib/translate/translate-cache.service.spec.ts new file mode 100644 index 00000000..c5958248 --- /dev/null +++ b/projects/rero/ng-core/src/lib/translate/translate-cache.service.spec.ts @@ -0,0 +1,81 @@ +/* + * RERO angular core + * Copyright (C) 2020-2023 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { TestBed } from '@angular/core/testing'; +import { TranslateCacheService } from './translate-cache.service'; +import { CoreConfigService } from '../core-config.service'; +import { LocalStorageService } from '../service/local-storage.service'; + +class AppConfigService extends CoreConfigService { + constructor() { + super(); + this.appVersion = '1.0.0'; + } +} + +export const translations = { + foo: 'bar' +}; + +export const historyLocaleStorage1 = { + version: '1.0.0', + storageName: ['translations_fr', 'translations_de'] +}; + +export const historyLocaleStorage11 = { + version: '1.0.1', + storageName: [] +}; + +describe('TranslateCacheService', () => { + let service: TranslateCacheService; + let localeStorage: LocalStorageService; + let appConfigService: AppConfigService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: CoreConfigService, useClass: AppConfigService } + ] + }); + service = TestBed.inject(TranslateCacheService); + localeStorage = TestBed.inject(LocalStorageService); + appConfigService = TestBed.inject(CoreConfigService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('Should return the correct cache values', () => { + localeStorage.clear(); + expect(service.get('fr')).toBeUndefined(); + service.set('fr', translations); + expect(service.get('fr')).toEqual(translations); + expect(service.get('de')).toBeUndefined(); + service.set('de', translations); + const history = localeStorage.get(service.translateHistoryName); + expect(history).toEqual(historyLocaleStorage1); + const {storageName} = history; + storageName.forEach((name: string) => expect(localeStorage.has(name)).toBeTrue()); + // Application version change + // The translation cache is cleared + appConfigService.appVersion = '1.0.1'; + expect(service.get('fr')).toBeUndefined(); + expect(localeStorage.get(service.translateHistoryName)).toEqual(historyLocaleStorage11); + storageName.forEach((name: string) => expect(localeStorage.has(name)).toBeFalse()); + }); +}); diff --git a/projects/rero/ng-core/src/lib/translate/translate-cache.service.ts b/projects/rero/ng-core/src/lib/translate/translate-cache.service.ts new file mode 100644 index 00000000..2ea8d1b3 --- /dev/null +++ b/projects/rero/ng-core/src/lib/translate/translate-cache.service.ts @@ -0,0 +1,120 @@ +/* + * RERO angular core + * Copyright (C) 2020-2023 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Injectable } from '@angular/core'; +import { CoreConfigService } from '../core-config.service'; +import { ICache } from '../interface/icache'; +import { LocalStorageService } from '../service/local-storage.service'; + +export interface ITranslations { + version: string; + storageName: string[]; +} + +@Injectable({ + providedIn: 'root' +}) +export class TranslateCacheService implements ICache { + + /** Name of local storage history */ + readonly translateHistoryName: string = 'translations'; + + /** + * Constructor + * @param localeStorage LocalStorageService + * @param coreConfigService CoreConfigService + */ + constructor( + private localeStorage: LocalStorageService, + private coreConfigService: CoreConfigService + ) { + if (this.coreConfigService.appVersion === undefined) { + throw new Error('Please set your application version to use the cache.'); + } + } + + /** + * Adding content to locale storage + * @param key - Locale storage key + * @param data - Locale storage contents + * @returns TranslateCacheService + */ + set(key: string, data: any): this { + const history = this.localeStorage.has(this.translateHistoryName) + ? this.localeStorage.get(this.translateHistoryName) + : this.initializeTranslationsHistory(); + const name = this.storageName(key); + if (!history.storageName.includes(name)) { + history.storageName.push(name); + this.localeStorage.set(this.translateHistoryName, history); + } + this.localeStorage.set(name, data); + return this; + } + + /** + * Retrieving content from locale storage + * @param key - Locale storage key + * @returns The contents of the locale storage or undefined + */ + get(key: string): any | undefined { + if (this.localeStorage.has(this.translateHistoryName)) { + const history = this.localeStorage.get(this.translateHistoryName); + if (history.version !== this.coreConfigService.appVersion) { + this.resetTranslationsHistory(history); + } else { + const name = this.storageName(key); + if (history.storageName.includes(name) && this.localeStorage.has(name)) { + return this.localeStorage.get(name); + } + } + } + return; + } + + /** + * Key generation for translations + * @param language - current language + * @returns Key name for locale storage + */ + private storageName(language: string): string { + return `${this.translateHistoryName}_${language}`; + } + + /** + * Delete languages in locale storage and reset object history. + * @param history - history object (ITranslations) + */ + private resetTranslationsHistory(history: ITranslations): void { + history.storageName.forEach(name => { + if (this.localeStorage.has(name)) { + this.localeStorage.remove(name); + } + }); + this.localeStorage.set(this.translateHistoryName, this.initializeTranslationsHistory()); + } + + /** + * Generate object history stored in locale storage. + * @returns - ITranslations + */ + private initializeTranslationsHistory(): ITranslations { + return { + version: this.coreConfigService.appVersion, + storageName: [] + } + } +} diff --git a/projects/rero/ng-core/src/lib/translate/translate-loader.ts b/projects/rero/ng-core/src/lib/translate/translate-loader.ts index 30ea0615..8cf55ea8 100644 --- a/projects/rero/ng-core/src/lib/translate/translate-loader.ts +++ b/projects/rero/ng-core/src/lib/translate/translate-loader.ts @@ -1,6 +1,6 @@ /* * RERO angular core - * Copyright (C) 2020 RERO + * Copyright (C) 2020-2023 RERO * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -19,6 +19,7 @@ import { TranslateLoader as BaseTranslateLoader } from '@ngx-translate/core'; import { forkJoin, Observable, of } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; import { CoreConfigService } from '../core-config.service'; +import { ICache } from '../interface/icache'; import de from './i18n/de.json'; import en from './i18n/en.json'; import fr from './i18n/fr.json'; @@ -36,25 +37,32 @@ export class TranslateLoader implements BaseTranslateLoader { // with angular<9 assets are not available for libraries // See: https://angular.io/guide/creating-libraries#managing-assets-in-a-library private _coreTranslations = { de, en, fr, it }; + /** * Constructor. * * @param _coreConfigService Configuration service. + * @param _http HttpClient + * @param _cache ICache */ constructor( private _coreConfigService: CoreConfigService, - private _http: HttpClient + private _http: HttpClient, + private _cache?: ICache ) { } /** * Return observable used by ngx-translate to get translations. - * @param lang - string, language to rerieve translations from. + * @param lang - string, language to retrieve translations from. */ getTranslation(lang: string): Observable { - // Already in cache - if (this._translations[lang] != null) { - return of(this._translations[lang]); + if (this._cache) { + const translateCacheData = this._cache.get(lang); + if (translateCacheData) { + return translateCacheData; + } } + const urls = this._coreConfigService.translationsURLs; // create the list of http requests const observers = urls.map( @@ -82,6 +90,9 @@ export class TranslateLoader implements BaseTranslateLoader { ...trans } ); + if (this._cache) { + this._cache.set(lang, this._translations[lang]); + } return this._translations[lang]; }) ); diff --git a/projects/rero/ng-core/src/public-api.ts b/projects/rero/ng-core/src/public-api.ts index 584354b0..d35e4bf8 100644 --- a/projects/rero/ng-core/src/public-api.ts +++ b/projects/rero/ng-core/src/public-api.ts @@ -27,6 +27,7 @@ export * from './lib/directives/ng-var.directive'; export * from './lib/error/error'; export * from './lib/error/error.component'; export * from './lib/guard/component-can-deactivate.guard'; +export * from './lib/interface/icache'; export * from './lib/menu/menu-factory'; export * from './lib/menu/menu-item'; export * from './lib/menu/menu-item-interface'; @@ -76,6 +77,7 @@ export * from './lib/service/logger.service'; export * from './lib/service/title-meta.service'; export * from './lib/text-read-more/text-read-more.component'; export * from './lib/translate/date-translate-pipe'; +export * from './lib/translate/translate-cache.service'; export * from './lib/translate/translate-language.pipe'; export * from './lib/translate/translate-language.service'; export * from './lib/translate/translate-loader';