How to approach Angular internationalization (i18n) in 2022

Internationalization is a common requirement in Angular applications. Despite this, there is no straightforward way to go when approaching this and you will soon find out that there are multiple decisions to be made and multiple options to choose from. This comes down to selecting a library, the way to perform translations, choosing the translation file format, deciding on who will perform the translations, and more. Therefore, this blog post will show you how to approach internationalization in Angular using a real-world project as an example. The blog post also features an accompanying GitHub Repository with a full code example on how to implement runtime translations with the native Angular i18n library.

Contents

Learnings

In this blog post, you will learn

  • what the term internationalization means
  • the differences between compile time and runtime difference
  • what popular options are there to integrate i18n in Angular
  • how to do runtime translations with the native Angular i18n library

In a recent project we had the requirement to implement internationalization support in an enterprise-grade angular application. Thus, we decided to evaluate the available internationalization options in angular regarding the specific requirements of this project. The requirements were to stick to long-term supported libraries that support runtime translations and use a simple JSON-based file format. Additionally, we wanted to be able to load translation files from an external source.

What is internationalization (i18n)?

Before we continue with the outcome of our research about suitable angular internationalization options, we must first define what exactly internationalization means. Often abbreviated with i18n, internationalization describes the process of preparing an application to support multiple locales. Localization, on the other hand, describes the actual translation of the text and the formatting of dates, numbers, or currencies for a specific locale. This can also mean building a specific version of the app for a specific locale. In other words, internationalization provides the infrastructure required to do localization. See this link for more information on the topic.

Differences between compile time and runtime translations

There are two approaches to internationalization. Namely, these are runtime translations and compile time translations. Both with their own advantages and disadvantages.

Compile time translations

Doing translations at compile time has the main benefit of avoiding a performance impact. Because all the strings that must be translated are replaced ahead of time there is no performance impact when running the application. On the other hand, this also requires generating a separate version of the application for every supported locale. Until recent improvements with the introduction of the Ivy compiler to the build process of Angular as the default option, this also meant having exceptionally long build times when supporting lots of locales. However, when using the Ivy compiler this is no longer an issue.

The main downside of compile time translations is the requirement to serve different versions of the application. Recall, that we must serve a separate version of the application for every locale. Thus, we have a more complicated web server configuration or routing than when serving only a single version. Additionally, this issue gets worse with the number of locales we want to support.

Another point that is mentioned from time to time is that compile time translations require the user to reload the page when switching locales. However, this shouldn’t be an issue because users won’t switch locales all the time. Usually, they will select their locale once and be done with it.

Overall, the decision of using compile time translations comes down to the trade-off between better performance and more configuration overhead.

Runtime translations

The main benefit of doing translations at runtime is the ability to load translations from an external source instead of baking them into the application at compile time. However, this approach is less performant and prevents the use of the Ivy Ahead-Of-Time (AOT) compiler, which impacts the performance of the application. Therefore, we must use the Just-In-Time (JIT) compiler to do translations at runtime.

Another benefit is that we can avoid the additional complexity and overhead of serving multiple versions of our application as it would be necessary when doing compile time translations. We only serve a single version of our application on a single route, no further configuration is required.

In conclusion, the decision of using runtime translations comes down to the same trade-off as compile time translations, just in reverse. Less performant but easier to set up and more flexible.

What options are there for i18n in Angular?

As we are now aware of the differences between compile time and runtime translations, the next step is to choose a package to support i18n in our application. In the past we would have used ngx-translate which satisfied our needs. However, with the requirement in mind that we want to choose a package with long-term support and the fact that ngx-translate is now in maintenance mode we decided to evaluate other options.

There are multiple popular options to choose from when it comes to implementing i18n. So, this list is not exhaustive and there are certainly other i18n libraries for Angular. But these are the packages we evaluated for the project which was mentioned in the introduction (ngx-translate included until we discovered the maintenance mode notice).

The first thing to note is that angular localize is now as popular as ngx-translate, while i18next stands at the top. Unfortunately, the angular integration of i18next is far less popular and not an official i18next library. Lastly, we have transloco which looks promising but is fairly new and not nearly as popular as angular localize.

Of course, this does not mean that the popularity of a library is the only relevant measure or that we discourage the use of any of these libraries. However, for us, this was the point we decided to give the official angular localize package a try. With our project’s requirements in mind, this seemed like the logical choice.

