A Quick Note About the Demo App

To be able to see the final result in action you’ll need to setup a web server. I am going to use Microsoft IIS but there are plenty of other solutions available that can get you up and running in a minute:

About 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, that 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 it’s 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 the 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

1

2

{"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 suggests to prefix your keys with an application’s name

1

2

{"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 @:

1

2

3

4

5

6

7

8

9

10

11

12

{

"@metadata":{

"authors":[

"Alice",

"David"

],

"last-updated":"2016-10-25",

"locale":"en"

},

"title":"Example Application"

}

Usually translation files are being 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:

1

2

3

4

5

6

7

"en":{

"title":"Example Application"

},

"ru":{

title:"Тестовое приложение"

}

Moreover, you may provide a path to the file instead of an object with translations:

1

2

3

4

5

"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

1

2

3

4

5

6

7

8

9

10

11

{

"@metadata":{

"authors":[

"Ilya"

],

"last-updated":"2016-10-25",

"locale":"en"

},

"app-title":"Example Application"

}

js/i18n/ru.json

1

2

3

4

5

6

7

8

9

10

11

{

"@metadata":{

"authors":[

"Ilya"

],

"last-updated":"2016-10-25",

"locale":"ru"

},

"app-title":"Тестовое приложение"

}

You might need to setup 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:

1

2

3

4

$git clonehttps://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:

1

2

3

4

5

6

7

8

9

10

11

12

13

|--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:

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:

1

2

3

4

5

6

$.i18n().load({

'en':{

'app-title':'Example Application'

}

});

Or for a specific locale:

1

2

3

4

$.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:

1

2

3

4

$.i18n().load({

'en':'dir/en.json'

});

Do note that you may load messages in parts, meaning that this code

1

2

3

4

5

6

7

8

9

10

11

12

13

14

$.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

1

2

3

4

5

6

7

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:

1

2

3

4

5

$.i18n().load({

'en':'i18n/en.json',

'ru':'i18n/ru.json'

}).done(function(){console.log('done!')});

Now that our translations is 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:

1

2

3

4

$.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

1

2

<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 behaviour it is recommended to set the default locale explicitly.

Of course it is possible to switch the chosen locale later by modifying the locale option:

1

2

$.i18n().locale='ru';

Let’s add links to switch locale now:

demo.html

1

2

3

4

5

<ul class="switch-locale">

<li><ahref="#"data-locale="en">English</a></li>

<li><ahref="#"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

1

2

3

4

5

6

7

8

9

10

11

12

13

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.

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

1

2

3

4

5

6

7

8

9

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

1

2

3

4

5

6

7

8

9

10

11

12

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

varset_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 a 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

1

2

<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

1

2

3

4

5

6

7

8

varset_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

1

2

<h1 data-i18n="app-title">Example Application</h1>

Now reload your page and observe result!

Parameters, gender and pluralization in translations

In the previous section we’ve seen the simplest example of displaying localized messages by using 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

1

2

$.i18n('some-key');

This way we can, for example, display a welcoming message (with a fallback text):

demo.html

1

2

3

4

5

<body>

<!--...-->

<h2 id="welcome">Welcome</h2>

</body>

Add translations:

en.json

1

2

"welcome":"Welcome!"

ru.json

1

2

"welcome":"Добро пожаловать!"

and tweak the code:

global.js

1

2

3

4

5

varset_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 parameter inside the message. Parameters in jQuery.I18n are being named $1, $2, $3 etc:

en.json

1

2

"welcome":"Welcome, $1!"

ru.json

1

2

"welcome":"Добро пожаловать, $1!"

To set parameter’s value, simply pass another argument to the $.i18n:

global.js

1

2

3

4

5

varset_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 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

1

2

3

4

5

6

7

8

9

10

11

<!--...-->

<div id="letter-1">

<p><em></em></p>

<p>letter's text...</p>

</div>

<div id="letter-2">

<p><em></em></p>

<p>letter'stext...</p>

</div>

Now translations:

en.json

1

2

"letter":"A letter from $1! {{GENDER:$2|He|She}} says:"

ru.json

1

2

"letter":"Письмо от $1! {{GENDER:$2|Он|Она}} говорит:"

So GENDER: acts as a switch. It receives parameter and chooses one of two options: the first is picked for the male gender, whereas the second – for the female. To be able to use this message, provide male or female as the second argument:

global.js

1

2

3

4

5

6

varset_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 does the user have. For this we’ll need the PLURAL: switch.

demo.html

1

2

<pid="letters"></p>

The messages:

en.json

1

2

"letters":"You have $1 {{PLURAL:$1|letter|letters}}"

ru.json

1

2

"letters":"У вас $1 {{PLURAL:$1|письмо|писем|письма}}"

Russian language has more complex pluralization rules so I had to provide additional option for the PLURAL: switch. Now the code:

global.js

1

2

3

4

5

varset_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 is working 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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

<!--...-->

<h1 class="translate"data-args="app-title">Example Application</h1>

<h2 class="translate"data-args="welcome,John">Welcome</h2>

<pclass="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'stext...</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 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:

1

2

3

4

5

6

7

8

9

varset_locale_to=function(locale){

// ...

$('.translate').each(function(){

varargs=[],$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

1

2

3

4

5

6

7

8

jQuery(function(){

$.extend($.i18n.parser.emitter,{

sitename:function(){

return"Demo";

}

});

});

So, the sitename is the magic word’s name and the corresponding functions returns its value. To fetch this value, use 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

1

2

"copyright":"{{SITENAME}}. All rights reserved."

ru.json

1

2

"copyright":"{{SITENAME}}. All rights reserved."

The markup:

demo.html

1

2

3

<!--...-->

<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

1

2

3

4

5

6

7

8

9

10

// ...

$.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 this magic words just like gender: or plural:

en.json

1

2

"about":"{{link:About {{SITENAME}}|localhost/about?locale=en}}"

ru.json

1

2

"about":"{{link:{{SITENAME}}|localhost/about?locale=ru}}"

Note that magic words can be nested, like shown in this example.

Documenting Your Translations

Localizing application is hard. 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 is done inside a file called qqq.json. Inside you provide translations keys and their descriptions. For example:

i18n/qqq.json

1

2

3

4

5

6

7

8

9

10

11

12

13

14

{

"@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 such file in place is really useful.

PhraseApp 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’s confusion. And so PhraseApp can make your life easier!

Grab your 14-days trial. PhraseApp supports many different languages and frameworks, including JavaScript of course. It allows to easily import and export translations 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 article we’ve seen 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.