Skip to content
Merged
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
10 changes: 8 additions & 2 deletions apps/i18n-test/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { RouterOutlet } from '@angular/router';
import { NgxI18nService } from '@ngx/i18n';
import { NgxI18nRootService, NgxI18nService } from '@ngx/i18n';

@Component({
selector: 'app-root',
Expand All @@ -11,7 +11,13 @@ import { NgxI18nService } from '@ngx/i18n';
imports: [RouterOutlet, TranslateModule],
})
export class AppComponent {
constructor(private readonly i18nService: NgxI18nService) {
constructor(
private readonly i18nService: NgxI18nService,
private readonly rootService: NgxI18nRootService
) {
setTimeout(() => {
rootService.setAvailableLanguages(['nl', 'fr']);
}, 3000);
i18nService.initI18n('nl').subscribe();
}
}
1 change: 0 additions & 1 deletion apps/i18n-test/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ bootstrapApplication(AppComponent, {
importProvidersFrom(BrowserModule, AppRoutingModule),
importNgxI18nProviders({
defaultLanguage: 'nl',
availableLanguages: ['nl', 'fr'],
defaultAssetPaths: ['./assets/shared/'],
}),
provideHttpClient(withInterceptorsFromDi()),
Expand Down
48 changes: 34 additions & 14 deletions libs/i18n/src/lib/guards/i18n/i18n.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { TestBed } from '@angular/core/testing';
import { ActivatedRouteSnapshot, Router, convertToParamMap } from '@angular/router';
import { subscribeSpyTo } from '@hirez_io/observer-spy';

import { Observable, of } from 'rxjs';
import { NgxI18nRootService } from '../../services';

import { NgxI18nConfigurationToken } from '../../tokens/i18n.token';
import { NgxI18nGuard } from './i18n.guard';

describe('NgxI18nGuard', () => {
Expand All @@ -15,18 +16,13 @@ describe('NgxI18nGuard', () => {
availableLanguages: ['nl', 'en'],
setCurrentLanguage: jasmine.createSpy(),
initializeLanguage: jasmine.createSpy(),
availableLanguages$: of(['nl', 'en']),
defaultLanguage: 'nl',
};

beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{
provide: NgxI18nConfigurationToken,
useValue: {
defaultLanguage: 'nl',
availableLanguages: ['nl', 'en'],
},
},
{
provide: NgxI18nRootService,
useValue: i18nService,
Expand All @@ -45,7 +41,11 @@ describe('NgxI18nGuard', () => {
paramMap: convertToParamMap({ language: 'nl' }),
} as ActivatedRouteSnapshot;

expect(NgxI18nGuard(route, undefined)).toBe(true);
expect(
subscribeSpyTo(
NgxI18nGuard(route, undefined) as Observable<boolean>
).getFirstValue()
).toBe(true);

route = {
parent: {
Expand All @@ -54,7 +54,11 @@ describe('NgxI18nGuard', () => {
paramMap: convertToParamMap({}),
} as ActivatedRouteSnapshot;

expect(NgxI18nGuard(route, undefined)).toBe(true);
expect(
subscribeSpyTo(
NgxI18nGuard(route, undefined) as Observable<boolean>
).getFirstValue()
).toBe(true);
});
});

Expand All @@ -64,7 +68,11 @@ describe('NgxI18nGuard', () => {
paramMap: convertToParamMap({ language: 'en' }),
} as ActivatedRouteSnapshot;

expect(NgxI18nGuard(route, undefined)).toBe(true);
expect(
subscribeSpyTo(
NgxI18nGuard(route, undefined) as Observable<boolean>
).getFirstValue()
).toBe(true);
expect(i18nService.setCurrentLanguage).toHaveBeenCalledWith('en');
expect(router.navigate).toHaveBeenCalledWith(['/', 'en']);

Expand All @@ -75,7 +83,11 @@ describe('NgxI18nGuard', () => {
paramMap: convertToParamMap({}),
} as ActivatedRouteSnapshot;

expect(NgxI18nGuard(route, undefined)).toBe(true);
expect(
subscribeSpyTo(
NgxI18nGuard(route, undefined) as Observable<boolean>
).getFirstValue()
).toBe(true);
expect(i18nService.setCurrentLanguage).toHaveBeenCalledWith('en');
expect(router.navigate).toHaveBeenCalledWith(['/', 'en']);
});
Expand All @@ -87,7 +99,11 @@ describe('NgxI18nGuard', () => {
paramMap: convertToParamMap({ language: 'de' }),
} as ActivatedRouteSnapshot;

expect(NgxI18nGuard(route, undefined)).toBe(false);
expect(
subscribeSpyTo(
NgxI18nGuard(route, undefined) as Observable<boolean>
).getFirstValue()
).toBe(false);
expect(router.navigate).toHaveBeenCalledWith(['/', 'nl']);

route = {
Expand All @@ -97,7 +113,11 @@ describe('NgxI18nGuard', () => {
paramMap: convertToParamMap({}),
} as ActivatedRouteSnapshot;

expect(NgxI18nGuard(route, undefined)).toBe(false);
expect(
subscribeSpyTo(
NgxI18nGuard(route, undefined) as Observable<boolean>
).getFirstValue()
).toBe(false);
expect(router.navigate).toHaveBeenCalledWith(['/', 'nl']);
});
});
Expand Down
63 changes: 37 additions & 26 deletions libs/i18n/src/lib/guards/i18n/i18n.guard.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,55 @@
import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivateFn, Router } from '@angular/router';

import { Observable, take, map } from 'rxjs';
import { NgxI18nRootService } from '../../services';
import { NgxI18nConfiguration } from '../../i18n.types';
import { NgxI18nConfigurationToken } from '../../tokens';

/**
* Set the language in the url of the route
*
* @param route - The provided route
*/
export const NgxI18nGuard: CanActivateFn = (route: ActivatedRouteSnapshot): boolean => {
export const NgxI18nGuard: CanActivateFn = (route: ActivatedRouteSnapshot): Observable<boolean> => {
// Iben: Fetch all injectables
const router: Router = inject(Router);
const i18nService = inject(NgxI18nRootService);
const config: NgxI18nConfiguration = inject(NgxI18nConfigurationToken);

// Iben: Initialize the current language of the application
i18nService.initializeLanguage();

// Iben: Get the two language params
const currentLanguage = i18nService.currentLanguage;
const routeLanguage = getLanguage(route, config);
return i18nService.availableLanguages$.pipe(
take(1),
map((availableLanguages) => {
// Iben: Get the two language params
const currentLanguage = i18nService.currentLanguage;
const routeLanguage = getLanguage(
route,
availableLanguages,
i18nService.languageRouteParam
);

// Iben: If both languages are the same, we can continue
if (currentLanguage === routeLanguage) {
return true;
}
// Iben: If both languages are the same, we can continue
if (currentLanguage === routeLanguage) {
return true;
}

// Iben: If the router language differs, we check if it is available
if (config?.availableLanguages.includes(routeLanguage)) {
// Iben: Update the language
i18nService.setCurrentLanguage(routeLanguage);
// Iben: If the router language differs, we check if it is available
if (availableLanguages.includes(routeLanguage)) {
// Iben: Update the language
i18nService.setCurrentLanguage(routeLanguage);

//Iben: Re-route to the new language
router.navigate(['/', routeLanguage]);
//Iben: Re-route to the new language
router.navigate(['/', routeLanguage]);

return true;
}
return true;
}

//Iben: The current language is set to "default" when no previous language exists.
router.navigate(['/', currentLanguage]);
//Iben: The current language is set to "default" when no previous language exists.
router.navigate(['/', currentLanguage]);

return false;
return false;
})
);
};

/**
Expand All @@ -51,17 +58,21 @@ export const NgxI18nGuard: CanActivateFn = (route: ActivatedRouteSnapshot): bool
* @param route - The provided route
* @param config - The provided config
*/
const getLanguage = (route: ActivatedRouteSnapshot, config: NgxI18nConfiguration): string => {
const language = route?.paramMap.get(config.languageRouteParam || 'language');
const getLanguage = (
route: ActivatedRouteSnapshot,
availableLanguages: string[],
languageRouteParam: string
): string => {
const language = route?.paramMap.get(languageRouteParam || 'language');
const parent = route?.parent;

if (!parent && !language) {
return undefined;
}

if (config.availableLanguages.includes(language)) {
if (availableLanguages.includes(language)) {
return language;
}

return getLanguage(parent, config);
return getLanguage(parent, availableLanguages, languageRouteParam);
};
2 changes: 1 addition & 1 deletion libs/i18n/src/lib/i18n.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export interface NgxI18nConfiguration {
defaultLanguage: string;
availableLanguages: string[];
availableLanguages?: string[];
defaultAssetPaths: string[];
languageRouteParam?: string;
}
57 changes: 47 additions & 10 deletions libs/i18n/src/lib/services/root-i18n/root-i18n.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isPlatformBrowser } from '@angular/common';
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { BehaviorSubject, filter, Observable } from 'rxjs';

import { NgxI18nConfiguration } from '../../i18n.types';
import { NgxI18nConfigurationToken } from '../../tokens';
Expand All @@ -15,11 +15,40 @@ export class NgxI18nRootService {
undefined
);

/**
* A subject to hold the available languages
*/
private readonly availableLanguagesSubject: BehaviorSubject<string[]> = new BehaviorSubject([]);

/**
* The available languages
*
* Only emits once the list contains at least one language
*/
public readonly availableLanguages$: Observable<string[]> = this.availableLanguagesSubject
.asObservable()
.pipe(filter((languages) => languages?.length > 0));

/**
* The default language of the application
*/
public defaultLanguage: string;

/**
* The route param we use to set the language, by default this is `language`
*/
public languageRouteParam: string;

constructor(
@Inject(PLATFORM_ID) private readonly platformId: string,
@Inject(NgxI18nConfigurationToken)
private readonly configuration: NgxI18nConfiguration
) {}
) {
// Iben: Set the initial values so that we can refer to the services as the source of truth
this.defaultLanguage = configuration.defaultLanguage;
this.languageRouteParam = configuration.languageRouteParam || 'language';
this.availableLanguagesSubject.next(this.configuration.availableLanguages || []);
}

/**
* The current language of the application, as an Observable
Expand Down Expand Up @@ -64,17 +93,25 @@ export class NgxI18nRootService {
}

// Iben: If the current language does not exist, we check if it exists in the local storage, if not, we use the default config
let language = this.configuration.defaultLanguage;
let language = this.defaultLanguage;

if (isPlatformBrowser(this.platformId)) {
language =
localStorage.getItem('ngx-i18n-language') || this.configuration.defaultLanguage;
language = localStorage.getItem('ngx-i18n-language') || this.defaultLanguage;
}

// Iben: We set the new language
this.setCurrentLanguage(language);
}

/**
* Set the list of available languages
*
* @param languages - The list of available languages
*/
public setAvailableLanguages(languages: string[]): void {
this.availableLanguagesSubject.next(languages);
}

/**
* Checks if the newly proposed language can be set, if not we return either the current language or the default language
*
Expand All @@ -85,12 +122,12 @@ export class NgxI18nRootService {
let newLanguage = language;

// Iben: Check if the new language is part of the available languages
if (!this.configuration.availableLanguages.includes(language)) {
if (!this.availableLanguagesSubject.getValue().includes(language)) {
// Iben: If a language is set that's not part of the available languages, we return a warn
console.warn(
`NgxI18n: A language, ${language}, was attempted to be set that was not part of the available languages (${this.configuration.availableLanguages.join(
', '
)})`
`NgxI18n: A language, ${language}, was attempted to be set that was not part of the available languages (${this.availableLanguagesSubject
.getValue()
.join(', ')})`
);

// Iben: If there is already a language set, we early exit and keep the remaining language
Expand All @@ -99,7 +136,7 @@ export class NgxI18nRootService {
}

// Iben: If no language exists, we use the default language
newLanguage = this.configuration.defaultLanguage;
newLanguage = this.defaultLanguage;
}

return newLanguage;
Expand Down