diff --git a/src/app/core/locale/locale.interceptor.spec.ts b/src/app/core/locale/locale.interceptor.spec.ts index 4731e109e5d..d832b6255b3 100644 --- a/src/app/core/locale/locale.interceptor.spec.ts +++ b/src/app/core/locale/locale.interceptor.spec.ts @@ -12,6 +12,7 @@ import { of } from 'rxjs'; import { RestRequestMethod } from '../data/rest-request-method'; import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { LocaleInterceptor } from './locale.interceptor'; import { LocaleService } from './locale.service'; @@ -27,6 +28,10 @@ describe(`LocaleInterceptor`, () => { getLanguageCodeList: of(languageList), }); + const mockHalEndpointService = { + getRootHref: jasmine.createSpy('getRootHref'), + }; + beforeEach(() => { TestBed.configureTestingModule({ imports: [], @@ -37,6 +42,7 @@ describe(`LocaleInterceptor`, () => { useClass: LocaleInterceptor, multi: true, }, + { provide: HALEndpointService, useValue: mockHalEndpointService }, { provide: LocaleService, useValue: mockLocaleService }, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting(), @@ -47,7 +53,7 @@ describe(`LocaleInterceptor`, () => { httpMock = TestBed.inject(HttpTestingController); localeService = TestBed.inject(LocaleService); - localeService.getCurrentLanguageCode.and.returnValue('en'); + localeService.getCurrentLanguageCode.and.returnValue(of('en')); }); describe('', () => { diff --git a/src/app/core/locale/locale.interceptor.ts b/src/app/core/locale/locale.interceptor.ts index 6dfa19485d9..a415ab8c514 100644 --- a/src/app/core/locale/locale.interceptor.ts +++ b/src/app/core/locale/locale.interceptor.ts @@ -9,14 +9,19 @@ import { Observable } from 'rxjs'; import { mergeMap, scan, + take, } from 'rxjs/operators'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { LocaleService } from './locale.service'; @Injectable() export class LocaleInterceptor implements HttpInterceptor { - constructor(private localeService: LocaleService) { + constructor( + protected halEndpointService: HALEndpointService, + protected localeService: LocaleService, + ) { } /** @@ -26,8 +31,9 @@ export class LocaleInterceptor implements HttpInterceptor { */ intercept(req: HttpRequest, next: HttpHandler): Observable> { let newReq: HttpRequest; - return this.localeService.getLanguageCodeList() + return this.localeService.getLanguageCodeList(req.url === this.halEndpointService.getRootHref()) .pipe( + take(1), scan((acc: any, value: any) => [...acc, value], []), mergeMap((languages) => { // Clone the request to add the new header. diff --git a/src/app/core/locale/locale.service.spec.ts b/src/app/core/locale/locale.service.spec.ts index 6b1120e2913..f075ff1f251 100644 --- a/src/app/core/locale/locale.service.spec.ts +++ b/src/app/core/locale/locale.service.spec.ts @@ -7,9 +7,12 @@ import { TranslateModule, TranslateService, } from '@ngx-translate/core'; +import { of } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock'; import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; +import { EPersonMock2 } from '../../shared/testing/eperson.mock'; import { routeServiceStub } from '../../shared/testing/route-service.stub'; import { AuthService } from '../auth/auth.service'; import { CookieService } from '../services/cookie.service'; @@ -36,6 +39,7 @@ describe('LocaleService', () => { authService = jasmine.createSpyObj('AuthService', { isAuthenticated: jasmine.createSpy('isAuthenticated'), isAuthenticationLoaded: jasmine.createSpy('isAuthenticationLoaded'), + getAuthenticatedUserFromStore: jasmine.createSpy('getAuthenticatedUserFromStore'), }); const langList = ['en', 'xx', 'de']; @@ -72,33 +76,80 @@ describe('LocaleService', () => { }); describe('getCurrentLanguageCode', () => { + let testScheduler: TestScheduler; + beforeEach(() => { spyOn(translateService, 'getLangs').and.returnValue(langList); + testScheduler = new TestScheduler((actual, expected) => { + // use jasmine to test equality + expect(actual).toEqual(expected); + }); + authService.isAuthenticated.and.returnValue(of(false)); + authService.isAuthenticationLoaded.and.returnValue(of(false)); }); it('should return the language saved on cookie if it\'s a valid & active language', () => { spyOnGet.and.returnValue('de'); - expect(service.getCurrentLanguageCode()).toBe('de'); + testScheduler.run(({ expectObservable }) => { + expectObservable(service.getCurrentLanguageCode()).toBe('(a|)', { a: 'de' }); + }); }); it('should return the fallback language if the cookie language is disabled', () => { spyOnGet.and.returnValue('disabled'); - expect(service.getCurrentLanguageCode()).toBe('en'); + testScheduler.run(({ expectObservable }) => { + expectObservable(service.getCurrentLanguageCode()).toBe('(a|)', { a: 'en' }); + }); }); it('should return the fallback language if the cookie language does not exist', () => { spyOnGet.and.returnValue('does-not-exist'); - expect(service.getCurrentLanguageCode()).toBe('en'); + testScheduler.run(({ expectObservable }) => { + expectObservable(service.getCurrentLanguageCode()).toBe('(a|)', { a: 'en' }); + }); }); it('should return language from browser setting', () => { - spyOn(translateService, 'getBrowserLang').and.returnValue('xx'); - expect(service.getCurrentLanguageCode()).toBe('xx'); + spyOn(service, 'getLanguageCodeList').and.returnValue(of(['xx', 'en'])); + testScheduler.run(({ expectObservable }) => { + expectObservable(service.getCurrentLanguageCode()).toBe('(a|)', { a: 'xx' }); + }); + }); + + it('should match language from browser setting case insensitive', () => { + spyOn(service, 'getLanguageCodeList').and.returnValue(of(['DE', 'en'])); + testScheduler.run(({ expectObservable }) => { + expectObservable(service.getCurrentLanguageCode()).toBe('(a|)', { a: 'DE' }); + }); + }); + }); + + describe('getLanguageCodeList', () => { + let testScheduler: TestScheduler; + + beforeEach(() => { + spyOn(translateService, 'getLangs').and.returnValue(langList); + testScheduler = new TestScheduler((actual, expected) => { + // use jasmine to test equality + expect(actual).toEqual(expected); + }); + }); + + it('should return default language list without user preferred language when no logged in user', () => { + authService.isAuthenticated.and.returnValue(of(false)); + authService.isAuthenticationLoaded.and.returnValue(of(false)); + testScheduler.run(({ expectObservable }) => { + expectObservable(service.getLanguageCodeList()).toBe('(a|)', { a: ['en-US;q=1', 'en;q=0.9'] }); + }); }); - it('should return default language from config', () => { - spyOn(translateService, 'getBrowserLang').and.returnValue('fr'); - expect(service.getCurrentLanguageCode()).toBe('en'); + it('should return default language list with user preferred language when user is logged in', () => { + authService.isAuthenticated.and.returnValue(of(true)); + authService.isAuthenticationLoaded.and.returnValue(of(true)); + authService.getAuthenticatedUserFromStore.and.returnValue(of(EPersonMock2)); + testScheduler.run(({ expectObservable }) => { + expectObservable(service.getLanguageCodeList()).toBe('(a|)', { a: ['fr;q=0.5', 'en-US;q=1', 'en;q=0.9'] }); + }); }); }); @@ -130,14 +181,13 @@ describe('LocaleService', () => { }); it('should set the current language', () => { - spyOn(service, 'getCurrentLanguageCode').and.returnValue('es'); + spyOn(service, 'getCurrentLanguageCode').and.returnValue(of('es')); service.setCurrentLanguageCode(); expect(translateService.use).toHaveBeenCalledWith('es'); - expect(service.saveLanguageCodeToCookie).toHaveBeenCalledWith('es'); }); it('should set the current language on the html tag', () => { - spyOn(service, 'getCurrentLanguageCode').and.returnValue('es'); + spyOn(service, 'getCurrentLanguageCode').and.returnValue(of('es')); service.setCurrentLanguageCode(); expect((service as any).document.documentElement.lang).toEqual('es'); }); diff --git a/src/app/core/locale/locale.service.ts b/src/app/core/locale/locale.service.ts index 4fc7d2378c2..a51988f9f0c 100644 --- a/src/app/core/locale/locale.service.ts +++ b/src/app/core/locale/locale.service.ts @@ -2,12 +2,14 @@ import { DOCUMENT, Inject, Injectable, + OnDestroy, } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { combineLatest, Observable, of, + Subscription, } from 'rxjs'; import { map, @@ -18,6 +20,7 @@ import { import { LangConfig } from '../../../config/lang-config.interface'; import { environment } from '../../../environments/environment'; import { + hasValue, isEmpty, isNotEmpty, } from '../../shared/empty.util'; @@ -44,13 +47,15 @@ export enum LANG_ORIGIN { * Service to provide localization handler */ @Injectable() -export class LocaleService { +export class LocaleService implements OnDestroy { /** * Eperson language metadata */ EPERSON_LANG_METADATA = 'eperson.language'; + subs: Subscription[] = []; + constructor( @Inject(NativeWindowService) protected _window: NativeWindowRef, protected cookie: CookieService, @@ -64,20 +69,25 @@ export class LocaleService { /** * Get the language currently used * - * @returns {string} The language code + * @returns {Observable} The language code */ - getCurrentLanguageCode(): string { + getCurrentLanguageCode(): Observable { // Attempt to get the language from a cookie - let lang = this.getLanguageCodeFromCookie(); + const lang = this.getLanguageCodeFromCookie(); if (isEmpty(lang) || environment.languages.find((langConfig: LangConfig) => langConfig.code === lang && langConfig.active) === undefined) { // Attempt to get the browser language from the user - if (this.translate.getLangs().includes(this.translate.getBrowserLang())) { - lang = this.translate.getBrowserLang(); - } else { - lang = environment.fallbackLanguage; - } + return this.getLanguageCodeList() + .pipe( + map(browserLangs => { + return browserLangs + .map(browserLang => browserLang.split(';')[0]) + .find(browserLang => + this.translate.getLangs().some(userLang => userLang.toLowerCase() === browserLang.toLowerCase()), + ) || environment.fallbackLanguage; + }), + ); } - return lang; + return of(lang); } /** @@ -85,18 +95,16 @@ export class LocaleService { * * @returns {Observable} */ - getLanguageCodeList(): Observable { + getLanguageCodeList(ignoreEPersonSettings = false): Observable { const obs$ = combineLatest([ this.authService.isAuthenticated(), this.authService.isAuthenticationLoaded(), ]); return obs$.pipe( - take(1), mergeMap(([isAuthenticated, isLoaded]) => { - // TODO to enabled again when https://github.com/DSpace/dspace-angular/issues/739 will be resolved - const epersonLang$: Observable = of([]); - /* if (isAuthenticated && isLoaded) { + let epersonLang$: Observable = of([]); + if (isAuthenticated && isLoaded && !ignoreEPersonSettings) { epersonLang$ = this.authService.getAuthenticatedUserFromStore().pipe( take(1), map((eperson) => { @@ -109,21 +117,21 @@ export class LocaleService { !isEmpty(this.translate.getCurrentLang()))); } return languages; - }) + }), ); - }*/ + } return epersonLang$.pipe( map((epersonLang: string[]) => { const languages: string[] = []; + if (isNotEmpty(epersonLang)) { + languages.push(...epersonLang); + } if (this.translate.currentLang) { languages.push(...this.setQuality( [this.translate.getCurrentLang()], LANG_ORIGIN.UI, false)); } - if (isNotEmpty(epersonLang)) { - languages.push(...epersonLang); - } if (navigator.languages) { languages.push(...this.setQuality( Object.assign([], navigator.languages), @@ -163,11 +171,16 @@ export class LocaleService { */ setCurrentLanguageCode(lang?: string): void { if (isEmpty(lang)) { - lang = this.getCurrentLanguageCode(); + this.subs.push(this.getCurrentLanguageCode().subscribe(curLang => { + lang = curLang; + this.translate.use(lang); + this.document.documentElement.lang = lang; + })); + } else { + this.saveLanguageCodeToCookie(lang); + this.translate.use(lang); + this.document.documentElement.lang = lang; } - this.translate.use(lang); - this.saveLanguageCodeToCookie(lang); - this.document.documentElement.lang = lang; } /** @@ -213,4 +226,10 @@ export class LocaleService { } + ngOnDestroy(): void { + this.subs + .filter((sub) => hasValue(sub)) + .forEach((sub) => sub.unsubscribe()); + } + } diff --git a/src/app/core/locale/server-locale.service.ts b/src/app/core/locale/server-locale.service.ts index c81ef0fb5b8..3653cfb740c 100644 --- a/src/app/core/locale/server-locale.service.ts +++ b/src/app/core/locale/server-locale.service.ts @@ -53,7 +53,7 @@ export class ServerLocaleService extends LocaleService { * * @returns {Observable} */ - getLanguageCodeList(): Observable { + getLanguageCodeList(ignoreEPersonSettings = false): Observable { const obs$ = combineLatest([ this.authService.isAuthenticated(), this.authService.isAuthenticationLoaded(),