@ngx-i18n-router/core

Route internationalization utility for Angular

Stats

stars 🌟issues ⚠️updated 🛠created 🐣size 🏋️‍♀️
3829Nov 19, 2019Feb 2, 2017Minified + gzip package size for @ngx-i18n-router/core in KB

Readme

@ngx-i18n-router/core npm version npm downloads

Route internationalization utility for Angular

CircleCI coverage tested with jest Conventional Commits Angular Style Guide

Please support this project by simply putting a Github star. Share this library with friends on Twitter and everywhere else you can.

@ngx-i18n-router/core translates each path and redirectTo property of routes, during Angular app initialization and also during runtime - when the working language gets changed.

NOTICE

This 5.x.x branch is intented to work with @angular v5.x.x. If you're developing on a later release of Angular than v5.x.x, then you should probably choose the appropriate version of this library by visiting the master branch.

Also, please check the Workaround for '@ngtools/webpack' section if your app depends on @angular/cli or @ngtools/webpack for AoT compilation.

Table of contents:

Prerequisites

This library depends on Angular v4.0.0. Older versions contain outdated dependencies, might produce errors.

Also, please ensure that you are using Typescript v2.5.3 or higher.

Getting started

Installation

You can install @ngx-i18n-router/core using npm

npm install @ngx-i18n-router/core --save

Examples

Related packages

The following packages may be used in conjunction with @ngx-i18n-router/core

Recommended packages

The following package(s) have no dependency for @ngx-i18n-router/core, however may provide supplementary/shorthand functionality:

  • @ngx-config/core: provides route translations from the application settings loaded during application initialization
  • @ngx-cache/core: provides caching features to retrieve the route translations using non-static loaders (http, fs, etc.)

Adding @ngx-i18n-router/core to your project (SystemJS)

Add map for @ngx-i18n-router/core in your systemjs.config

'@ngx-i18n-router/core': 'node_modules/@ngx-i18n-router/core/bundles/core.umd.min.js'

### Route configuration

In order to use @ngx-i18n-router/core properly, you should have more or less a similar route structure as follows:

app.routes.ts

export const routes: Routes = [
  {
    path: '',
    children: [
      {
        path: '',
        loadChildren: './+home/home.module#HomeModule'
      },
      {
        path: 'about',
        loadChildren: './+about/about.module#AboutModule'
      },

      ...

    ],
    data: {
      i18n: {
        isRoot: true
      }
    }
  },
  {
    path: 'change-language/:languageCode',
    component: ChangeLanguageComponent
  },

  ...

  {
    path: '**',
    redirectTo: '',
    pathMatch: 'full'
  }
];

I18N-ROOT

The route configuration above shows that, one of the routes contains i18n property inside the data property.

When its value is set to true, @ngx-i18n-router/core will prepend descendants of this route with the 2-letter language code (ex: en/fr/de/nl/tr).

We call this route, with data property containing i18n\isRoot set to true, I18N-ROOT of Angular application.

(English)

http://mysite.com/about -> http://mysite.com/en/about

Note: There must be a maximum of one i18n\isRoot in the route configuration, and (if exists) this must be placed inside the data property of any of the first-level routes.

Note: It is always a good practice to have exactly one route (and component) in the Home feature module, with a path set to '' (see home.routes.ts in this readme).

Non-prefixed routes

Routes outside I18N-ROOT scope will NOT have this 2-letter language code prefix. It allows the Angular application to support both prefixed and non-prefixed routes.

(English)

http://mysite.com/about -> http://mysite.com/en/about
http://mysite.com/sitemap
http://mysite.com/admin

Catchall route

There must be a catchall route in the route configuration, redirecting to the I18N-ROOT of the app (here, redirects to ''). The redirectTo property will be reset to the 2-letter language code by @ngx-i18n-router/core.

app.module configuration

Import I18NRouterModule using the mapping '@ngx-i18n-router/core' and append I18NRouterModule.forRoot(routes, {...}) within the imports property of app.module (considering the app.module is the core module in Angular application).

Also, don't forget to provide I18N_ROUTER_PROVIDERS within the providers property of app.module.

Note: I18N_ROUTER_PROVIDERS must be imported in the app.module (instead of in I18NRouterModule), to resolve the I18NRouterService dependency at the uppermost level (otherwise child modules will have different instances of I18NRouterService).

app.module.ts

...
import { I18NRouterModule, I18N_ROUTER_PROVIDERS } from '@ngx-i18n-router/core';
...

@NgModule({
  declarations: [
    AppComponent,
    ...
  ],
  ...
  imports: [
    RouterModule.forRoot(routes),
    I18NRouterModule.forRoot(routes),
    ...
  ],
  ...
  providers: [
    I18N_ROUTER_PROVIDERS,
    ...
  ],
  ...
  bootstrap: [AppComponent]
})

