Angular Localization - Unleash the full power of i18next

.jpg)
Let's talk about internationalization (i18n) for Angular (not AngularJS, not Angular 2, just Angular 😉).
When it comes to JavaScript localization, one of the most popular frameworks is i18next. One of the most famous Angular extension for i18next is angular-i18next. It was created back in April 2017 by Sergey Romanchuk.
TOC
So first of all: "Why i18next?"
i18next was created in late 2011. It's older than most of the libraries you will use nowadays, including your main frontend technology (React, Angular, Vue, ...).
➡️ sustainable
Based on how long i18next already is available open source, there is no real i18n case that could not be solved with i18next.
➡️ mature
i18next can be used in any javascript (and a few non-javascript - .net, elm, iOS, android, ruby, ...) environment, with any UI framework, with any i18n format, ... the possibilities are endless.➡️ extensible
There is a plenty of features and possibilities you'll get with i18next compared to other regular i18n frameworks.
➡️ rich
Here you can find more information about why i18next is special and how it works.
Let's get into it...
Prerequisites
Make sure you have Node.js and npm installed. It's best, if you have some experience with simple HTML, JavaScript and basic Angular, before jumping to angular-i18next.
Getting started
Take your own Angular project or create a new one, i.e. with the Angular cli.
npx @angular/cli new my-app
To simplify let's remove the "generated" content of the angular-cli:

We are going to adapt the app to detect the language according to the user’s preference. And we will create a language switcher to make the content change between different languages.
Let's install some i18next dependencies:
npm install i18next angular-i18next i18next-browser-languagedetector
Let's modify our app.module.ts
to integrate and initialize the i18next config:
1import { APP_INITIALIZER, NgModule, LOCALE_ID } from '@angular/core';
2import { BrowserModule } from '@angular/platform-browser';
3import { I18NEXT_SERVICE, I18NextModule, I18NextLoadResult, ITranslationService, defaultInterpolationFormat } from 'angular-i18next';
4import LanguageDetector from 'i18next-browser-languagedetector';
5
6import { AppComponent } from './app.component';
7
8const i18nextOptions = {
9 debug: true,
10 fallbackLng: 'en',
11 resources: {
12 en: {
13 translation: {
14 "welcome": "Welcome to Your Angular App"
15 }
16 },
17 de: {
18 translation: {
19 "welcome": "Willkommen zu Deiner Vue.js App"
20 }
21 }
22 },
23 interpolation: {
24 format: I18NextModule.interpolationFormat(defaultInterpolationFormat)
25 }
26};
27
28export function appInit(i18next: ITranslationService) {
29 return () => {
30 let promise: Promise<I18NextLoadResult> = i18next
31 .use(LocizeApi)
32 .use<any>(LanguageDetector)
33 .init(i18nextOptions);
34 return promise;
35 };
36}
37
38export function localeIdFactory(i18next: ITranslationService) {
39 return i18next.language;
40}
41
42export const I18N_PROVIDERS = [
43 {
44 provide: APP_INITIALIZER,
45 useFactory: appInit,
46 deps: [I18NEXT_SERVICE],
47 multi: true
48 },
49 {
50 provide: LOCALE_ID,
51 deps: [I18NEXT_SERVICE],
52 useFactory: localeIdFactory
53 },
54];
55
56@NgModule({
57 declarations: [
58 AppComponent
59 ],
60 imports: [
61 BrowserModule,
62 I18NextModule.forRoot()
63 ],
64 providers: [
65 I18N_PROVIDERS
66 ],
67 bootstrap: [AppComponent]
68})
69export class AppModule { }
Ok, now let's update the app.component.html
:
1<!-- Toolbar -->
2<div class="toolbar" role="banner">
3 <span>{{ 'welcome' | i18next }}</span>
4</div>
5
6<div class="content" role="main">
7
8 <!-- Highlight Card -->
9 <div class="card highlight-card card-small">
10 <span>{{ 'welcome' | i18next }}</span>
11 </div>
12</div>
You should now see something like this:

Nice! So let's add an additional text, with an interpolated unescaped value:
1<!-- Toolbar -->
2<div class="toolbar" role="banner">
3 <span>{{ 'welcome' | i18next }}</span>
4</div>
5
6<div class="content" role="main">
7
8 <!-- Highlight Card -->
9 <div class="card highlight-card card-small">
10 <span>{{ 'welcome' | i18next }}</span>
11 </div>
12
13 <br />
14 <p>{{ 'descr' | i18next: { url: 'https://github.com/Romanchuk/angular-i18next' } }}</p>
15</div>
Do not forget to add the new key also to the resources:
1const i18nextOptions = {
2 debug: true,
3 fallbackLng: 'en',
4 resources: {
5 en: {
6 translation: {
7 "welcome": "Welcome to Your Angular App",
8 "descr": "For a guide and recipes on how to configure / customize this project, check out {{-url}}."
9 }
10 },
11 de: {
12 translation: {
13 "welcome": "Willkommen zu Deiner Vue.js App",
14 "descr": "Eine Anleitung und Rezepte für das Konfigurieren / Anpassen dieses Projekts findest du in {{-url}}."
15 }
16 }
17 },
18 interpolation: {
19 format: I18NextModule.interpolationFormat(defaultInterpolationFormat)
20 }
21};
Does it work? - Of course!

