Skip to content

Commit

Permalink
feat(TranslateStore): added a store to share translations between ins…
Browse files Browse the repository at this point in the history
…tances of the service
  • Loading branch information
ocombe committed Feb 3, 2017
1 parent 7ce9fa7 commit b626c0e
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 22 deletions.
10 changes: 9 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {MissingTranslationHandler, FakeMissingTranslationHandler} from "./src/mi
import {TranslateParser, TranslateDefaultParser} from "./src/translate.parser";
import {TranslateDirective} from "./src/translate.directive";
import {TranslatePipe} from "./src/translate.pipe";
import {TranslateStore} from "./src/translate.store";
import {USE_STORE} from "./src/translate.service";
import {isDefined} from "./src/util";

export * from "./src/translate.loader";
export * from "./src/translate.service";
Expand All @@ -17,6 +20,7 @@ export interface TranslateModuleConfig {
loader?: Provider;
parser?: Provider;
missingTranslationHandler?: Provider;
useStore?: boolean;
}

@NgModule({
Expand All @@ -42,6 +46,8 @@ export class TranslateModule {
config.loader || {provide: TranslateLoader, useClass: TranslateFakeLoader},
config.parser || {provide: TranslateParser, useClass: TranslateDefaultParser},
config.missingTranslationHandler || {provide: MissingTranslationHandler, useClass: FakeMissingTranslationHandler},
TranslateStore,
{provide: USE_STORE, useValue: isDefined(config.useStore) ? config.useStore : true},
TranslateService
]
};
Expand All @@ -58,7 +64,9 @@ export class TranslateModule {
providers: [
config.loader || {provide: TranslateLoader, useClass: TranslateFakeLoader},
config.parser || {provide: TranslateParser, useClass: TranslateDefaultParser},
config.missingTranslationHandler || {provide: MissingTranslationHandler, useClass: FakeMissingTranslationHandler}
config.missingTranslationHandler || {provide: MissingTranslationHandler, useClass: FakeMissingTranslationHandler},
{provide: USE_STORE, useValue: isDefined(config.useStore) ? config.useStore : true},
TranslateService
]
};
}
Expand Down
108 changes: 90 additions & 18 deletions src/translate.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Injectable, EventEmitter} from "@angular/core";
import {Injectable, EventEmitter, Inject, OpaqueToken} from "@angular/core";
import {Observable} from "rxjs/Observable";
import {Observer} from "rxjs/Observer";
import "rxjs/add/observable/of";
Expand All @@ -8,11 +8,14 @@ import "rxjs/add/operator/merge";
import "rxjs/add/operator/toArray";
import "rxjs/add/operator/take";

import {TranslateStore} from "./translate.store";
import {TranslateLoader} from "./translate.loader";
import {MissingTranslationHandler, MissingTranslationHandlerParams} from "./missing-translation-handler";
import {TranslateParser} from "./translate.parser";
import {isDefined} from "./util";

export const USE_STORE = new OpaqueToken('USE_STORE');

