An implementation of routes localization for Angular.
Based on and extension of ngx-translate. Demo project can be found here.
npm install --save localize-router
In order to use localize-router
you must initialize it with following information:
- Available languages/locales
- Prefix for route segment translations
- Routes to be translated
Module can be initialized either using static file or manually by passing necessary values.
import {BrowserModule} from "@angular/platform-browser";
import {NgModule} from '@angular/core';
import {HttpModule} from '@angular/http';
import {TranslateModule} from '@ngx-translate/core';
import {LocalizeRouterModule} from 'localize-router/localize-router';
import {RouterModule} from '@angular/router';
import {routes} from './app.routes';
@NgModule({
imports: [
BrowserModule,
HttpModule,
TranslateModule.forRoot(),
LocalizeRouterModule.forRoot(routes),
RouterModule.forRoot(routes)
],
bootstrap: [AppComponent]
})
export class AppModule {
}
Static file's default path is assets/locales.json
. You can override the path by calling StaticParserLoader
on you own:
LocalizeRouterModule.forRoot(routes, {
provide: LocalizeParser,
useFactory: (translate, location, http) =>
new StaticParserLoader(translate, location, http, 'your/path/to/config.json'),
deps: [TranslateService, Location, Http]
})
If you are using child modules or routes you need to initialize them with forChild
command:
@NgModule({
imports: [
TranslateModule,
LocalizeRouterModule.forChild(routes),
RouterModule.forChild(routes)
],
declarations: [ChildComponent]
})
export class ChildModule { }
JSON config file has following structure:
{
"locales": ["en", "de", ...],
"prefix": "MY_PREFIX"
}
interface ILocalizeRouteConfig {
locales: Array<string>;
prefix?: string;
}
Prefix field is not mandatory and default value is empty string.
With manual initialization you need to provide information directly:
LocalizeRouterModule.forRoot(routes, {
provide: LocalizeParser,
useFactory: (translate, location) =>
new ManualParserLoader(translate, location, ['en','de',...], 'YOUR_PREFIX'),
deps: [TranslateService, Location]
})
In order to use server side initialization in isomorphic/universal projects you need to create loader similar to this:
export class LocalizeUniversalLoader extends LocalizeParser {
/**
* Gets config from the server
* @param routes
*/
public load(routes: Routes): Promise<any> {
return new Promise((resolve: any) => {
let data: any = JSON.parse(fs.readFileSync(`assets/locales.json`, 'utf8'));
this.locales = data.locales;
this.prefix = data.prefix;
this.init(routes).then(resolve);
});
}
}
export function localizeLoaderFactory(translate: TranslateService, location: Location) {
return new LocalizeUniversalLoader(translate, location);
}
Don't forget to create similar loader for ngx-translate
as well:
export class TranslateUniversalLoader implements TranslateLoader {
/**
* Gets the translations from the server
* @param lang
* @returns {any}
*/
public getTranslation(lang: string): Observable<any> {
return Observable.create(observer => {
observer.next(JSON.parse(fs.readFileSync(`src/assets/locales/${lang}.json`, 'utf8')));
observer.complete();
});
}
}
export function translateLoaderFactory() {
return new TranslateUniversalLoader();
}
Since node server expects to know which routes are allowed you can feed it like this:
let fs = require('fs');
let data: any = JSON.parse(fs.readFileSync(`src/assets/locales.json`, 'utf8'));
app.get('/', ngApp);
data.locales.forEach(route => {
app.get(`/${route}`, ngApp);
app.get(`/${route}/*`, ngApp);
});
Working example can be found here.
Localize router
intercepts Router initialization and translates each path
and redirectTo
path of Routes.
The translation process is done with ngx-translate. In order to separate
router translations from normal application translations we use prefix
. Default value for prefix is ROUTES.
.
'home' -> 'ROUTES.home'
Upon every route change Localize router
kicks in to check if there was a change to language. Translated routes are prepended with two letter language code:
http://yourpath/home -> http://yourpath/en/home
If no language is provided in the url path, application uses:
- cached language in LocalStorage (browser only) or
- current language of the browser (browser only) or
- first locale in the config
Make sure you therefore place most common language (e.g. 'en') as a first string in the array of locales.
Note that
localize-router
does not redirect routes likemy/route
to translated ones e.g.en/my/route
. All routes are prepended by currently selected language so route without language is unknown to Router.
LocalizeRouter
depends on ngx-translate
core service and automatically initializes it with selected locales.
Following code is run on LocalizeParser
init:
this.translate.setDefaultLang(cachedLanguage || languageOfBrowser || firstLanguageFromConfig);
// ...
this.translate.use(languageFromUrl || cachedLanguage || languageOfBrowser || firstLanguageFromConfig);
Both languageOfBrowser
and languageFromUrl
are cross-checked with locales from config.
LocalizeRouterPipe
is used to translate routerLink
directive's content. Pipe can be appended to partial strings in the routerLink's definition or to entire array element:
<a [routerLink]="['user', userId, 'profile'] | localize">{{'USER_PROFILE' | translate}}</a>
<a [routerLink]="['about' | localize]">{{'ABOUT' | translate}}</a>
Root routes work the same way with addition that in case of root links, link is prepended by language. Example for german language and link to 'about':
'/about' | localize -> '/de/über'
Routes can be manually translated using LocalizeRouterService
. This is important if you want to use router.navigate
for dynamical routes.
class MyComponent {
constructor(private localize: LocalizeRouterService) { }
myMethod() {
let translatedPath: any = this.localize.translateRoute('about/me');
// do something with translated path
// e.g. this.router.navigate([translatedPath]);
}
}
In order to use Ahead-Of-Time compilation any custom loaders must be exported as functions. This is the implementation currently in the solution:
export function localizeLoaderFactory(translate: TranslateService, location: Location, http: Http) {
return new StaticParserLoader(translate, location, http);
}
routerEvents
: An EventEmitter to listen to language change event
localizeService.routerEvents.subscribe((language: string) => {
// do something with language
});
parser
: Used instance of LocalizeParser
let selectedLanguage = localizeService.parser.currentLang;
translateRoute(path: string | any[]): string | any[]
: Translates given path. Ifpath
starts with backslash then path is prepended with currently set language.
localizeService.translateRoute('/'); // -> e.g. '/en'
localizeService.translateRoute('/about'); // -> '/de/uber' (e.g. for German language)
localizeService.translateRoute('about'); // -> 'uber' (e.g. for German language)
changeLanguage(lang: string)
: Translates current url to given language and changes the application's language For german language and route defined as:lang/users/:user_name/profile
yoursite.com/en/users/John%20Doe/profile -> yoursite.com/de/benutzer/John%20Doe/profil
locales
: Array of used language codescurrentLang
: Currently selected languageroutes
: Active translated routes
translateRoutes(language: string): Observable<any>
: Translates all the routes and sets language and current language across the application.translateRoute(path: string): string
: Translates single pathgetLocationLang(url?: string): string
: Extracts language from current url if matching defined locales
Licensed under MIT