And thanks to the language-detector, you can also try to switch the language with the query parameter ?lng=de
:

Language Switcher
We like to offer the possibility to change the language via some sort of language switcher.
So let's add a footer section in our app.component.html
file:
1<!-- Footer -->
2<footer>
3 <ng-template ngFor let-lang [ngForOf]="languages" let-i="index">
4 <span *ngIf="i !== 0"> | </span>
5 <a *ngIf="language !== lang" href="javascript:void(0)" class="link lang-item {{lang}}" (click)="changeLanguage(lang)">{{ lang.toUpperCase() }}</a>
6 <span *ngIf="language === lang" class="current lang-item {{lang}}">{{ lang.toUpperCase() }}</span>
7 </ng-template>
8</footer>
And we need also to update the app.components.ts
file:
1import { Component, Inject } from '@angular/core';
2import { I18NEXT_SERVICE, ITranslationService } from 'angular-i18next';
3
4@Component({
5 selector: 'app-root',
6 templateUrl: './app.component.html',
7 styleUrls: ['./app.component.less']
8})
9export class AppComponent {
10 language: string = 'en';
11 languages: string[] = ['en', 'de'];
12
13 constructor(
14 @Inject(I18NEXT_SERVICE) private i18NextService: ITranslationService
15 )
16 {}
17
18 ngOnInit() {
19 this.i18NextService.events.initialized.subscribe((e) => {
20 if (e) {
21 this.updateState(this.i18NextService.language);
22 }
23 });
24 }
25
26 changeLanguage(lang: string){
27 if (lang !== this.i18NextService.language) {
28 this.i18NextService.changeLanguage(lang).then(x => {
29 this.updateState(lang);
30 document.location.reload();
31 });
32 }
33 }
34
35 private updateState(lang: string) {
36 this.language = lang;
37 }
38}

🥳 Awesome, you've just created your first language switcher!
Thanks to i18next-browser-languagedetector now it tries to detect the browser language and automatically use that language if you've provided the translations for it. The manually selected language in the language switcher is persisted in the localStorage, next time you visit the page, that language is used as preferred language.
Separate translations from code
Having the translations in our code works, but is not that suitable to work with, for translators. Let's separate the translations from the code and pleace them in dedicated json files.
i18next-locize-backend will help us to do so.
What is loize?
How does this look like?
First you need to signup at locize and login. Then create a new project in locize and add your translations. You can add your translations either by importing the individual json files or via API or by using the CLI.
npm install i18next-locize-backend
Adapt the app.modules.ts
file to use the i18next-locize-backend and make sure you copy the project-id from within your locize project:
1import { APP_INITIALIZER, NgModule, LOCALE_ID } from '@angular/core';
2import { BrowserModule } from '@angular/platform-browser';
3import { I18NEXT_SERVICE, I18NextModule, I18NextLoadResult, ITranslationService, defaultInterpolationFormat } from 'angular-i18next';
4import LanguageDetector from 'i18next-browser-languagedetector';
5import LocizeApi from 'i18next-locize-backend';
6
7import { AppComponent } from './app.component';
8
9const i18nextOptions = {
10 debug: true,
11 fallbackLng: 'en',
12 backend: {
13 projectId: 'your-locize-project-id'
14 },
15 interpolation: {
16 format: I18NextModule.interpolationFormat(defaultInterpolationFormat)
17 }
18};
19
20export function appInit(i18next: ITranslationService) {
21 return () => {
22 let promise: Promise<I18NextLoadResult> = i18next
23 .use(LocizeApi)
24 .use<any>(LanguageDetector)
25 .init(i18nextOptions);
26 return promise;
27 };
28}
29
30export function localeIdFactory(i18next: ITranslationService) {
31 return i18next.language;
32}
33
34export const I18N_PROVIDERS = [
35 {
36 provide: APP_INITIALIZER,
37 useFactory: appInit,
38 deps: [I18NEXT_SERVICE],
39 multi: true
40 },
41 {
42 provide: LOCALE_ID,
43 deps: [I18NEXT_SERVICE],
44 useFactory: localeIdFactory
45 },
46];
47
48@NgModule({
49 declarations: [
50 AppComponent
51 ],
52 imports: [
53 BrowserModule,
54 I18NextModule.forRoot()
55 ],
56 providers: [
57 I18N_PROVIDERS
58 ],
59 bootstrap: [AppComponent]
60})
61export class AppModule { }
The app looks still the same, but the translations are now completely separated from the app and can be managed and released separately.

