File based translations - multi language

+ Example in the Mail app
+ Component/doc page for translations
This commit is contained in:
Sercan Yemen 2017-10-24 15:41:44 +03:00
parent 98e2ff0e1e
commit 9ecd921722
20 changed files with 292 additions and 19 deletions

5
package-lock.json generated
View File

@ -268,6 +268,11 @@
"integrity": "sha1-w6DFRNYjkqzCgTpCyKDcb1j4aSI=", "integrity": "sha1-w6DFRNYjkqzCgTpCyKDcb1j4aSI=",
"dev": true "dev": true
}, },
"@ngx-translate/core": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-8.0.0.tgz",
"integrity": "sha1-dR/WtRLYDzp0jS3o38lt/vopr+A="
},
"@schematics/angular": { "@schematics/angular": {
"version": "0.0.45", "version": "0.0.45",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-0.0.45.tgz", "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-0.0.45.tgz",

View File

@ -28,6 +28,7 @@
"@angular/platform-browser": "4.4.6", "@angular/platform-browser": "4.4.6",
"@angular/platform-browser-dynamic": "4.4.6", "@angular/platform-browser-dynamic": "4.4.6",
"@angular/router": "4.4.6", "@angular/router": "4.4.6",
"@ngx-translate/core": "8.0.0",
"@swimlane/ngx-charts": "6.0.2", "@swimlane/ngx-charts": "6.0.2",
"@swimlane/ngx-datatable": "9.3.1", "@swimlane/ngx-datatable": "9.3.1",
"@swimlane/ngx-dnd": "3.0.0", "@swimlane/ngx-dnd": "3.0.0",

View File

@ -1,5 +1,6 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { FuseSplashScreenService } from './core/services/splash-screen.service'; import { FuseSplashScreenService } from './core/services/splash-screen.service';
import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector : 'fuse-root', selector : 'fuse-root',
@ -8,7 +9,18 @@ import { FuseSplashScreenService } from './core/services/splash-screen.service';
}) })
export class AppComponent export class AppComponent
{ {
constructor(private fuseSplashScreen: FuseSplashScreenService) constructor(
private fuseSplashScreen: FuseSplashScreenService,
private translate: TranslateService
)
{ {
// Add languages
this.translate.addLangs(['en', 'tr']);
// Set the default language
this.translate.setDefaultLang('en');
// Use a language
this.translate.use('en');
} }
} }

View File

