Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Step 3: multi-lingual support #3

Merged
merged 4 commits into from Jul 9, 2019
Merged
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -6,13 +6,15 @@ import { ngExpressEngine } from '@nguniversal/express-engine';
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';

import * as express from 'express';
import * as cookieParser from 'cookie-parser';
import { join } from 'path';

// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();

// Express server
const app = express();
app.use(cookieParser());

const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist/browser');
@@ -21,12 +23,21 @@ const DIST_FOLDER = join(process.cwd(), 'dist/browser');
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('../../dist/server/main');

// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
app.engine(
'html',
app.engine('html', (_, options, callback) =>
ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [provideModuleMap(LAZY_MODULE_MAP)]
})
providers: [
provideModuleMap(LAZY_MODULE_MAP),
{
provide: 'REQUEST',
useValue: options.req
},
{
provide: 'RESPONSE',
useValue: options.req.res
}
]
})(_, options, callback)
);

app.set('view engine', 'html');
@@ -6,5 +6,4 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
styleUrls: ['./app.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
}
export class AppComponent {}
@@ -1,5 +1,5 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { HeaderComponent } from './components/header/header.component';
import { FooterComponent } from './components/footer/footer.component';
@@ -16,6 +16,17 @@ import { HomePageComponent } from './components/pages/home/home-page.component';
import { PrivacyPolicyPageComponent } from './components/pages/privacy-policy/privacy-policy-page.component';
import { TermsOfServicePageComponent } from './components/pages/terms-of-service/terms-of-service-page.component';
import { AppRoutingModule } from './app-routing.module';
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { TranslateModule, TranslateLoader, TranslateService, TranslateStore } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { AppInitializerService } from './services/app-initializer/app-initializer.service';
import { BrowserCookiesModule } from '@ngx-utils/cookies/browser';
import { PrebootModule } from 'preboot';

// AoT requires an exported function for factories
export function httpLoaderFactory(httpClient: HttpClient) {
return new TranslateHttpLoader(httpClient);
}

@NgModule({
declarations: [
@@ -28,16 +39,35 @@ import { AppRoutingModule } from './app-routing.module';
],
imports: [
BrowserModule.withServerTransition({ appId: 'frontend' }),
PrebootModule.withConfig({ appRoot: 'app-root' }),
BrowserCookiesModule.forRoot(),
MatToolbarModule,
MatIconModule,
MatFormFieldModule,
MatSelectModule,
MatButtonModule,
BrowserAnimationsModule,
FlexLayoutModule,
AppRoutingModule
AppRoutingModule,
HttpClientModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: httpLoaderFactory,
deps: [HttpClient]
}
})
],
providers: [
TranslateStore,
TranslateService,
{
provide: APP_INITIALIZER,
useFactory: (appInitializerService: AppInitializerService) => () => appInitializerService.init(),
deps: [AppInitializerService],
multi: true
}
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
@@ -1,12 +1,36 @@
import { NgModule } from '@angular/core';
import { Injector, NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';
import { ServerCookiesModule } from '@ngx-utils/cookies/server';
import { TranslateServerLoader } from './translate.server.loader';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { FlexLayoutServerModule } from '@angular/flex-layout/server';
import { getLanguageFromString, Cookie } from '@laas/shared';

// AoT requires an exported function for factories
export function serverLoaderFactory(injector: Injector) {
const request = injector.get('REQUEST');
const language = getLanguageFromString(request.cookies[Cookie.LANGUAGE]);
return new TranslateServerLoader(language);
}

@NgModule({
imports: [AppModule, ServerModule, ModuleMapLoaderModule],
imports: [
AppModule,
ServerModule,
ServerCookiesModule.forRoot(),
ModuleMapLoaderModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: serverLoaderFactory,
deps: [Injector]
}
}),
FlexLayoutServerModule
],
bootstrap: [AppComponent]
})
export class AppServerModule {}
@@ -7,13 +7,13 @@
[routerLink]="'/' + footerButton.page"
[routerLinkActive]="'button-active'"
>
{{ footerButton.caption }}
{{ footerButton.caption | translate }}
</button>
<a target="_blank" mat-button href="https://blog.laas.sh/">Blog</a>
<a target="_blank" mat-button href="https://blog.laas.sh">{{ 'Blog' | translate }}</a>
</div>
<mat-form-field>
<mat-select [value]="'English'">
<mat-option *ngFor="let language of ['English', 'Russian']" [value]="language"> {{ language }} </mat-option>
<mat-select [value]="languageService.currentLanguage$ | async" (valueChange)="onLanguageChange($event)">
<mat-option *ngFor="let language of languages" [value]="language"> {{ languageNames[language] }} </mat-option>
</mat-select>
</mat-form-field>
</mat-toolbar-row>
@@ -1,5 +1,6 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Page } from '@laas/shared';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';
import { Language, languageNames, languages, Page } from '@laas/shared';
import { LanguageService } from '../../services/language/language.service';

interface FooterButton {
caption: string;
@@ -23,4 +24,12 @@ export class FooterComponent {
page: Page.TERMS_OF_SERVICE
}
];
languages = languages;
languageNames = languageNames;

constructor(public languageService: LanguageService) {}