Feature modules configuration

Import I18NRouterModule using the mapping '@ngx-i18n-router/core' and append I18NRouterModule.forChild(routes, moduleKey) within the imports property of the feature module. The moduleKey parameter for the forChild method obviously refers to the module's root path (in kebab-case).

home.routes.ts

export const routes: Routes = [
  {
    path: '',
    component: HomeComponent
  }
];

home.module.ts

...
import { I18NRouterModule } from '@ngx-i18n-router/core';
...

@NgModule({
  ...
  imports: [
    //RouterModule.forChild(routes),
    I18NRouterModule.forChild(routes, 'home'),
    ...
  ],
  ...
})

about.routes.ts

export const routes: Routes = [
  {
    path: '',
    component: AboutComponent
  },
  {
    path: 'us/:topicId',
    component: AboutUsComponent
  },
  {
    path: 'banana',
    component: AboutBananaComponent
  },
  {
    path: 'apple/:fruitId/pear',
    component: AboutApplePearComponent
  }
];

about.module.ts

...
import { I18NRouterModule } from '@ngx-i18n-router/core';
...

@NgModule({
  ...
  imports: [
    //RouterModule.forChild(routes),
    I18NRouterModule.forChild(routes, 'about'),
    ...
  ],
  ...
})

Note: You must comment (or better delete) the line with RouterModule.forChild(routes), in order to get @ngx-i18n-router/core working. forChild method of I18NRouterModule provides routes for feature modules itself (if imports both RouterModule and I18NRouterModule, it will cause @ngx-i18n-router/core to malfunction, even crash).

app.component configuration

Import I18NRouterService using the mapping '@ngx-i18n-router/core' and inject it in the constructor of app.component (considering the app.component is the bootstrap component in Angular application).

Then, invoke the init method to fetch route translations loaded during application initialization and allow the use of @ngx-i18n-router/core by the Angular app.

Lastly, you need to invoke the changeLanguage method by supplying the 2-letter language code, which translates routes to the specified language.

app.component.ts

...
import { I18NRouterService } from '@ngx-i18n-router/core';
...

@Component({
  ...
})
export class AppComponent implements OnInit {
  ...
  constructor(private readonly i18nRouter: I18NRouterService) {
    // invoking the `init` method with false won't allow the use of i18n-router,
    // would be handy in the case you need to use i18n-router programmatically
    i18nRouter.init();
  }
  ...
  ngOnInit(): void {
    this.i18nRouter.changeLanguage('en');
  }
  ...
}

Settings

You can call the forRoot static method using I18NRouterStaticLoader. By default, it is configured to pass the routes to @ngx-i18n-router/core and have no translations.

You can customize this behavior (and ofc other settings) by supplying route translations to I18NRouterStaticLoader.

If you provide route translations using a JSON file or an API, you can call the forRoot static method using the I18NRouterHttpLoader. By default, it is configured to retrieve route translations from the path /routes.json (if not specified).

You can customize this behavior (and ofc other settings) by supplying a file path/api endpoint to I18NRouterHttpLoader.

You can also use the @ngx-i18n-router/config-loader, to reduce the amount of HTTP requests during application initialization, by including route translations within the application settings - if @ngx-config/core is already used to retrieve settings by the Angular app.

The following examples show the use of an exported function (instead of an inline function) for AoT compilation.

Setting up I18NRouterModule to use I18NRouterStaticLoader

app.module.ts

...
import { I18NRouterModule, I18NRouterLoader, I18NRouterStaticLoader, I18N_ROUTER_PROVIDERS, RAW_ROUTES } from '@ngx-i18n-router/core';
...

export function i18nRouterFactory(rawRoutes: Routes): I18NRouterLoader {
  return new I18NRouterStaticLoader({
    routes: rawRoutes,
    translations: {
      "en": {
        "ROOT.ABOUT": "about",
        "ROOT.ABOUT.US": "us",
        "ROOT.ABOUT.BANANA": "banana",
        "ROOT.ABOUT.APPLE": "apple",
        "ROOT.ABOUT.APPLE.PEAR": "pear",
        "CHANGE_LANGUAGE": "change-language"
      },
      "tr": {
        "ROOT.ABOUT": "hakkinda",
        "ROOT.ABOUT.US": "biz",
        "ROOT.ABOUT.BANANA": "muz",
        "ROOT.ABOUT.APPLE": "elma",
        "ROOT.ABOUT.APPLE.PEAR": "armut",
        "CHANGE_LANGUAGE": "dil-secimi"
      }
    }
  });
}

...

