Software localization
A Comprehensive Guide to Using the jQuery.i18n Library
We've been dealing with JavaScript localization quite often, and today will be no different. The focus of this tutorial is on jQuery.i18n, a robust internationalization library that supports gender, grammar forms, dynamic change of language, fallback chains, and more. We're going to discuss jQuery i18n's features as well as see it in action by creating a demo JavaScript application. Here is a breakdown of the topics to cover:
- Basic information on jQuery i18n
- Setting up demo application and loading the necessary files
- The process of creating translation files
- Loading translation files and some gotchas to take into consideration
- Switching locale
- Persisting locale data by using History API
- Displaying localized messages based on HTML5
data-
attributes - Displaying localized messages using custom JavaScript code
- Employing gender and pluralization info in translations
- Prettifying the code to avoid repeating ourselves
- Adding "magic" words
- Documenting your translations
The source code for this article is available on GitHub.
A Quick Note About Our Demo App
To see the final result in action, you'll need to set up a web server. I'm going to use Microsoft IIS, but there are plenty of other solutions available, like MAMP, WAMP, and others, that can get you up and running in no time.
Introduction to jQuery i18n
Of course, you know what Wikipedia is and probably have used this website many times, as it has knowledge about anything starting from physics and chemistry to popular (and less popular) films and computer games. Wikipedia is maintained by a company called Wikimedia Foundation, which also takes care of projects like Wikinews, Wikivoyage, and many others. All these projects are worldwide and available in different languages so internationalization is absolutely crucial for Wikimedia. Therefore, this company created a popular jQuery-based solution to internationalize JavaScript applications and called it simply jQuery.i18n. Not a very fancy name, but believe me, there is more than meets the eye—this library is really convenient and powerful, so let's not waste our time and discuss its features now. To make our journey more interesting and useful, we are going to apply the learned concepts into practice.
Translation Files
jQuery.i18n stores translations in simple JSON files. If you are, like me, a fan of Rails, you'll find this process very similar to storing translations in YAML files. The actual translations are in key-value format, for example:
{ "title": "Example Application" }
title
is, of course, the key, whereas Example Application
is its value. It is advised to name keys in lowercase with words separated by -
. jQuery.i18n's docs also suggest to prefix your keys with an application's name
{ "myapp-title": "Example Application" }
but that's not mandatory. Apart from translations, files may host metadata, like information about authors, last updated date, etc. Metadata has its own key starting with @
:
{
"@metadata": {
"authors": [
"Alice",
"David"
],
"last-updated": "2016-10-25",
"locale": "en"
},
"title": "Example Application"
}
Usually, translation files are placed into the i18n directory. Translations for different languages are often stored separately in the files named after the language, for example, en.json, de.json, ru.json, and so on. However, for a simple application, you may put everything in a single file. In this case, translations should be placed under the key named after the language:
"en": {
"title": "Example Application"
},
"ru": {
title: "Тестовое приложение"
}
Moreover, you may provide a path to the file instead of an object with translations:
"en": {
"title": "Example Application"
},
"ru": "ru/ru.json"
To start crafting our demo application, create a new folder with a nested js/i18n directory. Place translation files inside for the languages you prefer. I am going to work with English and Russian in this article:
- js/i18n/en.json
- js/i18n/ru.json
Populate your files with some basic contents: js/i18n/en.json
{
"@metadata": {
"authors": [
"Ilya"
],
"last-updated": "2016-10-25",
"locale": "en"
},
"app-title": "Example Application"
}
js/i18n/ru.json
{
"@metadata": {
"authors": [
"Ilya"
],
"last-updated": "2016-10-25",
"locale": "ru"
},
"app-title": "Тестовое приложение"
}
You might need to set up a proper MIME type for the .json extension in your server config. Also in order to prepare for the next steps clone jQuery i18n locally along with its dependencies:
$ git clone https://github.com/wikimedia/jquery.i18n.git
$ cd jquery.i18n
$ git submodule update --init
Inside your project's directory create js/lib/jquery.i18n folder and copy the contents of the src folder from the cloned project inside (without the nested languages directory). Next inside the js/lib create another directory CLDRPluralRuleParser. Copy the libs/CLDRPluralRuleParser/src/CLDRPluralRuleParser.js file from the cloned project there. Lastly, inside the js create a file called global.js where we will put our custom code. As a result, your project should have the following structure:
|-- js
|-- global.js
|-- i18n
|-- en.json
|-- ru.json
|-- lib
|-- CLDRPluralRuleParser
|-- CLDRPluralRuleParser.js
|-- jquery.i18n
|-- jquery.i18n.js
|-- jquery.i18n.language.js
|-- ...other files here
Now in the root of the project create a simple HTML file with the necessary libraries hooked up in the proper order: demo.html
<!DOCTYPE>
<html>
<head>
<meta charset="utf-8">
<title>Demo Application</title>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script src="js/lib/CLDRPluralRuleParser/CLDRPluralRuleParser.js"></script>
<script src="js/lib/jquery.i18n/jquery.i18n.js"></script>
<script src="js/lib/jquery.i18n/jquery.i18n.messagestore.js"></script>
<script src="js/lib/jquery.i18n/jquery.i18n.fallbacks.js"></script>
<script src="js/lib/jquery.i18n/jquery.i18n.language.js"></script>
<script src="js/lib/jquery.i18n/jquery.i18n.parser.js"></script>
<script src="js/lib/jquery.i18n/jquery.i18n.emitter.js"></script>
<script src="js/lib/jquery.i18n/jquery.i18n.emitter.bidi.js"></script>
<script src="js/global.js"></script>
</head>
<body>
</body>
</html>
I am using jQuery 3 here, but you may choose version 1 or 2, depending on the browsers you wish to support (version 3 works only with modern browsers, so no IE7 for you). So far so good. We can proceed to the next section and load our translation into the application.
Loading Translations
The next important step is of course loading your translations. In the simplest case you would just provide the key-value pairs directly in the code:
$.i18n().load( {
'en': {
'app-title': 'Example Application'
}
} );
Or for a specific locale:
$.i18n().load({
'app-title': 'Example Application'
}, 'en');
For complex apps, however, that's not the best choice. Instead, I'd recommend specifying a remote URL like this:
$.i18n().load( {
'en': 'dir/en.json'
} );
Do note that you may load messages in parts, meaning that this code
$.i18n().load( {
'en': {
'app-title': 'Example Application'
}
} );
// ... some instructions
$.i18n().load( {
'en': {
'another key': 'a value'
}
} );
is totally valid. Just make sure you don't overwrite any existing keys. Let's load translations into the demo app now: js/global.js
jQuery(function($) {
$.i18n().load( {
'en': './js/i18n/en.json',
'ru': './js/i18n/ru.json'
} );
});
Another important thing is that the load
function returns a jQuery Promise making it easy to perform some action only after the translations are loaded successfully, for example:
$.i18n().load( {
'en': 'i18n/en.json',
'ru': 'i18n/ru.json'
} ).done( function() { console.log('done!') } );
Now that our translations are loaded, we can introduce a mechanism to switch locales.
Switching Locale
Which locale is going to be set as a default upon the page load? First of all, the default locale can be provided as an option passed to the $.i18n
:
$.i18n( {
locale: 'en'
} );
Also, jQuery.i18n will check the value of the lang
attribute set for the html
tag. Let's add it now: demo.html
<html lang="en" dir="ltr">
If the default locale was not set in any of these two ways, the library will try to get the language setting passed by the browser. Internally the default language is set to English. To avoid any unexpected behavior it is recommended to set the default locale explicitly. Of course, it's possible to switch the chosen locale later by modifying the locale
option:
$.i18n().locale = 'ru';
Let's add links to switch locales now: demo.html
<ul class="switch-locale">
<li><a href="#" data-locale="en">English</a></li>
<li><a href="#" data-locale="ru">Русский</a></li>
</ul>
Handle the click event by preventing the default action and switching locale based on the data-locale
attribute: js/global.js
jQuery(function($) {
// ...
$.i18n().load( {
'en': './js/i18n/en.json',
'ru': './js/i18n/ru.json'
} ).done(function() {
$('.switch-locale').on('click', 'a', function(e) {
e.preventDefault();
$.i18n().locale = $(this).data('locale');
});
});
});
Note that I am putting this event handler inside the promise to make sure that the translations are loaded first.
Persisting Locale
Another thing that we are going to do is persist the chosen locale by appending it to the URL in a form of a GET param. So, for example, this URL http://localhost/demo.html?locale=ru
is going to request the Russian version of the website. It is very important to do so because when a user shares a link he expects that other people will see the same version of the site. In order to achieve this, however, we'll need two additional libraries. The first one is History API, which supports HTML5 History/State APIs. I'm going to take the bundled html4+html5 version for jQuery. The second is the url library, which makes working with URLs in JavaScript a breeze. Grab the minified version. Hook up these two libraries now: demo.html
<!-- ... -->
<script src="js/lib/history/jquery.history.js"></script>
<script src="js/lib/url/url.min.js"></script>
<script src="js/global.js"></script>
<!-- ... -->
Now code a simple function to change the locale: js/global.js
var set_locale_to = function(locale) {
if (locale)
$.i18n().locale = locale;
};
It should be called once the page is loaded and translations are done. The locale itself should be fetched from the ?locale
parameter: js/global.js
jQuery(function() {
$.i18n().load( {
// ...
} ).done(function() {
set_locale_to(url('?locale'));
// ...
});
});
url('?locale')
takes the value of the locale
GET parameter. Also, we need to listen for the statechange
event (that happens, for example, when the pushState
method was invoked) and change the locale accordingly: js/global.js
jQuery(function() {
$.i18n().load( {
// ...
} ).done(function() {
set_locale_to(url('?locale'));
History.Adapter.bind(window, 'statechange', function(){
set_locale_to(url('?locale'));
});
// ...
});
});
And lastly use pushState
once a link to switch the language was clicked. Here is the final version of the script: js/global.js
var set_locale_to = function(locale) {
if (locale)
$.i18n().locale = locale;
};
jQuery(function() {
$.i18n().load( {
'en': './js/i18n/en.json',
'ru': './js/i18n/ru.json'
} ).done(function() {
set_locale_to(url('?locale'));
History.Adapter.bind(window, 'statechange', function(){
set_locale_to(url('?locale'));
});
$('.switch-locale').on('click', 'a', function(e) {
e.preventDefault();
History.pushState(null, null, "?locale=" + $(this).data('locale'));
});
});
});
Even if the locale
parameter is missing, we still have the lang="en"
set for the html
tag.
Displaying Translations with Data API
The easiest way to display localized content is by using data-
attributes. All you need to do is provide a data-i18n
attribute and set the translation's key as its value. For example, let's display our site name: demo.html.
<h1 data-i18n="app-title"></h1>
app-title
, as you recall, was defined inside the en.json file as "app-title": "Example Application"
and in ru.json as "app-title": "Тестовое приложение"
. To actually display the message use the i18n()
method without any arguments. This method should be applied to the exact element or to its parent. We can simply say body
: js/global.js
var set_locale_to = function(locale) {
if (locale) {
$.i18n().locale = locale;
}
$('body').i18n();
};
// ...
In order to provide a fallback text that will be displayed while translations are being loaded, simply place it into the tag: demo.html
<h1 data-i18n="app-title">Example Application</h1>
Now reload your page and observe the result.
Parameters, gender and pluralization in translations
In the previous section, we've seen the simplest example of displaying localized messages by using the data-i18n
parameter, but obviously, in many cases, that's not enough. To fetch translation for an arbitrary key, you can use the following code:
$.i18n('some-key');
This way we can, for example, display a welcoming message (with a fallback text): demo.html
<body>
<!-- ... -->
<h2 id="welcome">Welcome</h2>
</body>
Add translations: en.json
"welcome": "Welcome!"
ru.json
"welcome": "Добро пожаловать!"
and tweak the code: global.js
var set_locale_to = function(locale) {
// ...
$('#welcome').text($.i18n('welcome'));
};
Pretty nice, but it would be better to display a user's name as well. In order to do this, we need to add a parameter inside the message. Parameters in jQuery.i18n are being named $1
, $2
, $3
etc: en.json
"welcome": "Welcome, $1!"
ru.json
"welcome": "Добро пожаловать, $1!"
To set the parameter's value, simply pass another argument to the $.i18n
: global.js
var set_locale_to = function(locale) {
// ...
$('#welcome').text($.i18n('welcome', 'John'));
};
If you had two parameters, you would of course pass two arguments to the method, and the first one will be assigned to the $1
variable, whereas the second to the $2
. Now, what about the gender info? Suppose, for example, we want to display a bunch of e-mails from different people with a header "You received a new letter from Someone. He/She says:". First of all, add a new markup: demo.html
<!-- ... -->
<div id="letter-1">
<p><em></em></p>
<p>letter's text...</p>
</div>
<div id="letter-2">
<p><em></em></p>
<p>letter's text...</p>
</div>
Now translations: en.json
"letter": "A letter from $1! {{GENDER:$2|He|She}} says:"
ru.json
"letter": "Письмо от $1! {{GENDER:$2|Он|Она}} говорит:"
So GENDER:
acts as a switch. It receives a parameter and chooses one of two options: The first is picked for the male gender, whereas the second is for the female one. To be able to use this message, provide male
or female
as the second argument: global.js
var set_locale_to = function(locale) {
// ...
$('#letter-1').find('p > em').text($.i18n('letter', 'Ann', 'female'));
$('#letter-2').find('p > em').text($.i18n('letter', 'Rick', 'male'));
}
Also, let's display how many letters the user has. For this, we'll need the PLURAL: switch. demo.html
<p id="letters"></p>
The messages: en.json
"letters": "You have $1 {{PLURAL:$1|letter|letters}}"
ru.json
"letters": "У вас $1 {{PLURAL:$1|письмо|писем|письма}}"
The Russian language has more complex pluralization rules, so I had to provide an additional option for the PLURAL:
switch. Now the code: global.js
var set_locale_to = function(locale) {
// ...
$('#letters').text($.i18n('letters', 5));
};
All information about gender and pluralization is stored inside the jquery.i18n.language.js file.
Prettifying the Code
Everything works pretty nice, but things are becoming tedious. For each separate translation, I have to write another line of code which is not particularly great. What I want to do is write a cycle that takes each element requiring translation and displays a message inside based on its parameters. As a first step, modify the markup: demo.html
<!-- ... -->
<h1 class="translate" data-args="app-title">Example Application</h1>
<h2 class="translate" data-args="welcome,John">Welcome</h2>
<p class="translate" data-args="letters,5"></p>
<div>
<p><em class="translate" data-args="letter,Ann,female"></em></p>
<p>letter's text...</p>
</div>
<div>
<p><em class="translate" data-args="letter,Rick,male"></em></p>
<p>letter's text...</p>
</div>
Now for each element with the .translate
class we need to take the value of the data-args
, split it into an array and pass it to the $.i18n
method. The only problem is that we don't know how many arguments are going to be passed. That's not a problem however with the apply method:
var set_locale_to = function(locale) {
// ...
$('.translate').each(function() {
var args = [], $this = $(this);
if ($this.data('args'))
args = $this.data('args').split(',');
$this.html( $.i18n.apply(null, args) );
};
apply
accepts two arguments: the first is the value of this
inside the called function and the second is an array representing all the arguments to pass. Now you may reload the page and observe the result. Of course this solution is not ideal but you can extend it further as needed.
Magic Words
PLURAL:
and GENDER:
are both "magic" words the library supports. You can, however, introduce new magic words as needed by extending the $.i18n.parser.emitter
: global.js
jQuery(function() {
$.extend($.i18n.parser.emitter, {
sitename: function() {
return "Demo";
}
});
});
So, the sitename
is the magic word, and the corresponding functions return its value. To fetch this value, use a template-like syntax (similar to what Handlebars use): {{SITENAME}}
or {{sitename}}
. :
is not needed because sitename
does not accept any arguments. Now you may use this magic word in your translations: en.json
"copyright": "{{SITENAME}}. All rights reserved."
ru.json
"copyright": "{{SITENAME}}. All rights reserved."
The markup: demo.html
<!-- ... -->
<footer class="translate" data-args="copyright"></footer>
The magic words can be more complex. This one, for example, is going to construct a link with the specified title and URL: global.js
// ...
$.extend($.i18n.parser.emitter, {
sitename: function() {
return "Demo";
},
link: function (nodes) {
return '<a href="' + nodes[1] + '">' + nodes[0] + '</a>';
}
} );
nodes
is an array of arguments. Use magic words just like gender:
or plural:
en.json
"about": "{{link:About {{SITENAME}}|localhost/about?locale=en}}"
ru.json
"about": "{{link:{{SITENAME}}|localhost/about?locale=ru}}"
Note that magic words can be nested as shown in this example.
Documenting Your Translations
Localizing applications can be challenging. In many cases simply translating messages is not enough as you need to know their context. Therefore jQuery.i18n's translations can be documented. Usually, it's done inside a file called qqq.json. Inside you provide translation keys and their descriptions. For example, i18n/qqq.json.
{
"@metadata": {
"authors": [
"Ilya"
]
},
"app-title": "App's title",
"welcome": "Welcoming message. Should be friendly.",
"letter": "Notification about letter from someone. Should be informal.",
"letters": "Total letters count.",
"copyright": "The name of the site (transliterated) and copyrights.",
"about": "Link to About Us page. The URL should contain the proper locale."
}
Having a such file in place is really useful.
Phrase and Translation Files
Working with translation files is hard, especially when the app is big and supports many languages. You might easily miss some translations for a specific language which will lead to user confusion—and so Phrase can make your life easier! Grab your 14-day trial. Phrase supports many different languages and frameworks, including JavaScript of course. It allows to easily import and export translation data. What's cool, you can quickly understand which translation keys are missing because it's easy to lose track when working with many languages in big applications. On top of that, you can collaborate with translators as it's much better to have professionally done localization for your website.
Conclusion
In this tutorial, we've seen the jQuery.i18n library by Wikimedia Foundation in action. We discussed all of its main features and hopefully, by now, you feel more confident about using it. Of course, there are other similar (and not so similar) solutions available, so you might want to try them as well.