At this point, one might ask, why choose a third-party library with an uncertain support lifecycle at all when there is an official core library supported by the framework maintainers? Well, when doing research on angular localize and i18n one will quickly find reports on how difficult and inflexible working with angular localize is. However, as many of these reports are outdated and angular localize made significant advancements in the last few Angular versions, we wanted to evaluate the current state of the library and if it now supports a use case like the one mentioned in the intro.

Runtime translations with the native Angular i18n package

Now, after all the introductory information, let’s look at the interesting stuff. How do you actually do runtime translations with the native angular localize package? Unfortunately, this is not straightforward. When searching for a solution you will find lots of outdated articles and advice that this is not possible and that you should use ngx-translate for runtime translations or when you want to load translations via an API.

However, at one point we stumbled over a GitHub Issue in which one of the Angular developers mentions that runtime translations are possible. With one caveat, you cannot do runtime translations without reloading the app, because that is not supported by the Angular architecture. Interestingly, there is also mentioned that the documentation on runtime translations needs more work and examples describing how runtime translations work.

If you read through the angular documentation on internationalization, this is still true. There is a detailed explanation on how to build the application and perform the actual translation and how to deploy it. But the only thing explained there is the process for compile time translation. So, if you didn’t read the GitHub Issue above, you might think, looks like there is no support for runtime translations when they aren’t explained in the official guide.

But if you are thorough and keep reading, you will find the section on how to set the runtime locale manually. At last, there are runtime translations. And if you keep reading there is even a section on importing global variants of the locale data, which is used for Angular built-in pipes for currency or dates and so on. Can’t be so hard then, right? Unfortunately, there are still some missing pieces, which we will have a closer look at in the following sections.

Building blocks

Let’s first get an understanding of the required building blocks for internationalization with the Angular localize library.

To support multiple locales, each locale gets its own translation file. For example, de.json and en.json, to support English and German locales. Depending on the selected locale the translations from one of the translations are used to replace localize strings contained in the application code or templates. Angular localize supports multiple translation file formats, such as:

  • Application Resource Bundle (.arb)
  • JavaScript Object Notation (.json)
  • XML Localization Interchange File Format 1.2 and 2.0 (.xlf)
  • XML Message Bundle (.xmb)

Originally, Angular localize only supported XLIFF (.xlf) for translation files, which is one of the reasons for the notion of Angular localize to be complicated and hard to use. While XLIFF is a standardized format for translations and is widely supported, it is also aimed at enterprise applications (e.g. Google) and is meant to be used with specialized software used by professional translators. Luckily, with the support of the simpler-to-use JSON file format, working with translation files becomes a lot easier for us developers. Furthermore, it is even possible to load them via an API, so this requirement from our project was also satisfied.

The next piece are $localize strings, which were introduced with the new Angular Ivy compiler. Before they were introduced, Angular localize did only support translation in templates, but not in the code. Of course, this was a severe limitation and is yet another reason why Angular localize has not the best reputation. However, this is no longer true but is still mentioned even in newer blog posts or on Stack Overflow.

So how do $localize strings work? There are two variants of them

// Used inside of templates
<div class="route-label" i18n="@@routes.label.start-page">Start page</div>

// Used inside of code
const routeLabel = $localize`:@@routes.label.start-page:Start page`

They both follow the same format. A translation ID followed by a default translation, which is used when there is no translation in the translation file. Under the hood both variants use the same representation, which looks like the variant used in the code:

$localize`:@@translation-id:default translation`

In a translation file, you then would reference the translation id and provide a translation for it. The following is an example of a JSON translation file, which would be used to replace the $localize string with the corresponding translation.

// en.json
{ "routes.label.start-page": "Start page" }
// de.json
{ "routes.label.start-page": "Startseite" }

Tutorial

As already explained, the official Angular i18n documentation on runtime translations is poor. Thus, implementing runtime translations is confusing and includes a lot of trial and error to get it right. Therefore, this tutorial attempts to fill the gap, by providing step-by-step instruction on how to implement it in the right way. This includes the following steps:

  1. Enable i18n support and support for $localize strings
  2. Prepare templates for translation
  3. Prepare strings in code for translation
  4. Load the desired locale at the application start-up
  5. Load locale data for built-in Angular pipes (date, currency, etc)
  6. Load the translation file for the selected locale
  7. Persist the selected locale to improve the user experience
