
Phrase and beyond
Software localization
Among the big 3 web UI libraries (React, Vue, Angular), Google's Angular stands out as a mature, batteries-included framework. With its CLI, TypeScript support, modules, dependency injectors, reactive programming, and string conventions, Angular is built for projects that can scale while maintaining structure.
Of course, you’re here for Angular internationalization (i18n), and we’ve got you covered. It's no surprise that Angular has robust built-in i18n support. In this step-by-step tutorial on Angular localization and internationalization, we’ll walk you through how to install, configure, and localize your Angular applications using the first-party @angular/localize package.
This is a hands-on guide, so we’ll hit the ground running with a quick demo app and localize it. Let’s get started 🙂
In this article, we’re using the following NPM packages (versions in parentheses):
Meet azcadea, a fictional e-commerce store that specializes in retro arcade cabinets, desperate for localization.
🔗 Resource » Get our starter demo app code from GitHub.
Shoutouts to the following people for making the assets we’ve used in this demo available for free.
Our demo is largely presentational to allow us to focus on the i18n. Its hierarchy looks like the following.
. └── src/ └── app/ │ │ # Defines routes to / and /about ├── app-routing.module.ts │ │ # Root component, with header, footer, │ # and <router-outlet> ├── app.component.html ├── app.component.ts │ │ # Top and bottom bars ├── layout/ │ ├── navbar/ │ │ └── (html/ts) │ └── footer/ │ └── (html/ts) │ └── pages/ │ │ # Renders list of arcade cabinets ├── home/ │ └── (html/ts) │ │ # Simple about page with text └── about/ └── (html/ts)
We’re using Angular’s router to provide two “pages”, Home and About.
Let’s take a look at the Home component in a bit more detail.
import { Component } from '@angular/core'; import { Cabinet } from '../../cabinet.model'; @Component({ selector: 'app-home', templateUrl: './home.component.html', }) export class HomeComponent { // Mock data that we would probably fetch from an API cabinets: Cabinet[] = [ { imageUrl: '/assets/cabinets/mappy.jpg', name: 'Mappy Mini-cabinet', description: 'The original cat and mouse game.', addedAt: new Date(2021, 12, 22), storeCount: 2, price: 27.99, }, // … ]; altFor(cabinet: Cabinet): string { // Hard-coded in English return `Image of ${cabinet.name}`; } }
🗒 Note » In our actual code on GitHub we’re using a little injected service to provide the cabinet data. We’re omitting that code here for brevity.
<!-- Tailwind CSS classes omitted for brevity--> <div> <div *ngFor="let cabinet of cabinets"> <div> <img [alt]="altFor(cabinet)" [src]="cabinet.imageUrl" /> </div> <div> <div> <h3>{{ cabinet.name }}</h3> <!-- Currency symbol is hard-coded --> <p>${{ cabinet.price }}</p> </div> <p>{{ cabinet.description }}</p> <div> <span> <!-- Hard-coded in English --> Available at {{ cabinet.storeCount }} outlets </span> <button> <!-- Hard-coded in English --> Add to cart </button> </div> </div> </div> </div>
This should all be bread-and-butter Angular for you. Note, however, that all of our UI strings are hard-coded in English. We want our app to be available in multiple languages, so we need to internationalize and localize it.
🔗 Resource » Get all of our starter demo app code from GitHub.
The basic recipe is:
We’ll go over all these steps in detail in the following sections.
All we have to do is use the Angular CLI to add the @angular/localize library. From the project root, let’s run the following from the command line.
$ ng add @angular/localize
This will install the @angular/localize NPM package for us, adding it to our project.json
.
🗒 Note » The command will also import the $localize template tag in polyfill.ts
. We’ll use $localize
directly a bit later. For now, just know that Angular needs it to handle strings marked for translation in our templates.
A special i18n
section in our project’s angular.json
file is reserved for configuring the @angular/localize library. We can use this section to define our app’s supported locales.
{ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "newProjectRoot": "projects", "projects": { "azcadea": { // … }, "root": "", "sourceRoot": "src", "prefix": "app", "i18n": { // The locale we use in development, defaults to en-US "sourceLocale": "en-CA", // All other locales our app supports "locales": { "ar": { "translation": "src/locale/messages.ar.xlf" } } }, "architect": { // … } } }, "defaultProject": "azcadea" }
By default, Angular will assume that we use the United States English locale (en-US
) when developing. This is the sourceLocale
, where we translate messages from. Each locale we want to translate to goes under the locales
key.
I changed the sourceLocale
to Canadian English (en-CA
) above and added Arabic (ar
) as a supported locale. Feel free to use any locales you want here. We need to include the path to a translation
file for each locale other than the source (more on translation files a bit later).
🗒 Note » To identify a locale in Angular, use a BCP 47 language tag (like en
for English) for the language, followed by an optional country code (like -US
for the United States). This CLDR list of locales might help.
Now that we’ve installed and configured the @angular/localize package, let’s run through an example of how we would localize a string of text in our app.
Let’s start with our app name in the navbar.
<nav> <div> <img alt="Logo" src="assets/logo.svg" /> <h1>azcadea</h1> </div> <!-- … --> </nav>
We'll add a special i18n
attribute to the <h1>
element, which marks its text for translation.
<nav> <div> <img alt="Logo" src="assets/logo.svg" /> <h1 i18n>azcadea</h1> </div> <!-- … --> </nav>
Next, let’s run the extract-i18n
CLI command from our project root to pull this marked string into a translation file.
$ ng extract-i18n --output-path src/locale
🗒 Note » For better organization, we add the optional --output-path
argument to the command, ensuring that all of our translation files go under the src/locale
directory.
A new file should have appeared in our project:
<?xml version="1.0" encoding="UTF-8" ?> <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <file source-language="en-CA" datatype="plaintext" original="ng2.template"> <body> <trans-unit id="7285274704628585281" datatype="html"> <source>azcadea</source> <context-group purpose="location"> <context context-type="sourcefile">src/app/layout/navbar/navbar.component.html</context> <context context-type="linenumber">11</context> </context-group> </trans-unit> </body> </file> </xliff>
The ng extract-i18n
command combed through our app, pulled out the string we marked with i18n
, and put it in a <trans-unit>
(translation unit) in the new messages.xlf
file.
🗒 Note » XLIFF stands for XML Localization Interchange File Format and is a common standard for translation files. Read more about the XLIFF format on Wikipedia.
🗒 Note » You can use formats other than XLIFF if you want.
To translate our string into Arabic, let’s copy the generated messages.xlf
file to a new file in the same directory and name it messages.ar.xlf
. We can name the copy anything we want as long as it matches the file path we specified in angular.json
above. Now to translate:
<?xml version="1.0" encoding="UTF-8" ?> <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <file source-language="en-CA" datatype="plaintext" original="ng2.template"> <body> <trans-unit id="7285274704628585281" datatype="html"> <source>azcadea</source> <!-- Add translation within <target> tags --> <target>أزكيديا</target> <!-- //… --> </trans-unit> </body> </file> </xliff>
Translation in place, the final step is to merge our new translation into a build. Providing a --localize
argument to the ng build
CLI command should do just the trick.
$ ng build --localize
Each of our configured locales will generate a unique copy of our app with that locale baked in. Take a look under the dist
directory and you should see something like the following.
. └── dist/ └── azcadea/ ├── ar/ │ ├── assets/ │ ├── index.html │ └── … └── en-CA/ ├── assets/ ├── index.html └── …
We can use a simple HTTP server to test these builds. I’ll use the popular http-server
here.
# From the project root $ npx http-server dist/azcadea
Now if we open http://localhost:8080/en-CA
in our browser, we’ll see our app exactly as it was in development. And if we open http://localhost:8080/ar
we’ll see that our brand name has been translated into Arabic.
✋ Heads up » If you’re working along with us from the starter demo, you'll notice that images aren’t showing up in the build. Localized builds will have each have their own assets
subdirectory, e.g. /en-CA/assets
, /ar/assets
. So any URLs in our app that having a leading forward slash, like /assets/foo.jpg
, will break in built versions. A quick fix is to remove the leading forward slash in such URLs, so /assets/foo.jpg
becomes assets/foo.jpg
.
There are two main considerations when configuring a production server for our localized Angular app.
/
to our default locale, /en-CA
in our case. Alternatively, we could try to detect the browser’s preferred language and redirect to the closest locale (more on that a bit later).index.html
whenever any localized URI is requested. So if we get a request for /ar/about
, we need to send back the /ar/about/index.html
file. Our index.html
file contains our single-page Angular app, which will handle the /about
route itself.🗒 Note » Angular builds an entire copy of our app for each locale we support. So switching locales essentially means that when a user wants the Arabic version of our app, we serve them the entire Arabic copy. In our case this means that we redirect them to /ar
, which is the subdirectory (and URI) where our Arabic version lives.
Of course, the implementation details will depend both on your production server and the needs of your app. Here’s a quick and dirty implementation in Express that we can use for testing our production builds in development.
/******************************************************* * ⚠️ This server is for testing production builds in a * development environment. It has not been checked for * security. Please do not use in production! *****************************************************/ const path = require('path'); const express = require('express'); const port = 8080; const rootDir = path.join(__dirname, 'dist/azcadea'); const locales = ['en-CA', 'ar']; const defaultLocale = 'en-CA'; const server = express(); // Serve static files (HTML, CSS, etc.) server.use(express.static(rootDir)); // Always serve the index.html file in a locale // build's directory e.g. /en-CA/foo will return // /en-CA/index.html. This allows Angular to handle // the /foo route itself. locales.forEach((locale) => { server.get(`/${locale}/*`, (req, res) => { res.sendFile( path.resolve(rootDir, locale, 'index.html') ); }); }); // Redirect / to /en-CA server.get('/', (req, res) => res.redirect(`/${defaultLocale}`) ); server.listen(port, () => console.log(`App running at port ${port}…`) );
Now can build our app and run our production test server.
$ ng build --localize --configuration production $ node serve-prod-test.js
If we visit http://localhost:8080
in our browser, we should be redirected to http://localhost:8080/en-CA
and see the English version of our app. And if we enter localhost/ar/about
directly in our browser’s address bar, we should see the Arabic version of our about page. All routes accounted for 🙂
🗒 Note » Depending on your configuration, you might want to adjust each locale’s <base href>
to control relative links. Angular has a handy baseHref
option when configuring a locale that takes care of this. Read about it in the official docs.
🔗 Resource » The Angular documentation has listings for Nginx and Apache server configurations for production Angular apps.
It can be a pain to build our app every time we want to test a change to one of our localizations. One way to alleviate this is to add a development server configuration for each of our locales. This allows us to run our dev server, with hot reloading as usual, while serving a locale-specific version of our app. Let’s add an Arabic version by modifying our angular.json
file.
{ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "newProjectRoot": "projects", "projects": { "azcadea": { // … "architect": { "build": { // … }, "configurations": { "production": { // … }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true, // Localizes default dev build to Canadian English "localize": ["en-CA"] }, // New build configuration with Arabic localization "ar": { // Options optimize for dev environment "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true, // This is the real magic "localize": ["ar"] } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "azcadea:build:production" }, "development": { "browserTarget": "azcadea:build:development" }, // New dev server configuration that targets // Arabic build we added above "ar": { "browserTarget": "azcadea:build:ar" } }, "defaultConfiguration": "development" }, "extract-i18n": { // … }, "test": { // … } } } }, "defaultProject": "azcadea" }
Now we can run the following command line at the project root to work with an Arabic development version of our app.
$ ng serve --configuration ar
🗒 Note » Running ng serve
without the --configuration
argument will start the dev server with the English version of our app.
We can shorten this a bit by adding it as an NPM script to our package.json
.
{ "name": "azcadea", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", // Our new script "start:ar": "ng serve --configuration ar", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", }, "private": true, // … }
Now we can run npm run start:ar
from the command line to run our app in development with Arabic localization.
People can set their preferred locales in their browsers, and these locales are sent as an Accept-Language HTTP header to servers along with normal GET
requests. We can use the Accept-Language
header to resolve the initial locale on the server for our Angular apps. Let’s update our production test Express server to add locale detection and serve a version of our app matching the closest supported locale.
/******************************************************* * ⚠️ This server is for testing production builds in a * development environment. It has not been checked for * security. Please do not use in production! *****************************************************/ const path = require('path'); const express = require('express'); const matchSupportedLocales = require('./match-supported-locales'); // … const locales = ['en-CA', 'ar']; const defaultLocale = 'en-CA'; const server = express(); // … server.get('/', (req, res) => { const closestSupportedLocale = matchSupportedLocales( req.acceptsLanguages(), locales, defaultLocale ); return res.redirect(`/${closestSupportedLocale}`); }); // …
When a visitor hits our root route (/
), we try to match one of our supported locales to one of their accepted locales, retrieved from Express’ helpful req.acceptsLanguages(). The function returns the locales in the incoming Accept-Language
header as an array. Our new matchSupportedLocales()
function uses this array, along with our supported locales and our default locale, to make the best match.
function matchSupportedLocale( acceptsLocales, supportedLocales, defaultLocale ) { return ( firstExactMatch(acceptsLocales, supportedLocales) || firstLanguageMatch(acceptsLocales, supportedLocales) || defaultLocale ); } function firstExactMatch(acceptsLocales, supportedLocales) { return acceptsLocales.find((al) => supportedLocales.includes(al) ); } function firstLanguageMatch( acceptsLocales, supportedLocales ) { for (acceptedLang of languagesFor(acceptsLocales)) { const match = supportedLocales.find( (sl) => languageFor(sl) === acceptedLang ); if (match) { return match; } } } function languagesFor(locales) { return locales.map((loc) => languageFor(loc)); } function languageFor(locale) { return locale.split('-')[0]; } module.exports = matchSupportedLocale;
Here’s the basic algorithm:
en-CA
in her accepted locales, we will serve the en-CA
version of our site.ar-SA
(Arabic, Saudi Arabia) in his accepted locales, we will serve the ar
version of our site.en-CA
in our case).And with that, we can serve the best possible experience to match our visitors’ preferred locales.
Even with browser locale detection we often need to provide our visitors a way to manually select their locale of choice. Luckily, we can cook up a little locale switcher without too much effort.
import { Inject, Component, LOCALE_ID, } from '@angular/core'; @Component({ selector: 'app-locale-switcher', templateUrl: './locale-switcher.component.html', }) export class LocaleSwitcherComponent { locales = [ { code: 'en-CA', name: 'English' }, { code: 'ar', name: 'عربي (Arabic)' }, ]; constructor( @Inject(LOCALE_ID) public activeLocale: string ) {} onChange() { // When the visitor selects Arabic, we redirect // to `/ar` window.location.href = `/${this.activeLocale}`; } }
The special injection token, LOCALE_ID, is used to retrieve the active locale from Angular. When the English version of our site is loaded LOCALE_ID
will be en-CA
, for example. We store this value in an activeLocale
field, which we bind to the switcher’s value in our template.
<select (change)="onChange()" [(ngModel)]="activeLocale" > <option *ngFor="let locale of locales" [value]="locale.code" > {{ locale.name }} </option> </select>
So with very little code we've given our visitors a nice UI to switch locales.
✋ Heads up » When we run ng serve
or npm start
our app will run in development mode with our source locale, and our locale switcher won’t work. If we build our app and run the production test server we wrote above, however, the switcher should switch with no hitch.
Now that we have our i18n infrastructure set up, let’s go deeper into Angular string translation.
We’ve gone through this one, but it bears repeating: we just use the handy i18n
attribute to mark a string for translation.
<h1 i18n>azcadea</h1>
When we run ng extract-i18n
, our marked string will be pulled into messages.xlf
.
<?xml version="1.0" encoding="UTF-8" ?> <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <file source-language="en-CA" datatype="plaintext" original="ng2.template"> <body> <trans-unit id="7285274704628585281" datatype="html"> <source>azcadea</source> <context-group purpose="location"> <context context-type="sourcefile">src/app/navbar/navbar.component.html</context> <context context-type="linenumber">14</context> </context-group> </trans-unit> <!-- ... --> </body> </file> </xliff>
We can copy messages.xlf
to messages.ar.xlf
to translate our string to Arabic (and again for each of our app’s supported locales).
<?xml version="1.0" encoding="UTF-8" ?> <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <file source-language="en-CA" datatype="plaintext" original="ng2.template"> <body> <trans-unit id="7285274704628585281" datatype="html"> <source>azcadea</source> <target>أزكيديا</target> </trans-unit> <!-- ... --> </body> </file> </xliff>
Now we can build our app for production with ng build --localize
to bake our translation into the Arabic version of our app.
🗒 Note » By default, Angular will mark translated strings using an auto-generated ID in the translation files <trans-unit>
s. You can set custom IDs for your translation strings if you want, however.
🗒 Note » You can use an <ng-container>
element if you want to mark an inline string for translation without wrapping it in an otherwise superfluous HTML element. <ng-container>
renders its contents without adding a wrapper.
Let’s mark our logo’s <img alt="…">
attribute for translation: We simply use i18n-{attribute}
.
<nav> <div> <div> <img i18n-alt alt="azcadea logo" src="assets/logo.svg" /> <h1 i18n>azcadea</h1> </div> <!-- … --> </div> <!-- … --> </nav>
Just like i18n
for inner text, i18n-{attribute}
will be caught by ng extract-i18n
and cause the string to be added as a <trans-unit>
to our messages.xlf
.
When we mark our HTML tags with i18n
, the Angular compiler generates code that calls the $localize
template tag underneath the hood. Like any other JavaScript template tag, the function can be called like $localize`Text in source locale`
. Let’s use $localize
to mark our navbar link strings for translation.
import { Component } from '@angular/core'; @Component({ selector: 'app-navbar', templateUrl: './navbar.component.html', }) export class NavbarComponent { home: string = $localize`Home`; about: string = $localize`About`; }
Strings in our component code marked with $localize
will be pulled by ng extract-i18n
into messages.xlf
as usual.
<!-- … --> <trans-unit id="2821179408673282599" datatype="html"> <source>Home</source> <context-group purpose="location"> <context context-type="sourcefile">src/app/navbar/navbar.component.ts</context> <context context-type="linenumber">8</context> </context-group> </trans-unit> <!-- … -->
To complete the translation we of course copy the <trans-unit>
to messages.ar.xlf
, add a <target>
with our Arabic translation, and run ng build --localize
.
Injecting strings that can change at runtime into translations is quite common, and works seamlessly with $localize
. Our arcade cabinet images currently have English-only alt
text that can serve to demonstrate.
// … @Component({ selector: 'app-home', templateUrl: './home.component.html', }) export class HomeComponent implements OnInit { // … altFor(cabinet: Cabinet): string { return `Image of ${cabinet.name}`; } }
All we need to do is stick $localize
in front of our template string, and interpolate cabinet.name
as usual.
// … @Component({ selector: 'app-home', templateUrl: './home.component.html', }) export class HomeComponent implements OnInit { // … altFor(cabinet: Cabinet): string { return $localize`Image of ${cabinet.name}`; } }
Angular is smart enough to know that interpolated values need special placeholders in translation strings, and it adds them in for us when we run ng extract-i18n
.
<!-- … --> <trans-unit id="6223458566712133696" datatype="html"> <source>Image of <x id="PH" equiv-text="cabinet.name"/></source> <context-group purpose="location"> <context context-type="sourcefile">src/app/home/home.component.ts</context> <context context-type="linenumber">29</context> </context-group> </trans-unit> <!-- … -->
Note the <x …>
placeholder tag added above. We need to use this same tag in our translations to let Angular know where we want to inject the runtime value.
<!-- … --> <trans-unit id="6223458566712133696"> <source>Image of <x id="PH" equiv-text="cabinet.name"/></source> <target>صورة <x id="PH" equiv-text="cabinet.name"/></target> </trans-unit> <!-- … -->
That’s all it takes. Now we can ng build --localize
and run our test production server. When we do, opening our browser’s dev tools reveals that our alt
tags are localized with proper interpolation in our builds.
Angular implements the ICU standard for its localized plurals. ICU is a bit outside the scope of this guide, but you can read more about it in The Missing Guide to the ICU Message Format. Still, the ICU syntax is intuitive enough that we don’t need a comprehensive understanding of the standard to use it. An ICU plural message looks like the following.
{todoCount, plural, one { You have one to-do remaining! } other { You have {{todoCount}} to-dos left. Keep going 🙂 } }
The example above is an English translation, of course, so we provide the language’s two plural forms, one
and other
. todoCount
(which can be named anything) is an interpolated integer counter that determines which form is rendered at runtime. We can inject the value of todoCount
into our plural form messages using Angular’s usual {{variable}}
syntax.
Let’s use this syntax to localize the string that indicates how many store outlets provide a given cabinet in azcadea.
<div> <div *ngFor="let cabinet of cabinets"> <!-- … --> <div> <!-- … --> <div> <span i18n> {cabinet.storeCount, plural, =0 {Sold out} =1 {Available at 1 outlet} other {Available at {{ cabinet.storeCount }} outlets} } </span> <!-- … --> </div> </div> </div> </div>
Note that we’ve used =0
and =1
, plural form selectors that override the language’s formal selectors (one
, few
, other
, etc.). This allows us to have a “zero” case in our English translation when English normally uses its other
form for zero.
✋ Heads up » In general, the other
case is always required.
You may have guessed what’s next at this point: We run ng extract-i18n
to get the message into messages.xlf
.
<!-- … --> <trans-unit id="8856905278208146821" datatype="html"> <source> <x id="ICU" equiv-text="{cabinet.storeCount, plural, =0 {Sold out} =1 {Available at 1 outlet} other {Available at {{ cabinet.storeCount }} outlets} }" xid="4465788077291796430"/> </source> <!-- … --> </trans-unit> <trans-unit id="7185053985224017078" datatype="html"> <source>{VAR_PLURAL, plural, =0 {Sold out} =1 {Available at 1 outlet} other {Available at <x id="INTERPOLATION"/> outlets}}</source> <!-- … --> </trans-unit> <!-- … -->
We get two <trans-unit>
s this time, which can seem a bit weird. Angular pulls the ICU nested expression bit out into its own separate <trans-unit>
. This is because sometimes the ICU expression is only part of a bigger translation string. In this case, the ICU expression takes up the entire string, so both <trans-units>
are virtually identical. Let’s copy both <trans-units>
into our Arabic translation file and translate them.
<!-- … --> <trans-unit id="8856905278208146821"> <source> <x id="ICU" equiv-text="{cabinet.storeCount, plural, =0 {Sold out} =1 {Available at 1 outlet} other {Available at {{ cabinet.storeCount }} outlets} }" xid="4465788077291796430"/> </source> <target> <x id="ICU" equiv-text="{cabinet.storeCount, plural, =0 {Sold out} =1 {Available at 1 outlet} other {Available at {{ cabinet.storeCount }} outlets} }" xid="4465788077291796430"/> </target> </trans-unit> <trans-unit id="7185053985224017078"> <source>{VAR_PLURAL, plural, =0 {Sold out} =1 {Available at 1 outlet} other {Available at <x id="INTERPOLATION"/> outlets}}</source> <target>{VAR_PLURAL, plural, =0 {غير متوفر} =1 {متوفر عند فرع <x id="INTERPOLATION"/>} two {متوفر عند فرعين} few {متوفر عند <x id="INTERPOLATION"/> أفرع} many {متوفر عند <x id="INTERPOLATION"/> فرع} other {متوفر عند <x id="INTERPOLATION"/> فرع} }</target> </trans-unit> <!-- … -->
The outer string is copied as-is in translation and is only included in this case to avoid a warning that the Angular build tools will output if we don’t include it. The important <trans-unit>
for us is the second, nested expression. Note that, unlike English, Arabic has six plural forms that need to be added for proper translation.
🔗 Resource » The CLDR Language Plural Rules document lists plural forms for all languages.
If we build our app for production and test it, we see that our plural forms are showing correctly for our supported locales.
The simplest way to localize numbers is to use Angular’s built-in formatting pipes in our component templates. These pipes will use the active locale for formatting by default.
import { Component } from '@angular/core'; @Component({ selector: 'app-number-pipes', template: ` <p>number: {{ myNumber | number }}</p> <p>currency: {{ myNumber | currency: 'CAD' }}</p> <p>percent: {{ myPercent | percent }}</p> `, }) export class NumberPipesComponent { myNumber: number = 9999.99; myPercent: number = 0.21; }
<!-- When active locale is en-CA, renders: --> <p>number: 9,999.99</p> <p>currency: $9,999.99</p> <p>percent: 21%</p> <!-- When active locale is ar, renders: --> <p>number: 9,999.99</p> <p>currency: CA$ 9,999.99</p> <p>percent: 21%</p>
You can notice a difference in the currency formatting above. Unfortunately, Angular always seems to use Western Arabic (1, 2, 3) digits regardless of the active locale. While not ideal, this is often adequate, since most people are familiar enough with Western Arabic digits.
🤿 Go deeper » The decimal, currency, and percent pipes have a lot of formatting options through arguments. They also have function equivalents we can use in our TypeScript: formatNumber(), formatCurrency(), and formatPercent().
Let’s update our Home component to localize our arcade cabinet prices.
<div> <div *ngFor="let cabinet of cabinets"> <!-- … --> <p> {{ cabinet.price | currency: 'USD' }} </p> <!-- … --> </div> </div>
Similar to numbers, the simplest way to localize a date is to use the date pipe in our component templates.
<div> <div *ngFor="let cabinet of cabinets"> <!-- … --> <p> <!-- cabinet.addedAt is a Date object --> {{ cabinet.addedAt | date }} </p> <!-- … --> </div> </div>
Angular will automatically use the active locale when rendering the date.
<!-- When active locale is en-CA, and cabinet.addedAt is 2022-01-22, the above will render: --> <p>Jan. 22, 2022</p> <!-- When active locale is ar, and cabinet.addedAt is 2022-01-22, the above will render: --> <p>2022/01/22</p>
We can specify a date format for the pipe to control its output.
<div> <div *ngFor="let cabinet of cabinets"> <!-- … --> <p> {{ cabinet.addedAt | date: 'd MMM YYYY' }} </p> <!-- … --> </div> </div>
🤿 Go deeper » Check out of all the formatting options for the date pipe, as well as its function equivalent, formatDate().
And with that, our demo app is pretty well localized 😊.
🔗 Resource » Get the entire code of our fully localized app from GitHub.
You may have noticed that managing translation files with Angular’s localization library is a bit clunky. We have to copy the source locale for each of our other supported locales, and we have to keep all these files in sync as we make changes within our app.
There’s a better way: the Phrase Localization Suite can take care of all this juggling for us and keep us focused on the code we love. Let me show you how to use Phrase to take all that file syncing headache out of your localization workflow.
🗒 Note » To follow along here, you’ll need to sign up for Phrase.
With the Phrase CLI installed, we can run the following from the command line in our Angular project root.
$ phrase init
We’ll be asked for an access token at this point, and we can get one by logging into Phrase, clicking our name near the top-right of the page, then clicking Profile Settings ➞ Access tokens.
From there we can click the Generate Token button, enter the required note, and click Save. Next, let’s copy the API token we’re given and paste it where the CLI command is waiting for it.
At this point, we’ll be given a few prompts to complete initialization:
azcadea
../src/locale/messages.xlf
.<locale_name>
placeholder here and enter ./src/locale/messages.<locale_name>.xlf
. This will allow us to add any number of locales to our Phrase project.At this point, we’ll be asked if we want to upload our locales for the first time. We can enter y
to start the upload.
Believe it or not, that’s it from a developer’s end. At this point, our English source translations are available in our Phrase project. Our translators can add Arabic (ar
) as a project language, then use the Phrase web console to get their translation work done.
🗒 Note » It’s probably a good idea to go to Project Settings ➞ Placeholders and selecting the OASIS XLIFF placeholders to match the <x …>
placeholders Angular uses in our translation files. This will make the placeholders easy to spot and use for our translators.
Once translations are ready, we just need to run the following from the command line at the root of our project.
$ phrase pull
This downloads the latest Arabic messages to our codebase.
<?xml version="1.0" encoding="UTF-8"?> <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2"> <file original="ng2.template" datatype="plaintext" source-language="en-CA" target-language="ar"> <body> <trans-unit id="1726363342938046830"> <source xml:lang="en-CA">About</source> <target xml:lang="ar">نبذة عنا</target> </trans-unit> <trans-unit id="7185053985224017078"> <source xml:lang="en-CA">{VAR_PLURAL, plural, =0 {Sold out} =1 {Available at 1 outlet} other {Available at <x id="INTERPOLATION"/> outlets}}</source> <target xml:lang="ar">{VAR_PLURAL, plural, =0 {غير متوفر} =1 {متوفر عند فرع <x id="INTERPOLATION"/>} two {متوفر عند فرعين} few {متوفر عند <x id="INTERPOLATION"/> أفرع} many {متوفر عند <x id="INTERPOLATION"/> فرع} other {متوفر عند <x id="INTERPOLATION"/> فرع} }</target> </trans-unit> <!-- … --> </body> </file> </xliff>
We didn’t have to sync translation files ourselves. Imagine how much easier our whole workflow would be if we had ten locales we had to support, or fifty!
The first-party @angular/localize package is a robust i18n solution for our Angular apps. It is missing some features, however, like support for localized numeral systems. And, if you find Angular creating a production version of your app for each locale a little excessive, you might want to try some third-party Angular libraries for internationalization.
At the time of writing no third-party i18n libraries for Angular are anywhere near as popular as ngx-translate. A small library that does things a bit differently from @angular/localize, ngx-translate has a few things on offer.
✋ Heads up » ngx-translate does not have any support for date and time formatting, however, so you’ll need additional libraries to handle that if you go with ngx-translate.
Here’s an example of a translated string in an Angular app using ngx-translate:
{ "welcome": "Hello there!" }
{ "welcome": "أهلاً بك!" }
<!-- In a template --> <p>{{ "welcome" | translate }}</p> <!-- When runtime locale is en, renders: --> <p>Hello there!</p> <!-- When runtime locale is ar, renders: --> <p>أهلاً بك!</p>
🤿 Go deeper » With ngx-translate we can translate strings using directives or in TypeScript, handle plurals using a plugin, and much more. Read A Deep Dive on Angular I18n with ngx-translate to get all the details.
While not as popular as ngx-translate, the Transloco i18n library for Angular shares the former’s basic design philosophy of loading JSON translation files into your Angular app—no need to rebuild the app for different locales. Well-documented and fully featured via first-party plugins, Transloco is definitely worth a look.
Much like ngx-translate, Transloco:
However, Transloco also:
🔗 Resource » Read our Concise Guide to Number Localization if you’re interested in how different locales use different numeral systems.
Here’s what a translated string looks like when working with Transloco:
{ "greeting": "Hello there!" }
{ "greeting": "أهلاً بك!" }
<!-- In a template --> <ng-container *transloco="let t"> <p>{{ t('greeting') }}</p> </ng-container> <!-- When runtime locale is en, renders: --> <p>Hello there!</p> <!-- When runtime locale is ar, renders: --> <p>أهلاً بك!</p>
🤿 Go deeper » The structural transloco
directive is just one way to translate strings in templates: we can also use a translation pipe or attribute directive. Translating within TypeScript is also available, and much more. Read the Angular Tutorial on Localizing with Transloco for a more detailed look.
At the time of writing i18next is one of the most popular i18n libraries in the world outside of the Angular ecosystem. If you’re coming to Angular from another UI library or framework, you might already be familiar with i18next. After all, i18next seems to support every library and framework under the sun. Angular support for i18next comes in via the third-party angular-i18next package.
Just like ngx-translate and Transloco, i18next:
In addition, i18next boasts unique features:
Here’s what a string translated with i18next looks like in an Angular app:
{ "hello": "Hello there!" }
{ "hello": "أهلاً بك!" }
<!-- In a template --> <p>{{ 'hello' | i18next }}</p> <!-- When runtime locale is en, renders: --> <p>Hello there!</p> <!-- When runtime locale is ar, renders: --> <p>أهلاً بك!</p>
🤿 Go deeper » We’ve just scratched the surface of i18next here. Check out Angular L10n with I18next to get more details on setting up and using i18next in your Angular apps.
We hope that our Angular i18n guide has served you well. No matter which i18n library you choose to go with, Phrase can take your localization process to the next level.
The Phrase Localization Suite supports all the translation file formats we’ve covered here and many more. With its CLI and Bitbucket, GitHub, and GitLab sync, your i18n can be on autopilot. The fully-featured Phrase web console, with machine learning and smart suggestions, is a joy for translators to use. Once translations are ready, they can sync back to your project automatically. You set it and forget it, leaving you to focus on the code you love.
Check out all Phrase features for developers and see for yourself how it can help you take your apps global.
Last updated on October 21, 2022.