Internationalizing your app can make software development a tough experience, particularly if you don’t start doing it from the very starting or you take a serious approach toward it.
Modern apps, where the front-end and the back-end are distinctly separate from one another, can be even trickier to deal with when it comes to internationalization. Suddenly you no longer have access to the plethora of time-tested tools that once helped with internationalizing your traditional server-side page generated web apps.
Accordingly, an AngularJS app requires on-demand delivery of internationalization (i18n) and localization (l10n) data to be delivered to the client to render itself in the appropriate locale. Unlike traditional server-side rendered apps, you can no longer rely on the server to deliver pages that are already localized.
Here, you will learn how you can internationalize your AngularJS app, and will learn about tools that you can use to ease the process. Making your AngularJS app multilingual can pose some interesting challenges, but certain approaches can make it easier to work around most of those challenges.
A Simple i18n Capable AngularJS App
To allow the client to change the language and locale on the fly based on user preferences, you will need to make a number of key design decisions:
- How do you design your app to be language and locale-agnostic from the start?
- How do you structure i18n and l10n data?
- How do you deliver this data efficiently to clients?
- How do you abstract away as much of the low-level implementation details to simplify the developer workflow?
Answering these questions as early as possible can help avoid hindrances in the development process down the line. Each of these challenges will be addressed in this article; some through robust AngularJS libraries, others through certain strategies and approaches.
Internationalization Libraries for AngularJS
There are a number of JavaScript libraries that are built specifically for internationalizing AngularJS apps.
angular-translate
is an AngularJS module that provides filters and directives, along with the ability to load i18n data asynchronously. It supports pluralization through MessageFormat
, and is designed to be highly extensible and configurable.
If you are using angular-translate
in your project, you may find some of the following packages super useful:
angular-sanitize
: can be used to guard against XSS attacks in translations.angular-translate-interpolation-messageformat
: pluralization with support for gender-sensitive text formatting.angular-translate-loader-partial
: used to deliver translated strings to clients.
For a truly dynamic experience, you can add angular-dynamic-locale
to the bunch. This library allows you to change the locale dynamically—and that includes the way dates, numbers, currencies, etc. are all formatted.
Getting Started: Installing Essential Packages
Assuming you already have your AngularJS boilerplate ready, you can use NPM to install the internationalization packages:
npm i -S
angular-translate
angular-translate-interpolation-messageformat
angular-translate-loader-partial
angular-sanitize
messageformat
Once the packages are installed, do not forget to add the modules as your app’s dependencies:
// /src/app/core/core.module.js
app.module('app.core', ['pascalprecht.translate', ...]);
Note that the name of the module is different from the name of the package.
Translating Your First String
Suppose your app has a toolbar with some text and a field with some placeholder text:
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#">Hello</a>
</div>
<div class="collapse navbar-collapse">
<form class="navbar-form navbar-left">
<div class="form-group">
<input type="text"
class="form-control"
ng-model="vm.query"
placeholder="Search">
</div>
...
</div>
</div>
</nav>
The above view has two bits of text that you can internationalize: “Hello” and “Search”. In terms of HTML, one appears as the innertext of an anchor tag, while the other appears as a value of an attribute.
To internationalize them, you will have to replace both string literals with tokens that AngularJS can then replace with the actual translated strings, based on the user’s preference, while rendering the page.
AngularJS can do this by using your tokens to perform a lookup in translation tables that you provide. The module angular-translate
expects these translation tables to be provided as plain JavaScript objects or as JSON objects (if loading remotely).
Here’s an example of what these translation tables would generally look like:
// /src/app/toolbar/i18n/en.json
{
"TOOLBAR": {
"HELLO": "Hello",
"SEARCH": "Search"
}
}
// /src/app/toolbar/i18n/tr.json
{
"TOOLBAR": {
"HELLO": "Merhaba",
"SEARCH": "Ara"
}
}
To internationalize the toolbar view from above, you need to replace the string literals with tokens that AngularJS can use to lookup in the translation table:
<!-- /src/app/toolbar/toolbar.html -->
<a class="navbar-brand" href="#" translate="TOOLBAR.HELLO"></a>
<!-- or -->
<a class="navbar-brand" href="#">{{'TOOLBAR.HELLO' | translate}}</a>
Notice how, for inner text, you can either use the translate
directive or the translate
filter. (You can learn more about the translate
directive here and about translate
filters here.)
With these changes, when the view is rendered, angular-translate
will automatically insert the appropriate translation corresponding to TOOLBAR.HELLO
into the DOM based on the current language.
To tokenize string literals that appear as attribute values, you can use the following approach:
<!-- /src/app/toolbar/toolbar.html -->
<input type="text"
class="form-control"
ng-model="vm.query"
translate
translate-attr-placeholder="TOOLBAR.SEARCH">
Now, what if your tokenized strings contained variables?
To handle cases like “Hello, {{name}}.”, you can perform variable replacement using the same interpolator syntax that AngularJS already supports:
Translation table:
// /src/app/toolbar/i18n/en.json
{
"TOOLBAR": {
"HELLO": "Hello, {{name}}."
}
}
You can then define the variable in a number of ways. Here are a few:
<!-- /src/app/toolbar/toolbar.html -->
<a ...
translate="TOOLBAR.HELLO"
translate-values='{ name: vm.user.name }'></a>
<!-- or -->
<a ...
translate="TOOLBAR.HELLO"
translate-value-name='{{vm.user.name}}'></a>
<!-- or -->
<a ...>{{'TOOLBAR.HELLO | translate:'{ name: vm.user.name }'}}</a>
Dealing with Pluralization and Gender
Pluralization is a pretty hard topic when it comes to i18n and l10n. Different languages and cultures have different rules for how a language handles pluralization in various situations.
Because of these challenges, software developers will sometimes simply not address the problem (or at least won’t address it adequately), resulting in software that produces silly sentences like these:
He saw 1 person(s) on floor 1.
She saw 1 person(s) on floor 3.
Number of people seen on floor 2: 2.
Fortunately, there is a standard for how to handle this, and a JavaScript implementation of the standard is available as MessageFormat.
With MessageFormat, you can replace the above poorly structured sentences with the following:
He saw 1 person on the 2nd floor.
She saw 1 person on the 3rd floor.
They saw 2 people on the 5th floor.
MessageFormat
accepts expressions like the following:
var message = [
'{GENDER, select, male{He} female{She} other{They}}',
'saw',
'{COUNT, plural, =0{no one} one{1 person} other{# people}}',
'on the',
'{FLOOR, selectordinal, one{#st} two{#nd} few{#rd} other{#th}}',
'floor.'
].join(' ');
You can build a formatter with the above array, and use it to generate strings:
var messageFormatter = new MessageFormat('en').compile(message);
messageFormatter({ GENDER: 'male', COUNT: 1, FLOOR: 2 })
// 'He saw 1 person on the 2nd floor.'
messageFormatter({ GENDER: 'female', COUNT: 1, FLOOR: 3 })
// 'She saw 1 person on the 3rd floor.'
messageFormatter({ COUNT: 2, FLOOR: 5 })
// 'They saw 2 people on the 5th floor.'
How can you use MessageFormat
with angular-translate
to take advantage of its full functionality within your apps?
In your app config, you simply tell angular-translate
that message format interpolation is available as follows:
/src/app/core/core.config.js
app.config(function ($translateProvider) {
$translateProvider.addInterpolation('$translateMessageFormatInterpolation');
});
Here is how an entry in the translation table might then look:
// /src/app/main/social/i18n/en.json
{
"SHARED": "{GENDER, select, male{He} female{She} other{They}} shared this."
}
And in the view:
<!-- /src/app/main/social/social.html -->
<div translate="SHARED"
translate-values="{ GENDER: 'male' }"
translate-interpolation="messageformat"></div>
<div>
{{ 'SHARED' | translate:"{ GENDER: 'male' }":'messageformat' }}
</div>
Here you must explicitly indicate that the message format interpolator should be used instead of the default interpolator in AngularJS. This is because the two interpolators differ slightly in their syntax. You can read more about this here.
Providing Translation Tables to Your App
Now that you know how AngularJS can lookup translations for your tokens from translation tables, how does your app know about the translation tables in the first place? How do you tell your app which locale/language should be used?
This is where you learn about $translateProvider
.
You can provide the translation tables for each locale that you want to support directly in your app’s core.config.js
file as follows:
// /src/app/core/core.config.js
app.config(function ($translateProvider) {
$translateProvider.addInterpolation('$translateMessageFormatInterpolation');
$translateProvider.translations('en', {
TOOLBAR: {
HELLO: 'Hello, {{name}}.'
}
});
$translateProvider.translations('tr', {
TOOLBAR: {
HELLO: 'Merhaba, {{name}}.'
}
});
$translateProvider.preferredLanguage('en');
});
Here you are providing translation tables as JavaScript objects for English (en) and Turkish (tr), while declaring the current language to be English (en). If the user wishes to change the language, you can do so with the $translate service:
// /src/app/toolbar/toolbar.controller.js
app.controller('ToolbarCtrl', function ($scope, $translate) {
$scope.changeLanguage = function (languageKey) {
$translate.use(languageKey);
// Persist selection in cookie/local-storage/database/etc...
};
});
There’s still the question of which language should be used by default. Hard-coding the initial language of our app may not always be acceptable. In such cases, an alternative is to attempt to determine the language automatically using $translateProvider:
// /src/app/core/core.config.js
app.config(function ($translateProvider) {
...
$translateProvider.determinePreferredLanguage();
});
determinePreferredLanguage
searches for values in window.navigator
and selects an intelligent default until a clear signal is provided by the user.