@NgModule({
  declarations: [
    AppComponent,
    ...
  ],
  ...
  imports: [
    RouterModule.forRoot(routes),
    I18NRouterModule.forRoot(routes, [
      {
        provide: I18NRouterLoader,
        useFactory: (i18nRouterFactory),
        deps: [RAW_ROUTES]
      }
    ]),
    ...
  ],
  ...
  providers: [
    I18N_ROUTER_PROVIDERS,
    ...
  ],
  ...
  bootstrap: [AppComponent]
})

I18NRouterStaticLoader has one parameter:

  • providedSettings: I18NRouterSettings : i18n-router settings
    • routes: Routes: raw routes
    • translations: any : route translations

Setting up I18NRouterModule to use I18NRouterHttpLoader

If you provide route translations using a JSON file or an API, you can call the forRoot static method using the I18NRouterHttpLoader. By default, it is configured to retrieve route translations from the endpoint /routes.json (if not specified).

You can customize this behavior (and ofc other settings) by supplying a api endpoint to I18NRouterHttpLoader.

You can find detailed information about the usage guidelines for the ConfigHttpLoader here.

Setting up I18NRouterModule to use I18NRouterConfigLoader

I18NRouterConfigLoader provides route translations to @ngx-i18n-router/core using @ngx-config/core.

You can find detailed information about the usage guidelines for the I18NRouterConfigLoader here.

Translations object

The translations object is designed to contain route translations in every language supported by Angular application, in JSON format.

When the changeLanguage method of @ngx-i18n-router/core is invoked, route configuration is reset based on the supplied translations.

You should use the following data structure while working with the translation object:

  • Assume that there're a number of {N} supported languages. The object contains {N} times first-level children,

keyed with its 2-letter language code and valued by translations specific to that language.

  • Language keys are in lowercase.
ex: Angular app supports en, fr and tr

