diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index a578c0d8c1..09e472650f 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -1,36 +1,22 @@ -import { - async, - ComponentFixture, - inject, - TestBed -} from '@angular/core/testing'; - -import { - CUSTOM_ELEMENTS_SCHEMA, - DebugElement -} from '@angular/core'; - +import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; +import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; import { CommonModule } from '@angular/common'; - import { By } from '@angular/platform-browser'; +import { ActivatedRoute, Router } from '@angular/router'; -import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { Store, StoreModule } from '@ngrx/store'; +import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; // Load the implementations that should be tested import { AppComponent } from './app.component'; - import { HostWindowState } from './shared/search/host-window.reducer'; import { HostWindowResizeAction } from './shared/host-window.actions'; - import { MetadataService } from './core/metadata/metadata.service'; - -import { GLOBAL_CONFIG, ENV_CONFIG } from '../config'; +import { ENV_CONFIG, GLOBAL_CONFIG } from '../config'; import { NativeWindowRef, NativeWindowService } from './core/services/window.service'; - import { MockTranslateLoader } from './shared/mocks/mock-translate-loader'; import { MockMetadataService } from './shared/mocks/mock-metadata-service'; -import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; import { AngularticsMock } from './shared/mocks/mock-angulartics.service'; import { AuthServiceMock } from './shared/mocks/mock-auth.service'; import { AuthService } from './core/auth/auth.service'; @@ -40,13 +26,11 @@ import { CSSVariableServiceStub } from './shared/testing/css-variable-service-st import { MenuServiceStub } from './shared/testing/menu-service-stub'; import { HostWindowService } from './shared/host-window.service'; import { HostWindowServiceStub } from './shared/testing/host-window-service-stub'; -import { ActivatedRoute, Router } from '@angular/router'; import { RouteService } from './core/services/route.service'; import { MockActivatedRoute } from './shared/mocks/mock-active-router'; import { MockRouter } from './shared/mocks/mock-router'; -import { MockCookieService } from './shared/mocks/mock-cookie.service'; -import { CookieService } from './core/services/cookie.service'; import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider'; +import { LocaleService } from './core/locale/locale.service'; let comp: AppComponent; let fixture: ComponentFixture; @@ -56,6 +40,12 @@ const menuService = new MenuServiceStub(); describe('App component', () => { + function getMockLocaleService(): LocaleService { + return jasmine.createSpyObj('LocaleService', { + setCurrentLanguageCode: jasmine.createSpy('setCurrentLanguageCode') + }) + } + // async beforeEach beforeEach(async(() => { return TestBed.configureTestingModule({ @@ -82,7 +72,7 @@ describe('App component', () => { { provide: MenuService, useValue: menuService }, { provide: CSSVariableService, useClass: CSSVariableServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, - { provide: CookieService, useValue: new MockCookieService()}, + { provide: LocaleService, useValue: getMockLocaleService() }, AppComponent, RouteService ], diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 4e029af8ca..2bd1b5e2b9 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,33 +1,36 @@ import { delay, filter, map, take } from 'rxjs/operators'; -import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, Inject, OnInit, ViewEncapsulation } from '@angular/core'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + HostListener, + Inject, + OnInit, + ViewEncapsulation +} from '@angular/core'; import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router'; +import { BehaviorSubject, combineLatest as combineLatestObservable, Observable, of } from 'rxjs'; import { select, Store } from '@ngrx/store'; - import { TranslateService } from '@ngx-translate/core'; +import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; import { GLOBAL_CONFIG, GlobalConfig } from '../config'; - import { MetadataService } from './core/metadata/metadata.service'; import { HostWindowResizeAction } from './shared/host-window.actions'; import { HostWindowState } from './shared/search/host-window.reducer'; import { NativeWindowRef, NativeWindowService } from './core/services/window.service'; import { isAuthenticated } from './core/auth/selectors'; import { AuthService } from './core/auth/auth.service'; -import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; import variables from '../styles/_exposed_variables.scss'; import { CSSVariableService } from './shared/sass-helper/sass-helper.service'; import { MenuService } from './shared/menu/menu.service'; import { MenuID } from './shared/menu/initial-menus-state'; -import { BehaviorSubject, combineLatest as combineLatestObservable, Observable, of } from 'rxjs'; import { slideSidebarPadding } from './shared/animations/slide'; import { HostWindowService } from './shared/host-window.service'; import { Theme } from '../config/theme.inferface'; -import { isNotEmpty } from './shared/empty.util'; -import { CookieService } from './core/services/cookie.service'; import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider'; - -export const LANG_COOKIE = 'language_cookie'; +import { LocaleService } from './core/locale/locale.service'; @Component({ selector: 'ds-app', @@ -58,29 +61,16 @@ export class AppComponent implements OnInit, AfterViewInit { private cssService: CSSVariableService, private menuService: MenuService, private windowService: HostWindowService, - private cookie: CookieService + private localeService: LocaleService ) { // Load all the languages that are defined as active from the config file translate.addLangs(config.languages.filter((LangConfig) => LangConfig.active === true).map((a) => a.code)); // Load the default language from the config file - translate.setDefaultLang(config.defaultLanguage); + // translate.setDefaultLang(config.defaultLanguage); - // Attempt to get the language from a cookie - const lang = cookie.get(LANG_COOKIE); - if (isNotEmpty(lang)) { - // Cookie found - // Use the language from the cookie - translate.use(lang); - } else { - // Cookie not found - // Attempt to get the browser language from the user - if (translate.getLangs().includes(translate.getBrowserLang())) { - translate.use(translate.getBrowserLang()); - } else { - translate.use(config.defaultLanguage); - } - } + // set the current language code + this.localeService.setCurrentLanguageCode(); angulartics2GoogleAnalytics.startTracking(); angulartics2DSpace.startTracking(); @@ -130,15 +120,15 @@ export class AppComponent implements OnInit, AfterViewInit { // More information on this bug-fix: https://blog.angular-university.io/angular-debugging/ delay(0) ).subscribe((event) => { - if (event instanceof NavigationStart) { - this.isLoading$.next(true); - } else if ( - event instanceof NavigationEnd || - event instanceof NavigationCancel - ) { - this.isLoading$.next(false); - } - }); + if (event instanceof NavigationStart) { + this.isLoading$.next(true); + } else if ( + event instanceof NavigationEnd || + event instanceof NavigationCancel + ) { + this.isLoading$.next(false); + } + }); } @HostListener('window:resize', ['$event']) diff --git a/src/app/core/locale/locale.service.spec.ts b/src/app/core/locale/locale.service.spec.ts new file mode 100644 index 0000000000..c17cdd8089 --- /dev/null +++ b/src/app/core/locale/locale.service.spec.ts @@ -0,0 +1,106 @@ +import { async, TestBed } from '@angular/core/testing'; + +import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; + +import { CookieService } from '../services/cookie.service'; +import { MockCookieService } from '../../shared/mocks/mock-cookie.service'; +import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; +import { LANG_COOKIE, LocaleService } from './locale.service'; + +describe('LocaleService test suite', () => { + let service: LocaleService; + let serviceAsAny: any; + let cookieService: CookieService; + let translateService: TranslateService; + let spyOnGet; + let spyOnSet; + + const config: any = { + defaultLanguage: 'en' + }; + + const langList = ['en', 'it', 'de']; + + beforeEach(async(() => { + return TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + }), + ], + providers: [ + { provide: CookieService, useValue: new MockCookieService() }, + ] + }); + })); + + beforeEach(() => { + cookieService = TestBed.get(CookieService); + translateService = TestBed.get(TranslateService); + service = new LocaleService(config, cookieService, translateService); + serviceAsAny = service; + spyOnGet = spyOn(cookieService, 'get'); + spyOnSet = spyOn(cookieService, 'set'); + }); + + describe('getCurrentLanguageCode', () => { + it('should return language saved on cookie', () => { + spyOnGet.and.returnValue('de'); + expect(service.getCurrentLanguageCode()).toBe('de'); + }); + + describe('', () => { + beforeEach(() => { + spyOn(translateService, 'getLangs').and.returnValue(langList); + }); + + it('should return language from browser setting', () => { + spyOn(translateService, 'getBrowserLang').and.returnValue('it'); + expect(service.getCurrentLanguageCode()).toBe('it'); + }); + + it('should return default language from config', () => { + spyOn(translateService, 'getBrowserLang').and.returnValue('fr'); + expect(service.getCurrentLanguageCode()).toBe('en'); + }); + }); + }); + + describe('getLanguageCodeFromCookie', () => { + it('should return language from cookie', () => { + spyOnGet.and.returnValue('de'); + expect(service.getLanguageCodeFromCookie()).toBe('de'); + }); + + }); + + describe('saveLanguageCodeToCookie', () => { + it('should save language to cookie', () => { + service.saveLanguageCodeToCookie('en'); + expect(spyOnSet).toHaveBeenCalledWith(LANG_COOKIE, 'en'); + }); + }); + + describe('setCurrentLanguageCode', () => { + beforeEach(() => { + spyOn(service, 'saveLanguageCodeToCookie'); + spyOn(translateService, 'use'); + }); + + it('should set the given language', () => { + service.setCurrentLanguageCode('it'); + expect(translateService.use).toHaveBeenCalledWith( 'it'); + expect(service.saveLanguageCodeToCookie).toHaveBeenCalledWith('it'); + }); + + it('should set the current language', () => { + spyOn(service, 'getCurrentLanguageCode').and.returnValue('es'); + service.setCurrentLanguageCode(); + expect(translateService.use).toHaveBeenCalledWith( 'es'); + expect(service.saveLanguageCodeToCookie).toHaveBeenCalledWith('es'); + }); + }); +}); diff --git a/src/app/core/locale/locale.service.ts b/src/app/core/locale/locale.service.ts new file mode 100644 index 0000000000..f70d7f8765 --- /dev/null +++ b/src/app/core/locale/locale.service.ts @@ -0,0 +1,76 @@ +import { Inject, Injectable } from '@angular/core'; + +import { TranslateService } from '@ngx-translate/core'; + +import { isEmpty } from '../../shared/empty.util'; +import { CookieService } from '../services/cookie.service'; +import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; + +export const LANG_COOKIE = 'language_cookie'; + +/** + * Service to provide localization handler + */ +@Injectable({ + providedIn: 'root' +}) +export class LocaleService { + + constructor( + @Inject(GLOBAL_CONFIG) public config: GlobalConfig, + private cookie: CookieService, + private translate: TranslateService) { + } + + /** + * Get the language currently used + * + * @returns {string} The language code + */ + getCurrentLanguageCode(): string { + // Attempt to get the language from a cookie + let lang = this.getLanguageCodeFromCookie(); + if (isEmpty(lang)) { + // Cookie not found + // Attempt to get the browser language from the user + if (this.translate.getLangs().includes(this.translate.getBrowserLang())) { + lang = this.translate.getBrowserLang(); + } else { + lang = this.config.defaultLanguage; + } + } + + return lang; + } + + /** + * Retrieve the language from a cookie + */ + getLanguageCodeFromCookie(): string { + return this.cookie.get(LANG_COOKIE); + } + + /** + * Set the language currently used + * + * @param lang + * The language to save + */ + saveLanguageCodeToCookie(lang: string): void { + this.cookie.set(LANG_COOKIE, lang); + } + + /** + * Set the language currently used + * + * @param lang + * The language to set, if it's not provided retrieve default one + */ + setCurrentLanguageCode(lang?: string): void { + if (isEmpty(lang)) { + lang = this.getCurrentLanguageCode() + } + this.translate.use(lang); + this.saveLanguageCodeToCookie(lang); + } +} diff --git a/src/app/shared/lang-switch/lang-switch.component.spec.ts b/src/app/shared/lang-switch/lang-switch.component.spec.ts index 3d7aca46b6..b0f242a2e0 100644 --- a/src/app/shared/lang-switch/lang-switch.component.spec.ts +++ b/src/app/shared/lang-switch/lang-switch.component.spec.ts @@ -1,14 +1,15 @@ -import {LangSwitchComponent} from './lang-switch.component'; -import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import {DebugElement, NO_ERRORS_SCHEMA} from '@angular/core'; -import {TranslateLoader, TranslateModule, TranslateService} from '@ngx-translate/core'; -import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing'; -import { GLOBAL_CONFIG } from '../../../config'; -import {LangConfig} from '../../../config/lang-config.interface'; -import {Observable, of} from 'rxjs'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; -import { MockCookieService } from '../mocks/mock-cookie.service'; -import { CookieService } from '../../core/services/cookie.service'; + +import { Observable, of } from 'rxjs'; +import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { LangSwitchComponent } from './lang-switch.component'; +import { GLOBAL_CONFIG } from '../../../config'; +import { LangConfig } from '../../../config/lang-config.interface'; +import { LocaleService } from '../../core/locale/locale.service'; // This test is completely independent from any message catalogs or keys in the codebase // The translation module is instantiated with these bogus messages that we aren't using anyway. @@ -28,16 +29,19 @@ class CustomLoader implements TranslateLoader { }); } } + /* tslint:enable:quotemark */ /* tslint:enable:object-literal-key-quotes */ -let cookie: CookieService; +let localService: any; describe('LangSwitchComponent', () => { - beforeEach(() => { - cookie = Object.assign(new MockCookieService()); - }); + function getMockLocaleService(): LocaleService { + return jasmine.createSpyObj('LocaleService', { + setCurrentLanguageCode: jasmine.createSpy('setCurrentLanguageCode') + }) + } describe('with English and Deutsch activated, English as default', () => { let component: LangSwitchComponent; @@ -65,7 +69,7 @@ describe('LangSwitchComponent', () => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule, TranslateModule.forRoot( { - loader: {provide: TranslateLoader, useClass: CustomLoader} + loader: { provide: TranslateLoader, useClass: CustomLoader } } )], declarations: [LangSwitchComponent], @@ -73,7 +77,7 @@ describe('LangSwitchComponent', () => { providers: [ TranslateService, { provide: GLOBAL_CONFIG, useValue: mockConfig }, - { provide: CookieService, useValue: cookie } + { provide: LocaleService, useValue: getMockLocaleService() }, ] }).compileComponents() .then(() => { @@ -83,6 +87,7 @@ describe('LangSwitchComponent', () => { translate.use('en'); http = TestBed.get(HttpTestingController); fixture = TestBed.createComponent(LangSwitchComponent); + localService = TestBed.get(LocaleService); component = fixture.componentInstance; de = fixture.debugElement; langSwitchElement = de.nativeElement; @@ -111,19 +116,15 @@ describe('LangSwitchComponent', () => { describe('when selecting a language', () => { beforeEach(() => { spyOn(translate, 'use'); - spyOn(cookie, 'set'); const langItem = fixture.debugElement.query(By.css('.dropdown-item')).nativeElement; langItem.click(); fixture.detectChanges(); }); - it('should translate the app', () => { - expect(translate.use).toHaveBeenCalled(); + it('should translate the app and set the client\'s language cookie', () => { + expect(localService.setCurrentLanguageCode).toHaveBeenCalled(); }); - it('should set the client\'s language cookie', () => { - expect(cookie.set).toHaveBeenCalled(); - }); }); }); @@ -154,7 +155,7 @@ describe('LangSwitchComponent', () => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule, TranslateModule.forRoot( { - loader: {provide: TranslateLoader, useClass: CustomLoader} + loader: { provide: TranslateLoader, useClass: CustomLoader } } )], declarations: [LangSwitchComponent], @@ -162,7 +163,7 @@ describe('LangSwitchComponent', () => { providers: [ TranslateService, { provide: GLOBAL_CONFIG, useValue: mockConfig }, - { provide: CookieService, useValue: cookie } + { provide: LocaleService, useValue: getMockLocaleService() }, ] }).compileComponents(); translate = TestBed.get(TranslateService); diff --git a/src/app/shared/lang-switch/lang-switch.component.ts b/src/app/shared/lang-switch/lang-switch.component.ts index e91ed5c9a2..ee301c8d54 100644 --- a/src/app/shared/lang-switch/lang-switch.component.ts +++ b/src/app/shared/lang-switch/lang-switch.component.ts @@ -1,9 +1,10 @@ -import {Component, Inject, OnInit} from '@angular/core'; +import { Component, Inject, OnInit } from '@angular/core'; + +import { TranslateService } from '@ngx-translate/core'; + import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; -import {TranslateService} from '@ngx-translate/core'; -import {LangConfig} from '../../../config/lang-config.interface'; -import { LANG_COOKIE } from '../../app.component'; -import { CookieService } from '../../core/services/cookie.service'; +import { LangConfig } from '../../../config/lang-config.interface'; +import { LocaleService } from '../../core/locale/locale.service'; @Component({ selector: 'ds-lang-switch', @@ -26,7 +27,7 @@ export class LangSwitchComponent implements OnInit { constructor( @Inject(GLOBAL_CONFIG) public config: GlobalConfig, public translate: TranslateService, - public cookie: CookieService + private localeService: LocaleService ) { } @@ -54,8 +55,7 @@ export class LangSwitchComponent implements OnInit { * @param lang The language to switch to */ useLang(lang: string) { - this.translate.use(lang); - this.cookie.set(LANG_COOKIE, lang); + this.localeService.setCurrentLanguageCode(lang); } }