Skip to content

Commit

Permalink
chore: add translations cache
Browse files Browse the repository at this point in the history
Use locale storage to cache translations.

Co-Authored-by: Bertrand Zuchuat <[email protected]>
  • Loading branch information
Garfield-fr committed Aug 22, 2023
1 parent e368767 commit 475b231
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 11 deletions.
4 changes: 3 additions & 1 deletion projects/rero/ng-core/src/lib/core-config.service.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -20,6 +20,7 @@ import { Injectable } from '@angular/core';
* Interface for configuration.
*/
export interface Config {
appVersion?: string;
production?: boolean;
prefixWindow?: string;
apiBaseUrl?: string;
Expand All @@ -39,6 +40,7 @@ export interface Config {
providedIn: 'root'
})
export class CoreConfigService implements Config {
appVersion = undefined;
production = false;
prefixWindow = undefined;
apiBaseUrl = '';
Expand Down
6 changes: 3 additions & 3 deletions projects/rero/ng-core/src/lib/core.module.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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';
Expand All @@ -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: [
Expand Down
22 changes: 22 additions & 0 deletions projects/rero/ng-core/src/lib/interface/icache.ts
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
export interface ICache {
set(key: string, data: any): this;
get(key: string): Observable<any> | undefined;
}
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
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());
});
});
120 changes: 120 additions & 0 deletions projects/rero/ng-core/src/lib/translate/translate-cache.service.ts
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
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: []
}
}
}
25 changes: 18 additions & 7 deletions projects/rero/ng-core/src/lib/translate/translate-loader.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -16,9 +16,10 @@
*/
import { HttpClient } from '@angular/common/http';
import { TranslateLoader as BaseTranslateLoader } from '@ngx-translate/core';
import { forkJoin, Observable, of } from 'rxjs';
import { forkJoin, isObservable, 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';
Expand All @@ -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<any> {
// 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(
Expand Down Expand Up @@ -82,6 +90,9 @@ export class TranslateLoader implements BaseTranslateLoader {
...trans
}
);
if (this._cache) {
this._cache.set(lang, this._translations[lang]);
}
return this._translations[lang];
})
);
Expand Down
2 changes: 2 additions & 0 deletions projects/rero/ng-core/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down

0 comments on commit 475b231

Please sign in to comment.