An implementation of routes localization for Angular.
- ngx-translate-router
- ngx-translate-router-http-loader
- ngx-translate-router-scully-plugin (Documentation here)
Fork of localize-router.
Based on and extension of ngx-translate.
Version to choose :
angular version | translate-router | http-loader | type | remarks |
---|---|---|---|---|
6 - 7 | 1.0.2 | 1.0.1 | legacy | |
7 | 1.7.3 | 1.1.0 | legacy | |
8 | 2.2.3 | 1.1.0 | legacy | |
8 - 12 | 3.1.9 | 1.1.2 | active | |
13 | 4.0.1 | 2.0.0 | active | |
14 | 5.1.1 | 2.0.0 | active | need rxjs 7 or higher |
15 | 6.0.0 | 2.0.0 | active | minimum angular 15.0.3 |
15.1 | 6.1.0 | 2.0.0 | active | minimum angular 15.1.0 |
16 | 7.0.0 | 2.0.0 | active | minimum angular 16 |
17 | 7.1.0 | 2.0.0 | active | optional standalone API |
18 | 7.2.1 | 2.0.0 | active |
Demo project can be found under sub folder src
.
This documentation is for version 1.x.x which requires Angular 6+. If you are migrating from the older version follow migration guide to upgrade to latest version.
npm install --save @gilsdav/ngx-translate-router
In order to use @gilsdav/ngx-translate-router
you must initialize it with following information:
- Available languages/locales
- Prefix for route segment translations
- Routes to be translated
import {LocalizeRouterModule} from '@gilsdav/ngx-translate-router';
Module can be initialized either using static file or manually by passing necessary values.
Be careful to import this module after the standard RouterModule and the TranslateModule. This should be done for the main router as well as for lazy loaded ones.
imports: [
TranslateModule.forRoot(),
RouterModule.forRoot(routes),
LocalizeRouterModule.forRoot(routes) // <--
]
Standalone mode is the new Angular API that allow you to manage your application without ng-modules.
This library provide an additional "RouterConfigurationFeature" called withLocalizeRouter
you can provide to provideRouter
Angular built-in function.
Parameters for this function are exactly the same as for LocalizeRouterModule.forRoot()
.
Here is an example to configure it within an SSR app:
providers: [
provideHttpClient(withFetch()),
importProvidersFrom(TranslateModule.forRoot()),
provideRouter(
routes,
withDisabledInitialNavigation(),
withLocalizeRouter(routes, { // <--
parser: {
provide: LocalizeParser,
useFactory: (createTranslateRouteLoader),
deps: [TranslateService, Location, LocalizeRouterSettings]
},
initialNavigation: true
})
),
provideClientHydration()
]
You are also able to import LocalizeRouterPipe
into your standalone components.
In order to use Http loader for config files, you must include @gilsdav/ngx-translate-router-http-loader
package and use its LocalizeRouterHttpLoader
.
import {BrowserModule} from "@angular/platform-browser";
import {NgModule} from '@angular/core';
import {Location} from '@angular/common';
import {HttpClientModule, HttpClient} from '@angular/common/http';
import {TranslateModule} from '@ngx-translate/core';
import {LocalizeRouterModule} from '@gilsdav/ngx-translate-router';
import {LocalizeRouterHttpLoader} from '@gilsdav/ngx-translate-router-http-loader';
import {RouterModule} from '@angular/router';
import {routes} from './app.routes';
@NgModule({
imports: [
BrowserModule,
HttpClientModule,
TranslateModule.forRoot(),
RouterModule.forRoot(routes),
LocalizeRouterModule.forRoot(routes, {
parser: {
provide: LocalizeParser,
useFactory: (translate, location, settings, http) =>
new LocalizeRouterHttpLoader(translate, location, settings, http),
deps: [TranslateService, Location, LocalizeRouterSettings, HttpClient]
}
})
],
bootstrap: [AppComponent]
})
export class AppModule { }
More details are available on localize-router-http-loader.
If you are using child modules or routes you need to initialize them with forChild
command:
@NgModule({
imports: [
TranslateModule,
RouterModule.forChild(routes),
LocalizeRouterModule.forChild(routes)
],
declarations: [ChildComponent]
})
export class ChildModule { }
With manual initialization you need to provide information directly:
LocalizeRouterModule.forRoot(routes, {
parser: {
provide: LocalizeParser,
useFactory: (translate, location, settings) =>
new ManualParserLoader(translate, location, settings, ['en','de',...], 'YOUR_PREFIX'),
deps: [TranslateService, Location, LocalizeRouterSettings]
}
})
Apart from providing routes which are mandatory, and parser loader you can provide additional configuration for more granular setting of @gilsdav/ngx-translate-router
. More information at LocalizeRouterConfig.
In order to use @gilsdav/ngx-translate-router
in Angular universal application (SSR) you need to:
-
In case you opted for initializing with Http loader, you need to take care of static file location.
@gilsdav/ngx-translate-router-http-loader
by default will try loading the config file fromassets/locales.json
. This is a relative path which won't work with SSR. You could use one of the following approaches,- Creating a factory function to override the default location with an absolute URL
export function localizeLoaderFactory(translate: TranslateService, location: Location, settings: LocalizeRouterSettings, http: HttpClient) { return new LocalizeRouterHttpLoader(translate, location, settings, http, 'http://example.com/assets/locales.json'); } LocalizeRouterModule.forRoot(routes, { parser: { provide: LocalizeParser, useFactory: localizeLoaderFactory, deps: [TranslateService, Location, LocalizeRouterSettings, HttpClient] } })
- Using an HTTP interceptor in your
server.module
to convert relative paths to absolute ons, ex:intercept(request: HttpRequest<any>, next: HttpHandler) { if (request.url.startsWith('assets') && isPlatformServer(this.platformId)) { const req = this.injector.get(REQUEST); const url = req.protocol + '://' + req.get('host') + '/' + request.url; request = request.clone({ url: url }); } return next.handle(request); }
- Creating a factory function to override the default location with an absolute URL
-
Let node server knows about the new routes:
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); });
-
In case you want to use
cacheMechanism = CacheMechanism.Cookie
you will need to handle the cookie in your node server. Something like,app.use(cookieParser()); app.get('/', (req, res) => { const defaultLang = 'de'; const lang = req.acceptsLanguages('de', 'en'); const cookieLang = req.cookies.LOCALIZE_DEFAULT_LANGUAGE; // This is the default name of cookie const definedLang = cookieLang || lang || defaultLang; res.redirect(301, `/${definedLang}/`); });
Gotchas
-
In case you are using domino in your project, you will face the following error
ERROR TypeError: Cannot read property 'indexOf' of undefined at TranslateService.getBrowserLang
to overcome this, use the following:
// language is readonly so normally you can't assign a value to it. // The following comments remove the compile time error and the IDE warning // @ts-ignore // noinspection JSAnnotator window.navigator.language = 'en';
When you add Universal into your app you will have initialNavigation
set to "enabled"
. This is to avoid the flickering of the lazy-load.
Unfortunatly it doesn't help with this library and can cause issues.
So you need to set it to "disabled"
and add the ngx-translate-router option initialNavigation: true
to have this desired behavior.
imports: [
RouterModule.forRoot(routes, { initialNavigation: 'disabled' }),
LocalizeRouterModule.forRoot(routes, {
...
initialNavigation: true
})
]
@gilsdav/ngx-translate-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.
. Finally, in order to avoid accidentally translating a URL segment that should not be translated, you can optionally use escapePrefix
so the prefix gets stripped and the segment doesn't get translated. Default escapePrefix
is unset.
'home' -> 'ROUTES.home'
Example to escape the translation of the segment with escapePrefix: '!'
'!segment' -> 'segment'
{ path: '!home/first' ... } -> '/fr/home/premier'
Upon every route change @gilsdav/ngx-translate-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/SessionStorage/Cookie (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
ngx-translate-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.
Sometimes you might have a need to have certain routes excluded from the localization process e.g. login page, registration page etc. This is possible by setting flag skipRouteLocalization
on route's data object.
In case you want to redirect to an url when skipRouteLocalization is activated, you can also provide config option localizeRedirectTo
to skip route localization but localize redirect to. Otherwise, route and redirectTo will not be translated.
let routes = [
// this route gets localized
{ path: 'home', component: HomeComponent },
// this route will not be localized
{ path: 'login', component: LoginComponent, data: { skipRouteLocalization: true } }
// this route will not be localized, but redirect to will do
{ path: 'logout', redirectTo: 'login', data: { skipRouteLocalization: { localizeRedirectTo: true } } }
];
Note that this flag should only be set on root routes. By excluding root route, all its sub routes are automatically excluded. Setting this flag on sub route has no effect as parent route would already have or have not language prefix.
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.
Do you use same path to load multiple lazy-loaded modules and you have wrong component tree ?
discriminantPathKey
will help ngx-translate-router to generate good component tree.
{
path: '',
loadChildren: () => import('app/home/home.module').then(m => m.HomeModule),
data: {
discriminantPathKey: 'HOMEPATH'
}
},
{
path: '',
loadChildren: () => import('app/information/information.module').then(m => m.InformationModule),
data: {
discriminantPathKey: 'INFOPATH'
}
}
The favored way to use WildCard ( '**'
path ) is to use the redirectTo
. It will let the user to translate the "not found" page message.
{
path: '404',
component: NotFoundComponent
},
{
path: '**',
redirectTo: '/404'
}
If you need to keep the wrong url you will face to a limitation: You can not translate current page. This limitation is because we can not determine the language from a wrong url.
{
path: '**',
component: NotFoundComponent
}
In case you want to translate some params of matcher, localizeMatcher
provides you the way to do it through a function per each param. Make sure that the key is the same as the one used in the navigate path (example: if the function returns "map", it must be contained in the not localized path: [routerLink]="['/matcher', 'aaa', 'map'] | localize"
) otherwise you will not be able to use routerLinkActiveOptions
.
Example:
{
path: 'matcher',
children: [
{
matcher: detailMatcher,
loadChildren: () => import('./matcher/matcher-detail/matcher-detail.module').then(mod => mod.MatcherDetailModule)
},
{
matcher: baseMatcher,
loadChildren: () => import('./matcher/matcher.module').then(mod => mod.MatcherModule),
data: {
localizeMatcher: {
params: {
mapPage: shouldTranslateMap
}
}
}
}
]
}
...
export function shouldTranslateMap(param: string): string {
if (isNaN(+param)) {
return 'map';
}
return null;
}
The output of the function should be falsy
if the param must not be translated or should return the key
(without prefix) you want to use when translating if you want to translate the param.
Notice that any function that you use in localizeMatcher
must be exported to be compatible with AOT.
We work with UrlSegment
to split URL into "params" in basic UrlMatchResult
but there is not enough information to apply the translations.
You must use the LocalizedMatcherUrlSegment
type to more strongly associate a segment with a parameter. It contains only the localizedParamName
attribute in addition to basic UrlSegment. Set this attribute before adding the segment into consumed
and posParams
.
const result: UrlMatchResult = {
consumed: [],
posParams: { }
};
...
(segment as LocalizedMatcherUrlSegment).localizedParamName = name;
result.consumed.push(segment);
result.posParams[name] = segment;
If the URL is accidentally translated from a language to another which creates an inconsistent state you have to enable escapePrefix
mechanism. (example: escapePrefix: '!'
)
Starting in Angular 18 introduced redirectTo as a function in addition to a string. If you use this feature, you can use the redirectTo
function to translate the path. However, the library translates the entire router at the start of the application statically, which means that the translate function has no yet the navigation context of the user in that moment as it should be evaluated dynamically once the user navigates to such route. If you want to use this feature, you can use the LocalizeRouterService
to translate the path injecting the service as in this example.
{
path: 'conditionalRedirectTo', redirectTo: ({ queryParams }) => {
const localizeRouterService = inject(LocalizeRouterService);
if (queryParams['redirect']) {
return localizeRouterService.translateRoute('/test') as string;
}
return localizeRouterService.translateRoute('/home') as string;
}
}
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);
}
forRoot(routes: Routes, config: LocalizeRouterConfig = {}): ModuleWithProviders
: Main initializer for @gilsdav/ngx-translate-router. Can provide custom configuration for more granular settings.forChild(routes: Routes): ModuleWithProviders
: Child module initializer for providing child routes.
parser
: Provider for loading of LocalizeParser. Default value isStaticParserLoader
.useCachedLang
: boolean. Flag whether default language should be cached. Default value istrue
.alwaysSetPrefix
: boolean. Flag whether language should always prefix the url. Default value istrue
.
When value isfalse
, prefix will not be used for for default language (this includes the situation when there is only one language).cacheMechanism
: CacheMechanism.LocalStorage || CacheMechanism.SessionStorage || CacheMechanism.Cookie. Default value isCacheMechanism.LocalStorage
.cacheName
: string. Name of cookie/local store. Default value isLOCALIZE_DEFAULT_LANGUAGE
.defaultLangFunction
: (languages: string[], cachedLang?: string, browserLang?: string) => string. Override method for custom logic for picking default language, when no language is provided via url. Default value isundefined
.cookieFormat
: string. Format of cookie to store. Default value is'{{value}};{{expires}}'
. (Extended format e.g :'{{value}};{{expires}};path=/'
){{value}}
will be replaced by the value to save (CACHE_NAME=language
). Must be present into format.{{expires}}
will be replaced byexpires=currentDate+30days
. Optional if you want session cookie.- you can configure the number of expiration days by using this synthax:
{{expires:365}}
. It will result asexpires=currentDate+365days
.
- you can configure the number of expiration days by using this synthax:
- results to :
LOCALIZE_DEFAULT_LANGUAGE=en;expires=Wed, 11 Sep 2019 21:19:23 GMT
.
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/ueber-uns' (e.g. for German language)
localizeService.translateRoute('about'); // -> 'ueber-uns' (e.g. for German language)
changeLanguage(lang: string, extras?: NavigationExtras, useNavigateMethod?: boolean)
: Translates current url to given language and changes the application's language.extras
will be passed down to Angular Router navigation methods.userNavigateMethod
tells localize-router to usenavigate
rather thannavigateByUrl
method.
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
For now there is only one hook which is only interesting if you are using initialNavigation
flag (and more specifically, if you are making an AppInitializer
that uses Angular's Router
or ngx-translate-router
).
hooks.initialized
: an observable with event sent when initialNavigation is executed.
Usage example:
export const appInitializerFactory = (injector: Injector) => {
return () => {
const localize = injector.get(LocalizeRouterService);
return firstValueFrom(
localize.hooks.initialized
.pipe(
tap(() => {
const router = injector.get(Router);
router.events.pipe(
filter(url => url instanceof NavigationEnd),
first()
).subscribe((route: NavigationEnd) => {
console.log(router.url, route.url);
router.navigate(['/fr/accueil']);
});
})
)
)
}
};
locales
: Array of used language codescurrentLang
: Currently selected languageroutes
: Active translated routesurlPrefix
: Language prefix for current language. Empty string ifalwaysSetPrefix=false
andcurrentLang
is same as default language.
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
Thanks to all our contributors
As well as to all the contributors of the initial project Made with contributors-img.