onLanguageChange(language: Language) {
this.languageService.setCurrentLanguage(language);
}
}
@@ -21,6 +21,8 @@
</span>
</button>
</ng-container>
<button *ngIf="!(authService.user$ | async)" mat-raised-button (click)="authService.login()">Log In</button>
<button *ngIf="!(authService.user$ | async)" mat-raised-button (click)="authService.login()">
{{ 'Log In' | translate }}
</button>
</mat-toolbar-row>
</mat-toolbar>
@@ -0,0 +1,15 @@
import { TestBed } from '@angular/core/testing';
import { AppInitializerService } from './app-initializer.service';

describe('AppInitializerService', () => {
let service: AppInitializerService;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.get(AppInitializerService);
});

xit('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,13 @@
import { Injectable } from '@angular/core';
import { LanguageService } from '../language/language.service';

@Injectable({
providedIn: 'root'
})
export class AppInitializerService {
constructor(private languageService: LanguageService) {}

init() {
return this.languageService.init();
}
}
@@ -7,9 +7,7 @@ import { User } from '@laas/shared';
})
export class AuthService {
private _user$ = new BehaviorSubject<User>(undefined);
get user$() {
return this._user$.asObservable();
}
user$ = this._user$.asObservable();

login() {
this._user$.next({
@@ -0,0 +1,11 @@
import { TestBed } from '@angular/core/testing';
import { LanguageService } from './language.service';

describe('LanguageService', () => {
beforeEach(() => TestBed.configureTestingModule({}));

xit('should be created', () => {
const service: LanguageService = TestBed.get(LanguageService);
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { getLanguageFromString, Cookie, Language } from '@laas/shared';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { CookiesService } from '@ngx-utils/cookies';

@Injectable({
providedIn: 'root'
})
export class LanguageService {
private _currentLanguage$ = new BehaviorSubject<Language>(undefined);
currentLanguage$ = this._currentLanguage$.asObservable();

constructor(public translateService: TranslateService, private cookiesService: CookiesService) {}

init(): Promise<any> {
const defaultLanguage = getLanguageFromString(this.translateService.getBrowserLang());
this.translateService.setDefaultLang(defaultLanguage);
return this._setCurrentLanguage(
getLanguageFromString(this.cookiesService.get(Cookie.LANGUAGE), defaultLanguage)
).toPromise();
}

setCurrentLanguage(language: Language) {
this._setCurrentLanguage(language);
this.cookiesService.put(Cookie.LANGUAGE, language);
}

private _setCurrentLanguage(language: Language): Observable<any> {
this._currentLanguage$.next(language);
return this.translateService.use(language);
}
}
@@ -7,15 +7,11 @@ import { MediaObserver } from '@angular/flex-layout';
})
export class MiscService {
private _isMobile$ = new BehaviorSubject<boolean>(undefined);
get isMobile$() {
return this._isMobile$.asObservable();
}
isMobile$ = this._isMobile$.asObservable();

constructor(public mediaObserver: MediaObserver) {
this.mediaObserver.asObservable().subscribe(mediaChanges => {
const isMobile = !!mediaChanges.find(
mediaChange => mediaChange.mqAlias === 'lt-md'
);
const isMobile = !!mediaChanges.find(mediaChange => mediaChange.mqAlias === 'lt-md');
if (this._isMobile$.value !== isMobile) {
this._isMobile$.next(isMobile);
}
@@ -0,0 +1,19 @@
import { TranslateLoader } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { Language } from '@laas/shared';
const fs = require('fs');

export class TranslateServerLoader implements TranslateLoader {
language: Language;

constructor(language: Language) {
this.language = language;
}

getTranslation(): Observable<any> {
return new Observable(observer => {
observer.next(JSON.parse(fs.readFileSync(`./dist/browser/assets/i18n/${this.language}.json`, 'utf8')));
observer.complete();
});
}
}
@@ -0,0 +1 @@
{}
@@ -0,0 +1,6 @@
{
"Log In": "Вход для пользователей",
"Privacy Policy": "Политика конфиденциальности",
"Terms of Service": "Условия обслуживания",
"Blog": "Блог"
}
@@ -7,29 +7,11 @@

<meta name="viewport" content="width=device-width, initial-scale=1" />

<link
rel="apple-touch-icon"
sizes="180x180"
href="/assets/favicon/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/assets/favicon/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/assets/favicon/favicon-16x16.png"
/>
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon/favicon-16x16.png" />
<link rel="manifest" href="/assets/favicon/site.webmanifest" />
<link
rel="mask-icon"
href="/assets/favicon/safari-pinned-tab.svg"
color="#5bbad5"
/>
<link rel="mask-icon" href="/assets/favicon/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" />
</head>
@@ -7,7 +7,10 @@
"exclude": [
"src/test-setup.ts",
"**/*.spec.ts",
"server.ts"
"server.ts",
"src/app/translate.server.loader.ts",
"src/app/app.server.module.ts",
"src/main.server.ts"
],
"include": ["**/*.ts"]
}
@@ -6,5 +6,6 @@
},
"angularCompilerOptions": {
"entryModule": "src/app/app.server.module#AppServerModule"
}
},
"files": ["server.ts", "src/app/translate.server.loader.ts", "src/app/app.server.module.ts", "src/main.server.ts"]
}
@@ -1,4 +1,8 @@
export * from './lib/shared.module';

export * from './lib/enums/page.enum';
export * from './lib/enums/language.enum';
export * from './lib/enums/cookie.enum';

export * from './lib/interfaces/user.interface';
export * from './lib/interfaces/image.interface';
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.