diff --git a/package.json b/package.json index 5ce12ff1..368241c5 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,8 @@ "@angular/platform-browser-dynamic": "5.2.2", "@angular/router": "5.2.2", "@angular/service-worker": "5.2.2", + "@ngx-translate/core": "9.1.1", + "@ngx-translate/http-loader": "2.0.1", "@types/hammerjs": "2.0.35", "core-js": "2.4.1", "hammerjs": "2.0.8", diff --git a/src/app/app.component.html b/src/app/app.component.html index f736e212..5764c887 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -13,7 +13,7 @@ [routerLinkActiveOptions]="menu.options" [routerLink]="menu.link" (click)="shouldOpenChildMenu(menu.title)"> - {{menu.title}} + {{menu.title | translate}} diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 08fa727e..e1069bba 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -1,36 +1,42 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { AppComponent } from './app.component'; import { MatSidenavModule, MatListModule } from '@angular/material'; +import { TranslateModule } from '@ngx-translate/core'; + +import { AppComponent } from './app.component'; import { CoreModule } from './core/core.module'; +import { LanguageService } from './core/services/language.service'; describe('AppComponent', () => { let component: AppComponent; let fixture: ComponentFixture; - - beforeEach( - async(() => { - TestBed.configureTestingModule({ - imports: [ - RouterTestingModule, - BrowserAnimationsModule, - CoreModule.forRoot(), - MatSidenavModule, - MatListModule - ], - declarations: [AppComponent] - }).compileComponents(); - }) - ); + let languageService: LanguageService; beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + BrowserAnimationsModule, + CoreModule.forRoot(), + MatSidenavModule, + MatListModule, + TranslateModule.forRoot() + ], + declarations: [AppComponent], + providers: [LanguageService] + }); + fixture = TestBed.createComponent(AppComponent); component = fixture.componentInstance; - fixture.detectChanges(); + languageService = TestBed.get(LanguageService); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should init supported languages on initialization', () => { + spyOn(languageService, 'init').and.stub(); + + fixture.detectChanges(); + + expect(languageService.init).toHaveBeenCalledWith(['en', 'ru']); }); }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index c25ca087..c081e1f1 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -8,6 +8,7 @@ import { import { filter, map, mergeMap } from 'rxjs/operators'; import { SeoService, SeoData } from './core/services/seo.service'; import { OperatorMenuService } from './core/services/operator-menu.service'; +import { LanguageService } from './core/services/language.service'; interface Menu { title: string; @@ -23,22 +24,22 @@ interface Menu { export class AppComponent implements OnInit { menus: Menu[] = [ { - title: 'Home', + title: 'MENU.HOME', link: '/', options: { exact: true } }, { - title: 'Operators', + title: 'MENU.OPERATORS', link: '/operators', options: { exact: false } }, { - title: 'Companies', + title: 'MENU.COMPANIES', link: '/companies', options: { exact: false } }, { - title: 'Team', + title: 'MENU.TEAM', link: '/team', options: { exact: false } } @@ -48,7 +49,8 @@ export class AppComponent implements OnInit { private _router: Router, private _activatedRoute: ActivatedRoute, private _seo: SeoService, - private _operatorMenuService: OperatorMenuService + private _operatorMenuService: OperatorMenuService, + private languageService: LanguageService ) {} ngOnInit() { @@ -67,11 +69,13 @@ export class AppComponent implements OnInit { filter((data: SeoData) => data.title !== undefined) ) .subscribe((data: SeoData) => this._seo.setHeaders(data)); + + this.languageService.init(['en', 'ru']); } - shouldOpenChildMenu(title: string) { + shouldOpenChildMenu(title: string): void { // for accessibility we need to ensure child menu is open when clicked - if (title === 'Operators') { + if (title === 'MENU.OPERATORS') { this._operatorMenuService.openOperatorMenu(); } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 65cc8442..dabffcbd 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,16 +1,23 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; +import { HttpClient, HttpClientModule } from '@angular/common/http'; import { ServiceWorkerModule } from '@angular/service-worker'; -import { MatSidenavModule, MatListModule } from '@angular/material'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { FlexLayoutModule } from '@angular/flex-layout'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; + import { environment } from '../environments/environment'; import { CoreModule } from '../app/core/core.module'; import { MaterialModule } from './material/material.module'; import { AppRoutingModule } from './app-routing.module'; - import { AppComponent } from './app.component'; +import { LanguageService } from './core/services/language.service'; + +export function HttpLoaderFactory(http: HttpClient) { + return new TranslateHttpLoader(http); +} @NgModule({ declarations: [AppComponent], @@ -23,8 +30,17 @@ import { AppComponent } from './app.component'; FlexLayoutModule, MaterialModule, AppRoutingModule, - CoreModule.forRoot() + HttpClientModule, + CoreModule.forRoot(), + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [HttpClient] + } + }) ], + providers: [LanguageService], bootstrap: [AppComponent] }) export class AppModule {} diff --git a/src/app/core/components/toolbar/toolbar.component.html b/src/app/core/components/toolbar/toolbar.component.html index f3c29420..5a5707b2 100644 --- a/src/app/core/components/toolbar/toolbar.component.html +++ b/src/app/core/components/toolbar/toolbar.component.html @@ -6,7 +6,15 @@ RxJS Docs WARNING: This is BETA site - + + + + + ReactiveX GitHub Repository GitHub diff --git a/src/app/core/components/toolbar/toolbar.component.spec.ts b/src/app/core/components/toolbar/toolbar.component.spec.ts index 4b7d31c3..434d2cfd 100644 --- a/src/app/core/components/toolbar/toolbar.component.spec.ts +++ b/src/app/core/components/toolbar/toolbar.component.spec.ts @@ -1,30 +1,52 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; import { ToolbarComponent } from './toolbar.component'; import { MaterialModule } from '../../../material/material.module'; +import { LanguageService } from '../../services/language.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { languagesList } from '../../data/language.data'; describe('ToolbarComponent', () => { + const languages = languagesList; let component: ToolbarComponent; let fixture: ComponentFixture; - - beforeEach( - async(() => { - TestBed.configureTestingModule({ - imports: [MaterialModule, BrowserAnimationsModule, RouterTestingModule], - declarations: [ToolbarComponent] - }).compileComponents(); - }) - ); + let languageService: LanguageService; beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + MaterialModule, + BrowserAnimationsModule, + RouterTestingModule, + TranslateModule.forRoot() + ], + declarations: [ToolbarComponent], + providers: [LanguageService] + }); + + languageService = TestBed.get(LanguageService); fixture = TestBed.createComponent(ToolbarComponent); component = fixture.componentInstance; + }); + + it('should set languagesList and currentLang on initialization', () => { + spyOn(languageService, 'getLanguagesList').and.returnValue(languages); + spyOn(languageService, 'getCurrentLang').and.returnValue(languages[1]); + fixture.detectChanges(); + + expect(component.languagesList).toEqual(languages); + expect(component.currentLang).toEqual(languages[1]); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should change current language on onLangSwitch', () => { + spyOn(languageService, 'saveLang').and.stub(); + + component.onLangSwitch(languages[1]); + + expect(component.currentLang).toEqual(languages[1]); + expect(languageService.saveLang).toHaveBeenCalledWith(languages[1]); }); }); diff --git a/src/app/core/components/toolbar/toolbar.component.ts b/src/app/core/components/toolbar/toolbar.component.ts index da367091..c7282605 100644 --- a/src/app/core/components/toolbar/toolbar.component.ts +++ b/src/app/core/components/toolbar/toolbar.component.ts @@ -1,4 +1,6 @@ import { Component, OnInit, Output, EventEmitter } from '@angular/core'; +import { Lang } from '../../models/language.model'; +import { LanguageService } from '../../services/language.service'; @Component({ selector: 'app-toolbar', @@ -7,11 +9,22 @@ import { Component, OnInit, Output, EventEmitter } from '@angular/core'; }) export class ToolbarComponent implements OnInit { @Output() navToggle = new EventEmitter(); + currentLang: Lang; + languagesList: Lang[]; + + constructor(private languageService: LanguageService) {} + + ngOnInit() { + this.languagesList = this.languageService.getLanguagesList(); + this.currentLang = this.languageService.getCurrentLang(); + } + navOpen() { this.navToggle.emit(true); } - constructor() {} - - ngOnInit() {} + onLangSwitch(lang: Lang): void { + this.currentLang = lang; + this.languageService.saveLang(lang); + } } diff --git a/src/app/core/data/language.data.ts b/src/app/core/data/language.data.ts new file mode 100644 index 00000000..6a761522 --- /dev/null +++ b/src/app/core/data/language.data.ts @@ -0,0 +1,14 @@ +import { Lang } from '../models/language.model'; + +export const languagesList: Lang[] = [ + { + code: 'en', + name: 'English', + nativeName: 'English' + }, + { + code: 'ru', + name: 'Russian', + nativeName: 'Русский' + } +]; diff --git a/src/app/core/models/language.model.ts b/src/app/core/models/language.model.ts new file mode 100644 index 00000000..98c96b62 --- /dev/null +++ b/src/app/core/models/language.model.ts @@ -0,0 +1,5 @@ +export interface Lang { + code: string; + name: string; + nativeName: string; +} diff --git a/src/app/core/services/language.service.spec.ts b/src/app/core/services/language.service.spec.ts new file mode 100644 index 00000000..3e254b6f --- /dev/null +++ b/src/app/core/services/language.service.spec.ts @@ -0,0 +1,65 @@ +import { TestBed } from '@angular/core/testing'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; + +import { languagesList } from '../data/language.data'; +import { LanguageService } from './language.service'; + +describe('LanguageService', () => { + const languages = languagesList; + let languageService: LanguageService; + let translateService: TranslateService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + providers: [LanguageService] + }); + + languageService = TestBed.get(LanguageService); + translateService = TestBed.get(TranslateService); + + window.localStorage.removeItem('current_lang'); + }); + + it('should set fullLangList on initialization', () => { + expect(languageService.fullLangList).toEqual(languages); + }); + + it('should set the using language and default language on init', () => { + spyOn(translateService, 'getBrowserLang').and.returnValue('en'); + spyOn(translateService, 'use').and.stub(); + + languageService.init(['en', 'ru']); + + expect(translateService.use).toHaveBeenCalledWith('en'); + }); + + it('should get language list on getLanguagesList', () => { + spyOn(translateService, 'getLangs').and.returnValue(['en', 'ru']); + + languageService.getLanguagesList(); + + expect(languageService.fullLangList).toEqual(languages); + }); + + it('should get current language on getCurrentLang', () => { + spyOn(languageService, 'getLanguagesList').and.returnValue(languages); + translateService.currentLang = 'ru'; + + const result = languageService.getCurrentLang(); + + expect(result).toEqual({ + code: 'ru', + name: 'Russian', + nativeName: 'Русский' + }); + }); + + it('should save language on saveLang', () => { + spyOn(translateService, 'use').and.stub(); + + languageService.saveLang(languages[1]); + + expect(translateService.use).toHaveBeenCalledWith(languages[1].code); + }); +}); diff --git a/src/app/core/services/language.service.ts b/src/app/core/services/language.service.ts new file mode 100644 index 00000000..e30a2061 --- /dev/null +++ b/src/app/core/services/language.service.ts @@ -0,0 +1,51 @@ +import { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { Lang } from '../models/language.model'; +import { languagesList } from '../data/language.data'; + +@Injectable() +export class LanguageService { + fullLangList: Lang[] = []; + + constructor(private translate: TranslateService) { + this.fullLangList = languagesList; + } + + init(languages: string[]): void { + const supportedLangs = languages; + const browserLang = this.translate.getBrowserLang(); + const savedLang = window.localStorage.getItem('current_lang'); + + this.translate.addLangs(supportedLangs); + this.translate.setDefaultLang('en'); + + if (!savedLang) { + this.translate.use( + browserLang.match(/en|fr|jp|ch/) + ? browserLang + : this.translate.getDefaultLang() + ); + } else { + this.translate.use(savedLang); + } + } + + getLanguagesList(): Lang[] { + const supportedLangs = this.translate.getLangs(); + + return this.fullLangList.filter((lang: Lang) => + supportedLangs.includes(lang.code) + ); + } + + getCurrentLang(): Lang { + return this.getLanguagesList().find( + lang => lang.code === this.translate.currentLang + ); + } + + saveLang(lang: Lang): void { + this.translate.use(lang.code); + window.localStorage.setItem('current_lang', lang.code); + } +} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json new file mode 100644 index 00000000..7e95b217 --- /dev/null +++ b/src/assets/i18n/en.json @@ -0,0 +1,8 @@ +{ + "MENU": { + "HOME": "Home", + "OPERATORS": "Operators", + "COMPANIES": "Companies", + "TEAM": "Team" + } +} diff --git a/src/assets/i18n/ru.json b/src/assets/i18n/ru.json new file mode 100644 index 00000000..b31168e3 --- /dev/null +++ b/src/assets/i18n/ru.json @@ -0,0 +1,8 @@ +{ + "MENU": { + "HOME": "Главная", + "OPERATORS": "Операторы", + "COMPANIES": "Компании", + "TEAM": "Команда" + } +}