Angular i18n and the localizing of applications had an overhaul with version 9, enabled by the new rendering engine Ivy. In this article, we take a closer look at how this built-in package of Angular now works, while pointing out the benefits and drawbacks we find.
We then set up an application with Angular internationalization and go through the complete process from marking texts for translation, extracting them to translation files, and how we manage these files to get the application deployed and maintained while keeping users all over the world happy with our translations.
Illustration by Vero Karén
Internationalization and localization
It’s easy to get confused with the terms internationalization (i18n) and localization (i10n), and where to draw the line between them. Internationalization is the process of designing your application so that it can be adapted to different locales around the world while localization is the process of building the versions of the applications to different locales.
Together they help us in adapting software to different languages and local variations in the look and feel expected by the target audience.
How localization works with Ivy
The new localization process of Angular Ivy is based on the concept of tagged templates. Tags allow you to parse template literals with a function. The tag used here is the global identifier $localize
. Instead of translating the strings, the Ivy template compiler converts all template text marked with i18n
attributes to $localize
tagged strings.
So when we add:
<h1 i18n>Hello World!</h1>
It will be compiled to $localize
calls and somewhere in the compiled code we will be able to find:
$localize`Hello World!`
The way the tagged template works is that you put the function that you want to run against the string before the template. Instead of function()
, you have function
or as in this case
$localize
.
When this step is done we have two choices:
compile-time inlining: the
$localize
tag is transformed at compile time by a transpiler, removing the tag and replacing the template literal string with the translation.run-time evaluation: the
$localize
tag is a run-time function that replaces the template literal string with translations loaded at run-time.
In this article, we use compile-time inlining to achieve our goals. At the very end of the build process, we run a step for the translation files by providing an option flag to get a localized application for the languages. Since we are doing the translations compile-time we get one application per locale.
At the end of the article, we take a further look into run-time evaluation.
Because the application does not need to be built again for each locale, the build process is much faster than before v9 of Angular.
You can read more about this in Angular localization with Ivy from where this picture is.
Now that we understand the process of building the application we start to get an understanding of what it entails.
The good and the bad
The standard Angular internationalization and localization are designed to produce one compiled application per language. By doing this we get optimal performance since there is no overhead of loading translation files and compiling them at run-time. But, this also means that each language has to be deployed to a separate URL:
www.mydomain.com/en
www.mydomain.com/nb
www.mydomain.com/fi
This means we need to do a bit more set up on our webserver. A limitation with ng serve
is that it only works with one language at a time and to run different languages also needs some configuration. To run all languages locally we need to use a local webserver. We look into how we do all this in this article.
Angular i18n uses XLIFF and XMB formats that are XML-based, more verbose formats than JSON. But since these files are used at compile-time it doesn’t matter. It makes sense to use JSON when we load the translation files at run-time to keep the file sizes smaller. The formats chosen for the built-in i18n are used by translation software which helps us with our translations as we will see.
The number one drawback that people find with this solution is that you need to reload the application when you switch languages. But, is this really going to be a problem for you? People usually switch languages once if ever. And that couple of seconds it takes to reload applications will not be a problem.
Having one bundle per language is not a problem for a web SPA other than that you have to configure your web server for this. But for standalone apps, this means you got to make the user download every translated bundle, or distribute a different app for every version.
It’s important to understand your requirements before deciding which route to take.
Transloco
If the standard Angular i18n doesn’t give you what you want then the best alternative today in my opinion is Transloco. It’s being actively maintained and has an active community. It will get you up and running faster and is more flexible than the built-in solution. Since Transloco is runtime translation you have just www.mydoman.com
and can change localization on the fly.
So, before choosing which way to go in such a fundamental choice you should check Transloco out to see if it would be a better fit for you.
OK, enough technicalities let’s see some code!
Add localize to Angular project
@angular/localize
package was released with Angular 9 and supports i18n in Ivy applications. This package requires a global $localize
symbol to exist. The symbol is loaded by importing the @angular/localize/init
module.
To add the localization features provided by Angular, we need to add the @angular/localize
package to our project:
ng add @angular/localize
This command:
Updates
package.json
and installs the package.Updates
polyfills.ts
to import the@angular/localize
package.
If you try using i18n without adding this package you get a self-explanatory error message reminding us to run ng add @angular/localize
.
Translating templates
To translate templates in our application, we need first to prepare the texts by marking them with the i18n
attribute.
i18n is a custom attribute from the WebExtensions API. It’s recognized by Angular tools and compilers. During the compilation, it is removed, and the tag content is replaced with the translations.
We mark the text like this:
<span i18n>Welcome</span>
This <span>
tag is now marked and ready for the next step in the translation process.
Translating TypeScript files
NB! You need Angular 10.1 or later to extract strings from source code (.ts) files.
It’s not only our templates that need to be translated. Sometimes we have code in our TypeScript files that also need a translation. To localize a string in the source code, we use the $localize
template literal:
title = $localize`My page`;
Note that template literals use the backtick character instead of double or single quotes.
Extracting texts
When our application is prepared to be translated, we can use the extract-i18n command to extract the marked texts into a source language file named messages.xlf
.
The command options we can use are:
--output-path
: Change the location of the source language file.--outFile
: Change the file name.--format
: Change file format. Possible formats are XLIFF 1.2 (default), XLIFF 2, and XML Message Bundle (XMB).
Running this command from the root directory of the project:
ng extract-i18n
We get the messages.xlf
file looking like this:
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en-US" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="3492007542396725315" datatype="html">
<source>Welcome</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">7</context>
</context-group>
</trans-unit>
<trans-unit id="5513198529962479337" datatype="html">
<source>My page</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">9</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>
We can see that we have the texts “Welcome” and “My page” in the file but what does it all mean?
trans-unit
is the tag containing a single translation.id
is a translation identifier thatextract-i18n
generates so don’t modify it!source
contains translation source text.context-group
specifies where the given translation can be found.context-type="sourcefile"
shows the file where translation is from.context-type="linenumber"
tells the line of code of the translation.
Now that we have extracted the source file, how do we get files with the languages we want to translate?
Create translation files
After we have generated the messages.xlf
file, we can add new languages by copying it and naming the new file accordingly with the associated locale.
To store Norwegian translations we rename the copied file to messages.nb.xlf
. Then we send this file to the translator so that he can do the translations with an XLIFF editor. But, let’s not get ahead of us and first do a manual translation to get a better understanding of the translation files.
Translating files manually
Open the file and find the <trans-unit>
element, representing the translation of the <h1>
greeting tag that was previously marked with the i18n
attribute. Duplicate the <source>...</source>
element in the text node, rename it to target
, and then replace its content with the Norwegian text:
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en-US" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="3492007542396725315" datatype="html">
<source>Welcome</source>
<target>Velkommen</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">7</context>
</context-group>
</trans-unit>
<trans-unit id="5513198529962479337" datatype="html">
<source>my page</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">9</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>
This is all that there is to it to add the translations to the files. Let’s see how we do it with an editor.
Translating files with an editor
Before we can use an editor, we need to provide the translation language. We can do this by adding the target-language
attribute for the file tag so that translation software can detect the locale:
<file source-language="en-US" datatype="plaintext" original="ng2.template" target-language="nb">
Let’s open this file in a translation tool to see what we are working with. I’m using the free version of PoEdit in this article:
This looks much easier to work with than the manual way. We even get some suggestions for translations. Let’s translate “my page” and save the file. If we then open messages.nb.xlf
we can see that it has added the translation in a target block like when we did it manually:
<source>My page</source>
<target state="translated">Min side</target>
We see that it added state="translated"
to the target tag. This is an optional attribute that can have the values translated
, needs-translation
, or final
. This helps us when using the editor to find the texts that are not yet translated.
This is a great start but before we try out the translations in our application, let’s see what more we can do by adding more information into the box in the screenshot named “Notes for translators”.
Notes for translators
Sometimes the translator needs more information about what they are translating. We can add a description of the translation as the value of the i18n attribute:
<span i18n="Welcome message">Welcome</span>
We can add even more context to the translator by adding the meaning of the text message. We can add the meaning together with the description and separate them with the |
character: <meaning>|<description>
. In this example we might want to let the translator know that this welcome message is located in the toolbar:
<span i18n="toolbar header|Welcome message">Welcome</span>
The last part that we can add to the value of the i18n
attribute is an ID by using @@
. Be sure to define unique custom ids. If you use the same id for two different text messages, only the first one is extracted, and its translation is used in place of both original text messages.
Here we add the ID toolbarHeader
:
<span i18n="toolbar header|Welcome message@@toolbarHeader">Welcome</span>
If we don’t add an ID for the translation, Angular will generate a random ID as we saw earlier. Running ng extract-i18n
again we can see that the helpful information has been added to our translation unit:
<trans-unit id="toolbarHeader" datatype="html">
<source>Welcome</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">7</context>
</context-group>
<note priority="1" from="description">Welcome message</note>
<note priority="1" from="meaning">toolbar header</note>
</trans-unit>
- There are now a couple of
note
tags that provide the translationdescription
andmeaning
and theid
is no longer a random number.
If we copy these to the messages.ng.xlf
file and open it in PoEdit we see that all these are now visible in “Notes for translators”:
Providing context in TypeScript files
Like with Angular templates you can provide more context to translators by providing meaning
, description
, and id
in TypeScript files. The format is the same as used for i18n
markers in the templates. Here are the different options as found in the Angular Docs:
$localize`:meaning|description@@id:source message text`;
$localize`:meaning|:source message text`;
$localize`:description:source message text`;
$localize`:@@id:source message text`;
Adding an id
and description
to our title could look like this:
title = $localize`:Header on first page@@firstPageTitle:My page`;
If the template literal string contains expressions, you can provide the placeholder name wrapped in :
characters directly after the expression:
$localize`Hello ${person.name}:name:`;
Specialized use cases
There are some specialized use cases for translations that we need to look at. Attributes can easily be overlooked but are also important to translate, not least for accessibility.
Different languages have different pluralization rules and grammatical constructions that can make translation difficult. To simplify translation, we can use plural
to mark the uses of plural numbers and select
to mark alternate text choices.
Attributes
Apart from the usual suspects of HTML tags, we need to also be aware that we need to translate HTML attributes. This is especially important when we are making our applications accessible to all people.
Let’s take the example of an img
tag. People using a screen reader would not see the picture but instead, the alt
attribute would be read to them. For this reason and others, provide a useful value for alt
whenever possible.
<img [src]="logo" alt="Welcome logo" />
To mark an attribute for translation, add i18n-
followed by the attribute that is being translated. To mark the alt
attribute on the img
tag we add i18n-alt
:
<img [src]="logo" i18n-alt alt="Welcome logo" />
In this case, the text “Welcome logo” will be extracted for translation.
You can also assign a meaning, description, and custom ID with the
i18n-attribute="<meaning>|<description>@@<id>"
syntax.
Plurals
Pluralization rules between languages differ. We need to account for all potential cases. We use the plural
clause to mark expressions we want to translate depending on the number of subjects.
For example, imagine we do a search and want to show how many results were found. We want to show “nothing found” or the number of results appended with “items found”. And of course, let’s not forget about the case with only one result.
The following expression allows us to translate the different plurals:
<p i18n>
{itemCount, plural, =0 {nothing found} =1 {one item found} other {{{itemCount}} items found}}
</p>
itemCount
is a property with the number of items found.plural
identifies the translation type.The third parameter lists all the possible cases (0, 1, other) and the corresponding text to display. Unmatched cases are caught by
other
. Angular supports more categories listed here.
When we translate plural expression we have two trans units: One for the regular text placed before the plural and one for the plural versions.
Alternates
If your text depends on the value of a variable, you need to translate all alternatives. Much like plural
, we can use the select
clause to mark choices of alternate texts. It allows you to choose one of the translations based on a value:
<p i18n>Color: {color, select, red {red} blue {blue} green {green}}</p>
Based on the value of color
we display either “red”, “blue”, or “green”. Like when translating plural expressions we get two trans units:
<trans-unit id="7195591759695550088" datatype="html">
<source>Color: <x id="ICU" equiv-text="{color, select, red {red} blue {blue} green {green}}"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">12</context>
</context-group>
</trans-unit>
<trans-unit id="3928679011634560837" datatype="html">
<source>{VAR_SELECT, select, red {red} blue {blue} green {green}}</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">12</context>
</context-group>
</trans-unit>
The editors understand these units and help us with the translations:
Interpolation
Let’s combine a welcome message the title
property:
<h1 i18n>Welcome to {{ title }}</h1>
This places the value of the title
variable that we earlier translated in the text. When we extract this text we see how the interpolation is handled:
<source>Welcome to <x id="INTERPOLATION" equiv-text="{{ title }}"/></source>
For the translation the <x.../>
stays the same for the target language:
<target>Velkommen til <x id="INTERPOLATION" equiv-text="{{ title }}"/></target>
And that’s the last example of translations that we are looking at. Now, let’s see how we can get this applications up and running with our new language!
Configuring locales
To be able to run our application in many languages we need to define the locales in the build configuration. In the angular.json
file, we can define locales for a project under the i18n
option and locales
, that maps locale identifiers to translation files:
"projects": {
"i18n-app": {
"i18n": {
"sourceLocale": "en-US",
"locales": {
"nb": "messages.nb.xlf"
}
}
}
Here we added the configuration for the Norwegian language. We provide the path for the translation file for the locale "nb"
. In our case, the file is still in the root directory.
The sourceLocale
is the locale you use within the app source code. The default is en-US
so we could leave this line out or we could change it to another language. Whatever value we use here is also used to build an application together with the locales
we define.
To use your locale definition in the build configuration, use the "localize"
option in angular.json
to tell the CLI which locales to generate for the build configuration:
Set
"localize"
totrue
for all the locales previously defined in the build configuration.Set
"localize"
to an array of a subset of the previously-defined locale identifiers to build only those locale versions.
The development server only supports localizing a single locale at a time. Setting the "localize"
option to true
will cause an error when using ng serve
if more than one locale is defined. Setting the option to a specific locale, such as "localize": ["nb"]
, can work if you want to develop against a specific locale.
Since we want to be able to ng serve
our application with a single language, we create a custom locale-specific configuration by specifying a single locale in angular.json
as follows:
"build": {
"configurations": {
"nb": {
"localize": ["nb"]
}
}
},
"serve": {
"configurations": {
"nb": {
"browserTarget": "ng-i18n:build:nb"
}
}
}
With this change we can serve the Norwegian version of the app and make sure the translations are working by sending in nb
to the configuration
option:
ng serve --configuration=nb
We can also build the app with a specific locale:
ng build --configuration=production,nb
Or with all the locales at once:
ng build --prod --localize
In other words, it’s more flexible to configure it the way we did but we could also have just set localize
and aot
to true and be done with it.
Run multiple languages locally
For performance reasons, running ng serve
only supports one locale at a time. As we saw earlier we can serve the specific languages by sending in the locale to the configuration
option. But, how can we run the application with all the configured languages?
Multiple languages
To run all languages simultaneously we need first to build the project. We can build applications with the locales defined in the build configuration with the localize
option:
ng build --prod --localize
When the build is localized and ready we need to set up a local webserver to serve the applications. Remember we have one application per language, which is what makes this a bit more complex.
In Angular Docs, there are a couple of examples of server-side code that we can use.
Nginx
To get our application up and running we need to:
Install Nginx
Add config from Angular Docs to
conf/nginx.conf
Build our applications
Copy applications to the folder defined in
root
innginx.conf
.Open browser in
localhost
The port is set in listen
and is normally set to 80. You change languages by changing the URL. We should now see our Norwegian application at localhost/nb
.
Here is an example of the nginx.conf
file:
events{}
http {
types {
module;
}
include /etc/nginx/mime.types;
# Expires map for caching resources
map $sent_http_content_type $expires {
default off;
text/html epoch;
text/css max;
application/javascript max;
~image/ max;
}
# Browser preferred language detection
map $http_accept_language $accept_language {
~*^en en;
~*^nb nb;
}
server {
listen 80;
root /usr/share/nginx/html;
# Set cache expires from the map we defined.
expires $expires;
# Security. Don't send nginx version in Server header.
server_tokens off;
# Fallback to default language if no preference defined by browser
if ($accept_language ~ "^$") {
set $accept_language "nb";
}
# Redirect "/" to Angular app in browser's preferred language
rewrite ^/$ /$accept_language permanent;
# Everything under the Angular app is always redirected to Angular in the correct language
location ~ ^/(en|nb) {
try_files $uri /$1/index.html?$args;
# Add security headers from separate file
include /etc/nginx/security-headers.conf;
}
# Proxy for APIs.
location /api {
proxy_pass https://api.address.here;
}
}
}
If we use Nginx in production, it makes sense to also test our application locally with it.
Deploy to production
If you are using Nginx in production, then you already have the language configuration setup. If not, you need to find out what changes you need for your particular server configuration.
We have to take into consideration if we are running the application locally or in production. We can do this by using isDevMode
, which returns whether Angular is in development mode:
isDevMode() ? '/' : `/${locale}/`;
So, when we are running the application locally with ng serve
we don’t add the locale to the URL as we do when we have localized the application in the production build.
Maintaining the application
Usually, when the application has been deployed it’s time to end the article. This time I wanted to address a few more things before ending. Let’s start by looking into what challenges we run into when going into maintenance mode.
The biggest challenge is the handling of the translation files. We need to make sure that the marked texts find their way to the translators and back to the application before it’s deployed. To help with this we need to find a way to automate the generation of translation files and get notified when we have missing translations.
Generating the translation files
It’s not sustainable to keep merging the translation files manually. We need some automation! To implement this, I’m using a free tool called Xliffmerge.
Since this tool has old Angular versions as
peerDependencies
we need to use--legacy-peer-deps
if we are using a new version of NPM (v7) that would otherwise fail on installation.
The documentation for Xliffmerge is targeting older versions of Angular, but after some experimentation, I found it enough to install the @ngx-i18nsupport/tooling
package:
npm install -D @ngx-i18nsupport/tooling --legacy-peer-deps
Note that -D
installs to devDependencies
, and for use in a CI pipeline, you should omit it to use in dependencies
.
Then we can add new languages to the configurations in angular.json
under projects -> projectName -> architect -> xliffmerge
.
"xliffmerge": {
"builder": "@ngx-i18nsupport/tooling:xliffmerge",
"options": {
"xliffmergeOptions": {
"defaultLanguage": "en-US",
"languages": ["nb"]
}
}
}
After adding new translations, we can extract them and migrate them to our translation files by running this script:
ng extract-i18n && ng run projectName:xliffmerge
We get a couple of warnings running the script which tells us its working!
WARNING: merged 1 trans-units from master to "nb"
WARNING: please translate file "messages.nb.xlf" to target-language="nb"
After this, you can distribute the language files to the translators. And when the translations finish, the files need to be merged back into the project repository.
Just a word of caution that this library was not being actively maintained at the time of this writing, so you might want to look into other options. There is an Angular issue on merging translated files. Go and upvote it if you think this is something that we need!
Missing Translations
Another way to make sure the translations are valid is to get noticed if translations are missing. By default, the build succeeds but generates a warning of missing translations. We can configure the level of the warning generated by the Angular compiler:
error
: An error message is displayed, and the build process is aborted.warning
(default): Show a Missing translation warning in the console or shell.ignore
: Do nothing.
Specify the warning level in the options section for the build target of your Angular CLI configuration file, angular.json
. The following example shows how to set the warning level to error:
"options": {
"i18nMissingTranslation": "error"
}
If you run the application and no translation is found, the application displays the source-language text. We have to make a decision here on how important the translations are. If they are crucial then we should break the build to make sure we get all translations delivered.
Format data based on locale
Languages are not the only thing to take into consideration when localizing applications. A couple of the more obvious things we need to think about is how we present dates and numbers to our local customers.
In Angular, we provide the LOCALE_ID
token to set the locale of the application and register locale data with registerLocaleData()
. When we use the --localize
option with ng build
or run the --configuration
flag with ng serve
, the Angular CLI automatically includes the locale data and sets the LOCALE_ID
value.
With the LOCALE_ID
set to the correct locale, we can use the built-in pipes of Angular to format our data. Angular provides the following pipes:
DatePipe
: Formats a date value.CurrencyPipe
: Transforms a number to a currency string.DecimalPipe
: Transforms a number into a decimal number string.PercentPipe
: Transforms a number to a percentage string.
For example, {{myDate | date}}
uses DatePipe
to display the date in the correct format. We can also use the pipes in TypeScript files as long as we provide them to the module.
Runtime translations
When we run ng serve --configuration=xx
or ng build --localize
then the application is compiled and translated before we run it. However, if we don’t tell Angular to localize our application, then the $localize
tags are left in the code, and it’s possible to instead do the translation at runtime.
This means that we can ship a single application and load the translations that we want to use before the application starts. There is a function loadTranslations
in @angular/localize
that can be used to load translations, in the form of key/value pairs, before the application starts.
Since the translations have to be called before any module file is imported, we can put it in polyfills.ts
. You could also use it in main.ts
by using a dynamic import(...)
for the module.
Here is an example of using loadTranslations
in polyfills.ts
:
import '@angular/localize/init';
import { loadTranslations } from '@angular/localize';
loadTranslations({
'welcome': 'Velkommen'
});
Note that the outcome of this is effectively the same as translation at compile-time. The translation happens only once If you want to change the language at runtime then you must restart the whole application. Since $localize
messages are only processed on the first encounter, they do not provide dynamic language changing without refreshing the browser.
The main benefit is allowing the project to deploy a single application with many translation files. The documentation on this part is still lacking, but hopefully, we get official documentation on how to best work with loadTranslations
and $localize
. There are 3rd party libraries like Soluling out there trying to bridge the gaps.
If a dynamic and runtime-friendly solution is what you are looking for, then you should use Transloco.
Conclusion
We started this article by looking into how the new Ivy engine changed the i18n and localizing of applications with Angular. We looked into what benefits and drawbacks this entails and if and when we should use alternative solutions.
We then looked into adding the built-in package to a solution and how we mark texts for translation. We learned how to configure the application for localization and added tooling to manage our translation files. When we used an editor for translating, we saw how adding context to translations helps.
Finally, after configuring and translating the application, we set up a web server to serve our application both locally and in production.
There are many parts to localizing an application and I hope that after reading this article, you have a better understanding of how you can create and manage multi-language applications with Angular.
Top comments (5)
Thanks for the article!
I'm a bit surprised not to see any mention to ngx-translate, though.
I haven't tried transloco yet -though coming from the ngneat team I don't have any doubts about its quality- but AFAIK ngx-translate is still a pretty popular alternative to the native Angular i18n implementation.
AFAIK ngx-translate is not being actively developed anymore so that's why I didn't mention it. If I'm wrong I'll happily add something about it.
Thanks!!!!
Have a look at this!
Here you can find an alternative, using i18next: dev.to/adrai/unleash-the-full-powe...