Step 1

The first step is straightforward. To enable i18n support, you’ll have to add the angular localize package with the following command:

ng add @angular/localize

The command will add the @angular/localize dependency to your project and update the polyfills.ts file in the root of your application with the following line:

import '@angular/localize/init';

This enables the support of $localize strings throughout your code. Without this line, your IDE will likely complain about every $localize string it encounters, and your project will not build. That’s it. Now your application is enabled to implement i18n and use $localize strings.

Step 2 and 3

The second and third steps are the same for compile time and runtime translations. Thus, they are well explained in the official documentation. So, head over there to read up on how to prepare your components for translation. Summarized, you’ll have to annotate all elements in templates that should be translated with i18n tags and replace all hardcoded strings in the code with $localize strings.

Step 4

The interesting part starts with step 4 when you must load the desired locale at the application start. While this sounds easy at the beginning, problems arise when you’ll try to figure out where to place the code to load the locale. If you place it for example inside the central app.component.ts it won’t work. If you place it in the global file polyfills.ts it will work, but you cannot use async/await syntax and your i18n code lives outside of the application. That’s not nice either. So where to put it then?

Luckily, Angular has the concept of app initializers which are executed during app initialization. To make runtime translations work you must implement steps 5 and 6 inside the app initializer. Step 5 is to load the required locale data for Angular’s built-in pipes, while step 6 is to load your custom translation file containing the translations for your components.

Step 5

To implement step 5, the following code leverages dynamic imports of ES modules and webpack magic strings to only include the required locale data. This is important if you only plan to support a few locales as we did in this example. Otherwise, the compiled output would be bloated with hundreds of chunks with locale data that is never loaded. Additionally, you will have to manually set the runtime locale as described in the Angular guide.

// Use web pack magic string to only include required locale data
const localeModule = await import(
  /* webpackInclude: /(de|en|es)\.mjs$/ */
  `/node_modules/@angular/common/locales/${this.locale}.mjs`
);
// Set locale for built in pipes, etc.
registerLocaleData(localeModule.default);
Step 6

The code to implement step 6 is using dynamic imports to load the translation file for the language the user has selected. This is again dynamic, so you don’t have to load translation files that are not needed for the currently active language.

// Load translation file
const localeTranslationsModule = await import(
  `src/assets/i18n/${this.locale}.json`
);
// Load translations for the current locale at run-time
loadTranslations(localeTranslationsModule.default);
Step 7

And that’s it, we have implemented runtime translations with the native Angular localize library. See our GitHub repository for a full example.

Now step 7 is to implement a way to retrieve the currently selected locale. Remember that we must reload the application for localization to be applied. So, we can’t simply store the locale in some variable. However, this is easy and there are multiple ways to solve this. For example, storing the locale in the local storage and loading it at app start-up or retrieving the locale from the URL, if you’d like to append the selected locale part of the URL.

Caveats

The only caveat we encountered was that $localize strings do not work in files loaded too early in the application lifecycle. In this case, the central app-routing module, in which we had used a central configuration holding the routes and labels to be displayed in the navbar. However, this can be solved by putting labels in a separate file or by using functions to return labels, so the code execution is deferred to the lifecycle when components and translation files are loaded.

Outcome

Runtime translations with the native Angular localize library works great once you figured out how to implement them. The only real downside is that you can no longer use the AOT compiler, but that is a trade-off implied by the concept of runtime translations. However, the application start-up is still fast and due to the architecture translations only happen once. So, there is no further performance impact after the first application start, which is great.

The important thing about the Angular localize library is, that it is officially maintained and that you are still flexible to switch to compile time translations at a later point. A switch would only require removing the specific code for runtime translations and adding additional configuration to build and serve multiple versions of the application. But besides that, using compile time translation requires no further changes to the components and translation files.

Conclusion

Although it doesn’t look like it’s possible at the first glance, runtime translations can be done with the native Angular localize library. So, there is no need to use an external package if you don’t have unique requirements. The main hindrance to the adoption of runtime translations with Angular localize is the extremely limited documentation. But when you finally have puzzled it together or have read this blog post, which has done that for you, it works great and without any hassle.

See the accompanying GitHub Repository for a full code example.