{
  "en": { ... },
  "fr": { ... },
  "tr": { ... }
}
  • Routes within the I18N-ROOT scope must be keyed with the ROOT prefix, followed by a dot char ., and then the moduleKey (module's root path in kebab-case).
  • Route keys are followed (if any, after prefixes) by the path attribute of routes.
  • Route keys are in UPPERCASE.
  • Dot char . is used as a separator between prefixes/parts (ex: ROOT.ABOUT).
ex: Angular app supports en and tr

modules:
  AboutModule (path: 'about')

components:
  AboutComponent (path: '')

routes:
  http://mysite.com/about -> http://mysite.com/en/about, http://mysite.com/tr/hakkinda

{
  "en": {
    "ROOT.ABOUT": "about"
  },
  "tr": {
    "ROOT.ABOUT": "iletisim"
  }
}
  • Routes outside the I18N-ROOT scope must be keyed with the moduleKey (module's root path in kebab-case).
ex: Angular app supports en and tr

modules:
  AboutModule (path: 'about')
  SitemapModule (path: 'sitemap')

components:
  AboutComponent (path: '')
  SitemapComponent (path: '')

routes:
  http://mysite.com/about -> http://mysite.com/en/about, http://mysite.com/tr/hakkinda
  http://mysite.com/sitemap -> http://mysite.com/sitemap

{
  "en": {
    "ROOT.ABOUT": "about"
  },
  "tr": {
    "ROOT.ABOUT": "iletisim"
  }
}
  • Underscores _ must be used instead of dashes - in the route keys (using dashes in keys causes errors). They're automatically replaced with dashes - while I18nRouterService is translating the routes.
ex: Angular app supports en and tr

modules:
  AboutModule (path: 'about')
  SitemapModule (path: 'site-map')

components:
  AboutComponent (path: '')
  SitemapComponent (path: '')

routes:
  http://mysite.com/about -> http://mysite.com/en/about, http://mysite.com/tr/hakkinda
  http://mysite.com/site-map -> http://mysite.com/site-map, http://mysite.com/site-haritasi

{
  "en": {
    "ROOT.ABOUT": "about",
    "SITE_MAP": "site-map"
  },
  "tr": {
    "ROOT.ABOUT": "iletisim",
    "SITE_MAP": "site-haritasi"
  }
}
  • Route paths are split by slashes / into string parts. When keying and they're separated by dots . (ex: path: 'goes/to/your-page' -> 'GOES.TO.YOUR_PAGE').
  • Route params (followed by colon) are not needed to be included in the translations object (*ex: path: goes/to/:id/page
  • 'GOES.TO.PAGE'*)

ex: Angular app supports en and tr

modules:
  AboutModule (path: 'about'), ContactModule (path: 'contact'), ProfileComponent (path: 'profile')
  SitemapModule (path: 'site-map')

components:
  AboutComponent (path: ''), ContactComponent (path: ''), ContactMapComponent (path: 'map'), ProfileComponent (path: 'profile'), ProfileEditComponent (path: 'profile/:id/edit')
  SitemapComponent (path: '')

routes:
  http://mysite.com/about -> http://mysite.com/en/about, http://mysite.com/tr/hakkinda
  http://mysite.com/contact -> http://mysite.com/en/contact, http://mysite.com/tr/iletisim
  http://mysite.com/contact/map -> http://mysite.com/en/contact/map, http://mysite.com/tr/iletisim/harita
  http://mysite.com/profile -> http://mysite.com/en/profile, http://mysite.com/tr/profil
  http://mysite.com/profile/:id/edit -> http://mysite.com/en/profile/:id/edit, http://mysite.com/tr/profil/:id/duzenle

{
  "en": {
    "ROOT.ABOUT": "about",
    "ROOT.CONTACT": "contact",
    "ROOT.PROFILE": "profile",
    "ROOT.PROFILE.EDIT": "edit",
    "SITE_MAP": "site-map"
  },
  "tr": {
    "ROOT.ABOUT": "iletisim",
    "ROOT.CONTACT": "hakkimizda",
    "ROOT.PROFILE": "profil",
    "ROOT.PROFILE.EDIT": "duzenle",
    "SITE_MAP": "site-haritasi"
  }
}

:+1: Hooyah! It was quite a long story, but @ngx-i18n-router/core will now translate each path and redirectTo property of routes.

Change language (runtime)

Import I18NRouterService using the mapping '@ngx-i18n-router/core' and inject it in the constructor of change-language.component (considering the change-language.component changes the language in Angular application).

Then, invoke the changeLanguage method by supplying the 2-letter language code, which translates routes to the destination language.

...
import { I18NRouterService } from '@ngx-i18n-router/core';
...

@Component({
  ...
})
export class ChangeLanguageComponent implements OnInit {
  ...
  constructor(private readonly route: ActivatedRoute,
              private readonly i18nRouter: I18NRouterService,
              private readonly router: Router) { }
  ...
  ngOnInit(): void {
    this.route.params.subscribe(params => {
      var languageCode = params['languageCode'];

      if (languageCode)
        // change language
        this.i18nRouter.changeLanguage(languageCode);

      this.router.navigate(['/']);
    });
  }
  ...
}

Pipe

I18nRouterPipe is used to prefix and translate routerLink directive's content. Pipe can be appended ONLY to a single empty string in the routerLink's definition or to an entire array element:

<a [routerLink]="['' | i18nRouter]">Home</a>
<a [routerLink]="['about'] | i18nRouter">About</a>
<a [routerLink]="['about', 'us', 22] | i18nRouter">About us</a>
<a [routerLink]="['about', 'banana'] | i18nRouter">Banana Republic</a>
<a [routerLink]="['about', 'apple', 6, 'pear'] | i18nRouter">(apple || pear)</a>

Example for Turkish language and link to 'about':

['about'] | i18nRouter -> '/tr/hakkinda'

Workaround for '@ngtools/webpack'

@ngx-i18n-router/core does not work with angular-cli (yet), and giving the following error during AoT compilation:

ERROR in Cannot read property 'loadChildren' of undefined

@ngx-i18n-router/core injects routes with the ROUTES DI token using the useFactory property. However @ngtools/webpack forces routes to be static, and prevents code splitting (for lazy-loaded modules) by third parties.

This issue is caused by the ngtools_impl located in the package @angular/compiler-cli.

You can track the actual status of this issue at the following URLs:

On the other hand, the ng-router-loader (together with awesome-typescipt-loader) is safe to go with - it compiles without a problem. There's an overhead: you need to manually configure build tools (dev/prod sever, task runners, webpack, etc).

If you really need to stick to angular-cli, you can use the following workaround, by changing the contents of /node_modules/@angular/compiler-cli/src/ngtools_impl.js as described below:

  • Method name: _collectRoutes
  • Line number: 139
  • Replacement: comment the line containing return routeList.concat(p.useValue);, and replace with:
    if (p.useFactory != null) {
    return routeList.concat(p.useFactory);
    } else {
    return routeList.concat(p.useValue);
    }
    

ngtools_impl.js

function _collectRoutes(providers, reflector, ROUTES) {
  return providers.reduce(function (routeList, p) {
    if (p.provide === ROUTES) {
      // return routeList.concat(p.useValue);
      if (p.useFactory != null) {
        return routeList.concat(p.useFactory);
      } else {
        return routeList.concat(p.useValue);
      }
    }
    else if (Array.isArray(p)) {
      return routeList.concat(_collectRoutes(p, reflector, ROUTES));
    }
    else {
      return routeList;
    }
  }, []);
}

Credits

License

The MIT License (MIT)

Copyright (c) 2018 Burak Tasci

If you find any bugs or have a feature request, please open an issue on github!

The npm package download data comes from npm's download counts api and package details come from npms.io.