save missing translations
Thanks to the use of the saveMissing functionality, new keys gets added to locize automatically, while developing the app.
Just pass saveMissing: true
in the i18next options and make sure you copy the api-key from within your locize project:
1const i18nextOptions = {
2 debug: true,
3 saveMissing: true, // do not use the saveMissing functionality in production: https://docs.locize.com/guides-tips-and-tricks/going-production
4 fallbackLng: 'en',
5 backend: {
6 projectId: 'my-locize-project-id',
7 apiKey: 'my-api-key' // used for handleMissing functionality, do not add your api-key in a production build
8 },
9 interpolation: {
10 format: I18NextModule.interpolationFormat(defaultInterpolationFormat)
11 }
12};
Each time you'll use a new key, it will be sent to locize, i.e.:
<p>{{ 'cool' | i18next: { defaultValue: 'This is very cool!' } }}</p>
will result in locize like this:

👀 but there's more...
Thanks to the locize-lastused plugin, you'll be able to find and filter in locize which keys are used or not used anymore.
With the help of the locize plugin, you'll be able to use your app within the locize InContext Editor.
Lastly, with the help of the auto-machinetranslation workflow, new keys not only gets added to locize automatically, while developing the app, but are also automatically translated into the target languages using machine translation:

npm install locize-lastused locize
use them in app.modules.ts
:
1import { APP_INITIALIZER, NgModule, LOCALE_ID } from '@angular/core';
2import { BrowserModule } from '@angular/platform-browser';
3import { I18NEXT_SERVICE, I18NextModule, I18NextLoadResult, ITranslationService, defaultInterpolationFormat } from 'angular-i18next';
4import LanguageDetector from 'i18next-browser-languagedetector';
5import LocizeApi from 'i18next-locize-backend';
6import LastUsed from 'locize-lastused';
7import { locizePlugin } from 'locize';
8
9import { AppComponent } from './app.component';
10
11const locizeOptions = {
12 projectId: 'my-locize-project-id',
13 apiKey: 'my-api-key' // used for handleMissing functionality, do not add your api-key in a production buildyour
14};
15
16const i18nextOptions = {
17 debug: true,
18 fallbackLng: 'en',
19 saveMissing: true, // do not use the saveMissing functionality in production: https://docs.locize.com/guides-tips-and-tricks/going-production
20 backend: locizeOptions,
21 locizeLastUsed: locizeOptions,
22 interpolation: {
23 format: I18NextModule.interpolationFormat(defaultInterpolationFormat)
24 }
25};
26
27export function appInit(i18next: ITranslationService) {
28 return () => {
29 let promise: Promise<I18NextLoadResult> = i18next
30 // locize-lastused
31 // sets a timestamp of last access on every translation segment on locize
32 // -> safely remove the ones not being touched for weeks/months
33 // https://github.com/locize/locize-lastused
34 // do not use the lastused functionality in production: https://docs.locize.com/guides-tips-and-tricks/going-production
35 .use(LastUsed)
36 // locize-editor
37 // InContext Editor of locize
38 .use(locizePlugin)
39 // i18next-locize-backend
40 // loads translations from your project, saves new keys to it (saveMissing: true)
41 // https://github.com/locize/i18next-locize-backend
42 .use(LocizeApi)
43 .use<any>(LanguageDetector)
44 .init(i18nextOptions);
45 return promise;
46 };
47}
48
49export function localeIdFactory(i18next: ITranslationService) {
50 return i18next.language;
51}
52
53export const I18N_PROVIDERS = [
54 {
55 provide: APP_INITIALIZER,
56 useFactory: appInit,
57 deps: [I18NEXT_SERVICE],
58 multi: true
59 },
60 {
61 provide: LOCALE_ID,
62 deps: [I18NEXT_SERVICE],
63 useFactory: localeIdFactory
64 },
65];
66
67@NgModule({
68 declarations: [
69 AppComponent
70 ],
71 imports: [
72 BrowserModule,
73 I18NextModule.forRoot()
74 ],
75 providers: [
76 I18N_PROVIDERS
77 ],
78 bootstrap: [AppComponent]
79})
80export class AppModule { }
Automatic machine translation:

Last used translations filter:




overwrite version
🧑💻 The complete code can be found here.
There's also an i18next crash course video.
🎉🥳 Congratulations 🎊🎁
I hope you’ve learned a few new things about i18next, angular-i18next and the modern translation software workflows.
So if you want to take your i18n topic to the next level, it's worth to try locize.
The founders of locize are also the creators of i18next. So by using locize you directly support the future of i18next.