export interface TranslationChangeEvent {
translations: any;
lang: string;
Expand All @@ -35,10 +38,15 @@ declare const window: Window;

@Injectable()
export class TranslateService {
/**
* The lang currently used
*/
public currentLang: string = this.defaultLang;
private loadingTranslations: Observable<any>;
private pending: boolean = false;
private _onTranslationChange: EventEmitter<TranslationChangeEvent> = new EventEmitter<TranslationChangeEvent>();
private _onLangChange: EventEmitter<LangChangeEvent> = new EventEmitter<LangChangeEvent>();
private _onDefaultLangChange: EventEmitter<DefaultLangChangeEvent> = new EventEmitter<DefaultLangChangeEvent>();
private _defaultLang: string;
private _currentLang: string;
private _langs: Array<string> = [];
private _translations: any = {};

/**
* An EventEmitter to listen to translation change events
Expand All @@ -47,7 +55,9 @@ export class TranslateService {
* });
* @type {EventEmitter<TranslationChangeEvent>}
*/
public onTranslationChange: EventEmitter<TranslationChangeEvent> = new EventEmitter<TranslationChangeEvent>();
get onTranslationChange(): EventEmitter<TranslationChangeEvent> {
return this.useStore ? this.store.onTranslationChange : this._onTranslationChange;
}

/**
* An EventEmitter to listen to lang change events
Expand All @@ -56,7 +66,9 @@ export class TranslateService {
* });
* @type {EventEmitter<LangChangeEvent>}
*/
public onLangChange: EventEmitter<LangChangeEvent> = new EventEmitter<LangChangeEvent>();
get onLangChange(): EventEmitter<LangChangeEvent> {
return this.useStore ? this.store.onLangChange : this._onLangChange;
}

/**
* An EventEmitter to listen to default lang change events
Expand All @@ -65,23 +77,83 @@ export class TranslateService {
* });
* @type {EventEmitter<DefaultLangChangeEvent>}
*/
public onDefaultLangChange: EventEmitter<DefaultLangChangeEvent> = new EventEmitter<DefaultLangChangeEvent>();
get onDefaultLangChange() {
return this.useStore ? this.store.onDefaultLangChange : this._onDefaultLangChange;
}

private loadingTranslations: Observable<any>;
private pending: boolean = false;
private translations: any = {};
private defaultLang: string;
private langs: Array<string> = [];
/**
* The default lang to fallback when translations are missing on the current lang
*/
get defaultLang(): string {
return this.useStore ? this.store.defaultLang : this._defaultLang;
}
set defaultLang(defaultLang: string) {
if(this.useStore) {
this.store.defaultLang = defaultLang;
} else {
this._defaultLang = defaultLang;
}
}

/**
* The lang currently used
* @type {string}
*/
get currentLang(): string {
return this.useStore ? this.store.currentLang : this._currentLang;
}
set currentLang(currentLang: string) {
if(this.useStore) {
this.store.currentLang = currentLang;
} else {
this._currentLang = currentLang;
}
}

/**
* an array of langs
* @type {Array}
*/
get langs(): string[] {
return this.useStore ? this.store.langs : this._langs;
}
set langs(langs: string[]) {
if(this.useStore) {
this.store.langs = langs;
} else {
this._langs = langs;
}
}

/**
* a list of translations per lang
* @type {{}}
*/
get translations(): any {
return this.useStore ? this.store.translations : this._translations;
}

set translations(translations: any) {
if(this.useStore) {
this.store.translations = translations;
} else {
this._currentLang = translations;
}
}

/**
*
* @param store an instance of the store (that is supposed to be unique)
* @param currentLoader An instance of the loader currently used
* @param parser An instance of the parser currently used
* @param missingTranslationHandler A handler for missing translations.
* @param useStore whether this service should use the store or not
*/
constructor(public currentLoader: TranslateLoader,
constructor(public store: TranslateStore,
public currentLoader: TranslateLoader,
public parser: TranslateParser,
public missingTranslationHandler: MissingTranslationHandler) {
public missingTranslationHandler: MissingTranslationHandler,
@Inject(USE_STORE) private useStore: boolean = true) {
}

/**
Expand Down Expand Up @@ -233,7 +305,7 @@ export class TranslateService {
* @returns {any}
*/
public getParsedResult(translations: any, key: any, interpolateParams?: Object): any {
let res: string|Observable<string>;
let res: string | Observable<string>;

if(key instanceof Array) {
let result: any = {},
Expand Down Expand Up @@ -290,7 +362,7 @@ export class TranslateService {
* @param interpolateParams
* @returns {any} the translated key, or an object of translated keys
*/
public get(key: string|Array<string>, interpolateParams?: Object): Observable<string|any> {
public get(key: string | Array<string>, interpolateParams?: Object): Observable<string | any> {
if(!isDefined(key) || !key.length) {
throw new Error(`Parameter "key" required`);
}
Expand Down Expand Up @@ -330,7 +402,7 @@ export class TranslateService {
* @param interpolateParams
* @returns {string}
*/
public instant(key: string|Array<string>, interpolateParams?: Object): string|any {
public instant(key: string | Array<string>, interpolateParams?: Object): string | any {
if(!isDefined(key) || !key.length) {
throw new Error(`Parameter "key" required`);
}
Expand Down
54 changes: 54 additions & 0 deletions src/translate.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {EventEmitter} from "@angular/core";
import {DefaultLangChangeEvent, LangChangeEvent, TranslationChangeEvent} from "./translate.service";

export class TranslateStore {
/**
* The default lang to fallback when translations are missing on the current lang
*/
public defaultLang: string;

/**
* The lang currently used
* @type {string}
*/
public currentLang: string = this.defaultLang;

/**
* a list of translations per lang
* @type {{}}
*/
public translations: any = {};

/**
* an array of langs
* @type {Array}
*/
public langs: Array<string> = [];

/**
* An EventEmitter to listen to translation change events
* onTranslationChange.subscribe((params: TranslationChangeEvent) => {
* // do something
* });
* @type {EventEmitter<TranslationChangeEvent>}
*/
public onTranslationChange: EventEmitter<TranslationChangeEvent> = new EventEmitter<TranslationChangeEvent>();

/**
* An EventEmitter to listen to lang change events
* onLangChange.subscribe((params: LangChangeEvent) => {
* // do something
* });
* @type {EventEmitter<LangChangeEvent>}
*/
public onLangChange: EventEmitter<LangChangeEvent> = new EventEmitter<LangChangeEvent>();

/**
* An EventEmitter to listen to default lang change events
* onDefaultLangChange.subscribe((params: DefaultLangChangeEvent) => {
* // do something
* });
* @type {EventEmitter<DefaultLangChangeEvent>}
*/
public onDefaultLangChange: EventEmitter<DefaultLangChangeEvent> = new EventEmitter<DefaultLangChangeEvent>();
}
80 changes: 77 additions & 3 deletions tests/translate.module.spec.ts → tests/translate.store.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Component, NgModuleFactoryLoader, NgModule, ModuleWithProviders} from "@angular/core";
import {Location} from '@angular/common';
import {Router, RouterModule} from "@angular/router";
import {Route, Router, RouterModule} from "@angular/router";
import {SpyNgModuleFactoryLoader, RouterTestingModule} from "@angular/router/testing";
import {ComponentFixture, TestBed, tick, inject, fakeAsync, getTestBed} from "@angular/core/testing";
import {TranslateModule, TranslateService} from "../index";
Expand Down Expand Up @@ -37,7 +37,7 @@ function getLazyLoadedModule(importedModule: ModuleWithProviders) {
@NgModule({
declarations: [ParentLazyLoadedComponent, ChildLazyLoadedComponent],
imports: [
RouterModule.forChild([{
RouterModule.forChild([<Route>{
path: 'loaded',
component: ParentLazyLoadedComponent,
children: [{path: 'child', component: ChildLazyLoadedComponent}]
Expand Down Expand Up @@ -79,7 +79,6 @@ describe("module", () => {
[Router, Location, NgModuleFactoryLoader],
(router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
let LoadedModule = getLazyLoadedModule(TranslateModule.forChild());

loader.stubbedModules = {expected: LoadedModule};

const fixture = createRoot(router, RootCmp),
Expand All @@ -106,7 +105,32 @@ describe("module", () => {
[Router, Location, NgModuleFactoryLoader],
(router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
let LoadedModule = getLazyLoadedModule(TranslateModule.forRoot());
loader.stubbedModules = {expected: LoadedModule};

const fixture = createRoot(router, RootCmp),
injector = getTestBed(),
translate = injector.get(TranslateService);

expect(translate.instant('TEST')).toEqual('Root');

router.resetConfig([{path: 'lazy', loadChildren: 'expected'}]);

router.navigateByUrl('/lazy/loaded/child');
advance(fixture);

expect(location.path()).toEqual('/lazy/loaded/child');

// since both the root module and the lazy loaded module use forRoot to define the TranslateModule
// the translate service is NOT shared, and 2 instances co-exist
// the constructor of the ChildLazyLoadedComponent didn't overwrote the "TEST" key of the root TranslateService
expect(translate.instant('TEST')).toEqual('Root');
}))
);

it("should create 2 instances of the service when lazy loaded using forChild and useStore false", fakeAsync(inject(
[Router, Location, NgModuleFactoryLoader],
(router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
let LoadedModule = getLazyLoadedModule(TranslateModule.forChild({useStore: false}));
loader.stubbedModules = {expected: LoadedModule};

const fixture = createRoot(router, RootCmp),
Expand All @@ -128,4 +152,54 @@ describe("module", () => {
expect(translate.instant('TEST')).toEqual('Root');
}))
);

it("should relay events when lazy loading & using forChild with useStore true", fakeAsync(inject(
[Router, Location, NgModuleFactoryLoader],
(router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
let LoadedModule = getLazyLoadedModule(TranslateModule.forChild());
loader.stubbedModules = {expected: LoadedModule};

const fixture = createRoot(router, RootCmp),
injector = getTestBed(),
translate = injector.get(TranslateService);

let spy = jasmine.createSpy('TranslationChange');
let sub = translate.onTranslationChange.subscribe(spy);

expect(spy).toHaveBeenCalledTimes(0);

router.resetConfig([{path: 'lazy', loadChildren: 'expected'}]);

router.navigateByUrl('/lazy/loaded/child');
advance(fixture);

expect(spy).toHaveBeenCalledTimes(1);
sub.unsubscribe();
}))
);

it("should not relay events when lazy loading & using forChild with useStore false", fakeAsync(inject(
[Router, Location, NgModuleFactoryLoader],
(router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
let LoadedModule = getLazyLoadedModule(TranslateModule.forChild({useStore: false}));
loader.stubbedModules = {expected: LoadedModule};

const fixture = createRoot(router, RootCmp),
injector = getTestBed(),
translate = injector.get(TranslateService);

let spy = jasmine.createSpy('TranslationChange');
let sub = translate.onTranslationChange.subscribe(spy);

expect(spy).toHaveBeenCalledTimes(0);

router.resetConfig([{path: 'lazy', loadChildren: 'expected'}]);

router.navigateByUrl('/lazy/loaded/child');
advance(fixture);

expect(spy).toHaveBeenCalledTimes(0);
sub.unsubscribe();
}))
);
});

0 comments on commit b626c0e

Please sign in to comment.