@ -21,6 +21,7 @@ import { ComponentsThirdPartyModule } from './main/content/components-third-part
import { ServicesModule } from './main/content/services/services.module'; import { ServicesModule } from './main/content/services/services.module';
import { FuseAngularMaterialModule } from './main/content/components/angular-material/angular-material.module'; import { FuseAngularMaterialModule } from './main/content/components/angular-material/angular-material.module';
import { MarkdownModule } from 'angular2-markdown'; import { MarkdownModule } from 'angular2-markdown';
import { TranslateModule } from '@ngx-translate/core';
const appRoutes: Routes = [ const appRoutes: Routes = [
{ {
@ -69,6 +70,7 @@ const appRoutes: Routes = [
RouterModule.forRoot(appRoutes), RouterModule.forRoot(appRoutes),
SharedModule, SharedModule,
MarkdownModule.forRoot(), MarkdownModule.forRoot(),
TranslateModule.forRoot(),
InMemoryWebApiModule.forRoot(FuseFakeDbService, { InMemoryWebApiModule.forRoot(FuseFakeDbService, {
delay : 0, delay : 0,

View File

@ -19,8 +19,10 @@ import { FuseHljsComponent } from '../components/hljs/hljs.component';
import { FusePerfectScrollbarDirective } from '../directives/fuse-perfect-scrollbar/fuse-perfect-scrollbar.directive'; import { FusePerfectScrollbarDirective } from '../directives/fuse-perfect-scrollbar/fuse-perfect-scrollbar.directive';
import { FuseIfOnDomDirective } from '../directives/fuse-if-on-dom/fuse-if-on-dom.directive'; import { FuseIfOnDomDirective } from '../directives/fuse-if-on-dom/fuse-if-on-dom.directive';
import { FuseMaterialColorPickerComponent } from '../components/material-color-picker/material-color-picker.component'; import { FuseMaterialColorPickerComponent } from '../components/material-color-picker/material-color-picker.component';
import { FuseTranslationLoaderService } from '../services/translation-loader.service';
import { CookieService } from 'ngx-cookie-service'; import { CookieService } from 'ngx-cookie-service';
import { MarkdownModule } from 'angular2-markdown'; import { MarkdownModule } from 'angular2-markdown';
import { TranslateModule } from '@ngx-translate/core';
@NgModule({ @NgModule({
declarations : [ declarations : [
@ -62,7 +64,8 @@ import { MarkdownModule } from 'angular2-markdown';
NgxDatatableModule, NgxDatatableModule,
FuseIfOnDomDirective, FuseIfOnDomDirective,
FuseMaterialColorPickerComponent, FuseMaterialColorPickerComponent,
MarkdownModule MarkdownModule,
TranslateModule
], ],
entryComponents: [ entryComponents: [
FuseConfirmDialogComponent FuseConfirmDialogComponent
@ -71,7 +74,8 @@ import { MarkdownModule } from 'angular2-markdown';
CookieService, CookieService,
FuseMatchMedia, FuseMatchMedia,
FuseNavbarVerticalService, FuseNavbarVerticalService,
FuseMatSidenavHelperService FuseMatSidenavHelperService,
FuseTranslationLoaderService
] ]
}) })

View File

@ -0,0 +1,27 @@
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
export interface Locale
{
lang: string;
data: Object;
}
@Injectable()
export class FuseTranslationLoaderService
{
constructor(private translate: TranslateService)
{
}
public loadTranslations(...args: Locale[]): void
{
const locales = [...args];
locales.forEach((locale) => {
// use setTranslation() with the third argument set to true
// to append translations instead of replacing them
this.translate.setTranslation(locale.lang, locale.data, true);
});
}
}

View File

@ -0,0 +1,14 @@
export const locale = {
lang: 'en',
data: {
'MAIL': {
'COMPOSE' : 'COMPOSE',
'FOLDERS' : 'FOLDERS',
'FILTERS' : 'FILTERS',
'LABELS' : 'LABELS',
'NO_MESSAGES' : 'There are no messages!',
'SELECT_A_MESSAGE_TO_READ': 'Select a message to read',
'SEARCH_PLACEHOLDER': 'Search for an e-mail or contact'
}
}
};

View File

@ -0,0 +1,14 @@
export const locale = {
lang: 'tr',
data: {
'MAIL': {
'COMPOSE' : 'YENİ E-POSTA',
'FOLDERS' : 'KLASÖRLER',
'FILTERS' : 'FİLTRELER',
'LABELS' : 'ETİKETLER',
'NO_MESSAGES' : 'Mesajiniz bulunmamakta!',
'SELECT_A_MESSAGE_TO_READ': 'Okumak için bir mesaj seçin',
'SEARCH_PLACEHOLDER' : 'E-mail yada bir kişi arayın'
}
}
};

View File

@ -1,6 +1,10 @@
<div *ngIf="!mail" fxLayout="column" fxLayoutAlign="center center" fxFlex> <div *ngIf="!mail" fxLayout="column" fxLayoutAlign="center center" fxFlex>
<mat-icon class="s-128 mb-16 select-message-icon" *fuseIfOnDom [@animate]="{value:'*',params:{delay:'300ms',scale:'0.2'}}">email</mat-icon> <mat-icon class="s-128 mb-16 select-message-icon" *fuseIfOnDom [@animate]="{value:'*',params:{delay:'300ms',scale:'0.2'}}">
<span class="select-message-text hint-text" *fuseIfOnDom [@animate]="{value:'*',params:{delay:'400ms'}}">Select a message to read</span> email
</mat-icon>
<span class="select-message-text hint-text" *fuseIfOnDom [@animate]="{value:'*',params:{delay:'400ms'}}">
<span>{{ 'MAIL.SELECT_A_MESSAGE_TO_READ' | translate }}</span>
</span>
</div> </div>
<div *ngIf="mail"> <div *ngIf="mail">

View File

@ -1,5 +1,5 @@
<div *ngIf="mails.length === 0" fxLayout="column" fxLayoutAlign="center center" fxFlexFill> <div *ngIf="mails.length === 0" fxLayout="column" fxLayoutAlign="center center" fxFlexFill>
<span class="no-messages-text hint-text">There are no messages!</span> <span class="no-messages-text hint-text">{{ 'MAIL.NO_MESSAGES' | translate }}</span>
</div> </div>
<div class="mail-list" *fuseIfOnDom [@animateStagger]="{value:'50'}"> <div class="mail-list" *fuseIfOnDom [@animateStagger]="{value:'50'}">

View File

@ -29,7 +29,7 @@
<div class="search mat-white-bg" flex fxLayout="row" fxLayoutAlign="start center"> <div class="search mat-white-bg" flex fxLayout="row" fxLayoutAlign="start center">
<mat-icon>search</mat-icon> <mat-icon>search</mat-icon>
<input [formControl]="searchInput" placeholder="Search for an e-mail or contact" fxFlex> <input [formControl]="searchInput" [placeholder]="'MAIL.SEARCH_PLACEHOLDER' | translate" fxFlex>
</div> </div>
</div> </div>

View File

@ -3,6 +3,9 @@ import { MailService } from './mail.service';
import { Subscription } from 'rxjs/Subscription'; import { Subscription } from 'rxjs/Subscription';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { Mail } from './mail.model'; import { Mail } from './mail.model';
import { FuseTranslationLoaderService } from '../../../../core/services/translation-loader.service';
import { locale as english } from './i18n/en';
import { locale as turkish } from './i18n/tr';
@Component({ @Component({
selector : 'fuse-mail', selector : 'fuse-mail',
@ -25,9 +28,13 @@ export class FuseMailComponent implements OnInit, OnDestroy
onLabelsChanged: Subscription; onLabelsChanged: Subscription;
onCurrentMailChanged: Subscription; onCurrentMailChanged: Subscription;
constructor(private mailService: MailService) constructor(
private mailService: MailService,
private translationLoader: FuseTranslationLoaderService
)
{ {
this.searchInput = new FormControl(''); this.searchInput = new FormControl('');
this.translationLoader.loadTranslations(english, turkish);
} }
ngOnInit() ngOnInit()

View File

@ -30,13 +30,13 @@
class="mat-accent compose-dialog-button" class="mat-accent compose-dialog-button"
(click)="composeDialog()" (click)="composeDialog()"
aria-label="Compose"> aria-label="Compose">
COMPOSE {{ 'MAIL.COMPOSE' | translate }}
</button> </button>
</div> </div>
<div class="nav"> <div class="nav">
<div class="nav-subheader">FOLDERS</div> <div class="nav-subheader">{{ 'MAIL.FOLDERS' | translate }}</div>
<div class="nav-item" *ngFor="let folder of folders"> <div class="nav-item" *ngFor="let folder of folders">
<a class="nav-link" matRipple [routerLink]="'/apps/mail/' + folder.handle" routerLinkActive="active"> <a class="nav-link" matRipple [routerLink]="'/apps/mail/' + folder.handle" routerLinkActive="active">
@ -45,7 +45,7 @@
</a> </a>
</div> </div>
<div class="nav-subheader">FILTERS</div> <div class="nav-subheader">{{ 'MAIL.FILTERS' | translate }}</div>
<div class="nav-item" *ngFor="let filter of filters"> <div class="nav-item" *ngFor="let filter of filters">
<a class="nav-link" matRipple [routerLink]="'/apps/mail/filter/' + filter.handle" routerLinkActive="active"> <a class="nav-link" matRipple [routerLink]="'/apps/mail/filter/' + filter.handle" routerLinkActive="active">
@ -54,7 +54,7 @@
</a> </a>
</div> </div>
<div class="nav-subheader">LABELS</div> <div class="nav-subheader">{{ 'MAIL.LABELS' | translate }}</div>
<div class="nav-item" *ngFor="let label of labels"> <div class="nav-item" *ngFor="let label of labels">
<a class="nav-link" matRipple [routerLink]="'/apps/mail/label/' + label.handle" routerLinkActive="active"> <a class="nav-link" matRipple [routerLink]="'/apps/mail/label/' + label.handle" routerLinkActive="active">

View File

@ -4,6 +4,7 @@ import { RouterModule } from '@angular/router';
import { FuseCountdownDocsComponent } from './countdown/countdown.component'; import { FuseCountdownDocsComponent } from './countdown/countdown.component';
import { FuseHljsDocsComponent } from './hljs/hljs.component'; import { FuseHljsDocsComponent } from './hljs/hljs.component';
import { FuseMaterialColorPickerDocsComponent } from './material-color-picker/material-color-picker.component'; import { FuseMaterialColorPickerDocsComponent } from './material-color-picker/material-color-picker.component';
import { FuseMultiLanguageDocsComponent } from './multi-language/multi-language.component';
import { FuseNavigationDocsComponent } from './navigation/navigation.component'; import { FuseNavigationDocsComponent } from './navigation/navigation.component';
import { FuseShortcutsDocsComponent } from './shortcuts/shortcuts.component'; import { FuseShortcutsDocsComponent } from './shortcuts/shortcuts.component';
import { FuseSearchBarDocsComponent } from 'app/main/content/components/search-bar/search-bar.component'; import { FuseSearchBarDocsComponent } from 'app/main/content/components/search-bar/search-bar.component';
@ -23,6 +24,10 @@ const routes = [
path : 'components/material-color-picker', path : 'components/material-color-picker',
component: FuseMaterialColorPickerDocsComponent component: FuseMaterialColorPickerDocsComponent
}, },
{
path : 'components/multi-language',
component: FuseMultiLanguageDocsComponent
},
{ {
path : 'components/navigation', path : 'components/navigation',
component: FuseNavigationDocsComponent component: FuseNavigationDocsComponent
@ -51,6 +56,7 @@ const routes = [
FuseCountdownDocsComponent, FuseCountdownDocsComponent,
FuseHljsDocsComponent, FuseHljsDocsComponent,
FuseMaterialColorPickerDocsComponent, FuseMaterialColorPickerDocsComponent,
FuseMultiLanguageDocsComponent,
FuseNavigationDocsComponent, FuseNavigationDocsComponent,
FuseSearchBarDocsComponent, FuseSearchBarDocsComponent,
FuseShortcutsDocsComponent, FuseShortcutsDocsComponent,

View File

@ -0,0 +1,138 @@
<div id="multi-language" class="page-layout simple fullwidth" fusePerfectScrollbar>
<!-- HEADER -->
<div class="header mat-accent-bg p-24 h-160" fxLayout="row" fxLayoutAlign="start center">
<div fxLayout="column" fxLayoutAlign="center start">
<div class="black-fg" fxLayout="row" fxLayoutAlign="start center">
<mat-icon class="secondary-text s-16">home</mat-icon>
<mat-icon class="secondary-text s-16">chevron_right</mat-icon>
<span class="secondary-text">Components</span>
</div>
<div class="h2 mt-16">Multi Language</div>
</div>
</div>
<!-- / HEADER -->
<!-- CONTENT -->
<div class="content p-24">
<p>
Fuse uses <a class="link" href="https://github.com/ngx-translate/core" target="_blank">ngx-translate</a>
module and supports multiple languages and translations.
</p>
<p class="warning-box">
Since not everybody need multi-language setup for their apps, we decided NOT to put translations everywhere.
If you want to see the translations in action, visit <a class="link" [routerLink]="'/apps/mail'">
Mail</a> app and then change the language from the <b>Toolbar.</b>
<br><br>
Mail app is the only app that has translations for demonstration purposes. You can look at its source code
to see the usage.
</p>
<div class="my-48">
<h2>Usage</h2>
<p>In order to use the translations, create your translation file within the module you want to use
the translations. For example, for the Mail app, create <code>i18n/en.ts</code> file inside the
<code>apps/mail</code> folder.
</p>
<p>
The structure of the translation file is important and it must define the language id along with the
translation data:
</p>
<p class="mat-grey-200-bg py-8">
<fuse-hljs lang="ts" class="source-code">
<textarea #source hidden="hidden">
// i18n/en.ts
export const locale = {
lang: 'en',
data: {
'MAIL': {
'COMPOSE': 'COMPOSE'
}
}
};
// i18n/tr.ts
export const locale = {
lang: 'en',
data: {
'MAIL': {
'COMPOSE': 'YENİ E-POSTA'
}
}
};
</textarea>
</fuse-hljs>
</p>
</div>
<div class="my-48">
<p>
After you create your translation files, open the <code>*.component.ts</code> file for the module you
want to have translations, and register your translation file. For this example, we will use the
<code>mail.component.ts</code> file:
</p>
<p class="mat-grey-200-bg py-8">
<fuse-hljs lang="ts" class="source-code">
<textarea #source hidden="hidden">
// Your imports
import { ... } from '..';
// Import the locale files
import { locale as english } from './i18n/en';
import { locale as turkish } from './i18n/tr';
@Component({
selector : 'fuse-mail',
templateUrl: './mail.component.html',
styleUrls : ['./mail.component.scss']
})
export class FuseMailComponent
{
constructor(private translationLoader: FuseTranslationLoaderService)
{
this.translationLoader.loadTranslations(english, turkish);
}
...
}
</textarea>
</fuse-hljs>
</p>
</div>
<div class="my-48">
<h2>Changing the language</h2>
<p class="py-8">
Changing the current language can happen instantly. Simply call the <code>use</code> method from the
translate service:
</p>
<p class="mat-grey-200-bg py-8">
<fuse-hljs lang="ts" class="source-code">
<textarea #source hidden="hidden">
import { TranslateService } from '@ngx-translate/core';
constructor(private translate: TranslateService) {}
setLanguage()
{
// Use the selected language for translations
this.translate.use('tr');
}
</textarea>
</fuse-hljs>
</p>
<p>
More detailed usage of the translation service can be found in the <code>toolbar.component.ts</code>
file.
</p>
</div>
</div>
</div>

View File

@ -0,0 +1,13 @@
:host {
.content{
max-width: 1100px;
.warning-box{
background: #FFFDE7;
border: 1px solid #FFC107;
padding: 16px;
}
}
}

View File

@ -0,0 +1,13 @@
import { Component } from '@angular/core';
@Component({
selector : 'fuse-multi-language-docs',
templateUrl: './multi-language.component.html',
styleUrls : ['./multi-language.component.scss']
})
export class FuseMultiLanguageDocsComponent
{
constructor()
{
}
}

View File

@ -71,7 +71,7 @@
</button> </button>
<mat-menu #languageMenu="matMenu"> <mat-menu #languageMenu="matMenu">
<button mat-menu-item *ngFor="let lang of languages" (click)="selectedLanguage = lang"> <button mat-menu-item *ngFor="let lang of languages" (click)="setLanguage(lang)">
<div fxLayout="row" fxLayoutAlign="start center"> <div fxLayout="row" fxLayoutAlign="start center">
<img class="flag mr-16" [src]="'assets/images/flags/'+lang.flag+'.png'"> <img class="flag mr-16" [src]="'assets/images/flags/'+lang.flag+'.png'">
<span class="iso">{{lang.title}}</span> <span class="iso">{{lang.title}}</span>

View File

@ -1,6 +1,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { NavigationEnd, NavigationStart, Router } from '@angular/router'; import { NavigationEnd, NavigationStart, Router } from '@angular/router';
import { FuseConfigService } from '../../core/services/config.service'; import { FuseConfigService } from '../../core/services/config.service';
import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector : 'fuse-toolbar', selector : 'fuse-toolbar',
@ -18,7 +19,8 @@ export class FuseToolbarComponent
constructor( constructor(
private router: Router, private router: Router,
private fuseConfig: FuseConfigService private fuseConfig: FuseConfigService,
private translate: TranslateService
) )
{ {
this.userStatusOptions = [ this.userStatusOptions = [
@ -55,11 +57,6 @@ export class FuseToolbarComponent
'title': 'English', 'title': 'English',
'flag' : 'us' 'flag' : 'us'
}, },
{
'id' : 'es',
'title': 'Spanish',
'flag' : 'es'
},
{ {
'id' : 'tr', 'id' : 'tr',
'title': 'Turkish', 'title': 'Turkish',
@ -92,4 +89,13 @@ export class FuseToolbarComponent
// Do your search here... // Do your search here...
console.log(value); console.log(value);
} }
setLanguage(lang)
{
// Set the selected language for toolbar
this.selectedLanguage = lang;
// Use the selected language for translations
this.translate.use(lang.id);
}
} }

View File

@ -725,6 +725,13 @@ export class NavigationModel
'icon' : 'settings_input_component', 'icon' : 'settings_input_component',
'url' : '/components/material-color-picker' 'url' : '/components/material-color-picker'
}, },
{
'id' : 'multi-language',
'title': 'Multi Language',
'type' : 'item',
'icon' : 'settings_input_component',
'url' : '/components/multi-language'
},
{ {
'id' : 'navigation', 'id' : 'navigation',
'title': 'Navigation', 'title': 'Navigation',