Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add translations cache #563

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
Comment on lines +19 to +22
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the purpose of using an interface. But it should work without any problem

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: []
}
}
}
23 changes: 17 additions & 6 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 @@ -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';
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