AngularJS–Part 12, Multi language support

Introduction

Our application is a product used by many global companies and thus we support multiple languages like English, French, Spanish, German and more. The question is now, how does AngularJS help us to provide our product in all the necessary languages? It turns out that Angular itself does only have limited support for globalization and localization (also called internationalization and localization). But a quick research on the web quickly leads to the following three implementations helping with translating texts into different languages depending on the locale of the user. Here are the links

I am sure there exist other implementations out there but for our purposes the first approach which is based on the Gettext format is the favorite one due to the fact that we do not have to use keywords as placeholders for texts but can just use plain English texts in code, and the rich ecosystem around it. Specifically of interest is the Potedit application which provides a very user friendly way of translating texts.

Preparing the system

Since this time we need a couple of more libraries than just AngularJS we will use Bower to install all our dependencies. Bower is a package management system for the client similar to the nodeJS package manager npm which is used to manage server side packages. If you do not have node installed then please do it now from here.

Open a command prompt as Administrator or a bash console and enter the following command

image

This will install Bower globally such as that it is available in any directory of the system. Now we create a new folder for our sample app. In my case I will call it c:\samples\translation. In your console navigate to this directory.

To install Angular locally in the current directory enter

image

To install Bootstrap (which we use for our layout) locally enter

image

Finally we want to install the angular-gettext module, thus enter

image

Bower has created a sub folder bower_components in our current directory and placed the downloaded components into it. The folder structure should look like this

image

As we can see Bower has created four folders, one for angular, one for bootstrap, one for angular-gettext and one for the implicitly downloaded jQuery (bootstrap depends on jQuery). Now we need one more install. We need to install Grunt which is a JavaScript task runner. We need Grunt to help us automate the extraction of texts to translate from the source code. We will install Grunt globally such as that it is available for all projects on our system. Still in the console enter the following command

image

This uses npm to install the Grunt command line interface. Now we install Grunt locally with this command

image

and then we also need the node module containing the Grunt tasks for text extraction and compilation

image

Sample application

Finally after all this setup we can start to implement a sample application which shows the various aspects affected by globalization and localization. Using your favorite code editor (in my case Sublime Text 3) create a new file app.js which will contain the JavaScript code. Create a new Angular module and define a controller TestCtrl as follows

image

Now add another file index.html which will contain the template for our sample application. Add the base HTML to make it an Angular application

image

Note how on line 5 we have added a link to the bootstrap CSS file and on line 12 through 14 we have included the angular, angular-gettext and our own app JavaScript file.

Now lets add some simple texts that later on we want to have translated into the language of the user. Add this code snippet to the div of the template

image

Now open index.html in your favorite browser. The result is very unspectacular yet expected. We will just see two paragraphs with the texts as entered above.

To enable translation support we have to add the directive translate to the two paragraph tags.

image

If we refresh the browser nothing changes at all. This is clear since we did not yet initialize angular-gettext. Lets do this now. We use the run function of the Angular module to do so

image

On line 3 we ask the Angular injector service to provide us the gettextCatalog service defined in the angular-gettext library. Then on line 4 we use this service and declare that we want to use the language German (code ‘de’) as the current language. On line 5 we declare that we want the service to help us to easily spot missing translations by marking the (original) text with a [Missing] prefix. After adding this code snippet save and refresh the browser. Nothing changed so far and the text is still in English as we have added it in code.

Extracting and translating

Now we need to extract all texts that need a translation from our source code. If we have a lot of texts that would be a nightmare if we did have to do it by hand. But don’t worry, we can easily automate this daunting task. We will use Grunt for this. Still within your favorite text editor create a new file called Gruntfile.js in the sample folder. This file will contain the instructions for the Grunt task runner (for an introduction on how to write a Grunt file see this tutorial).  The base structure of a simple Grunt file always looks like this

image

That is, we export a function containing the various tasks that Grunt can execute. Inside the above function we first need to load the Grunt tasks provided by the node module grunt-angular-gettext

image

Now we want to use the first task provided by grunt-angular-gettext module and configure it. This task will parse all our source files and extract the texts we want to have translated – the ones that have an associated translate directive – and add them to a so called pot file. We do this by adding the following snippet to the Gruntfile.js

image

On line 5 we define the task name we want to configure or initialize. On line 8 we declare that we want the task to parse all html files in the current directory or any sub directory of it and output the result of the task into a file called template.pot residing in the sub folder po of the current directory.

Once we have saved the Grunt file we can issue the following command in the console

image

If we have done everything right then we should see the following output

image

and we will have a new subfolder po containing a file template.pot. We can open this file in our text editor and it should look like this

image

We can see on line 7 and 11 that the two texts to translate have been correctly identified and extracted. This file will be used as input by one of the many editors available that are able to handle the gettext format. In this sample we will use the (free) Poedit application. Please head to this site and download and install the application on your system.

Run Poedit and create a new catalog from pot file. Select the template.pot file just created when asked for it. Select the language – German in our case. And then translate the texts

image

Hit Save when done. Save the result as suggested with the name de.po in the po subdirectory of our working folder. We can then once again (just for curiosity) open the saved file with our text editor just to see this

image

Now this although human readable is not the format we need in our Angular application. But no worries, we have another task provided by the grunt-angular-gettext module called nggettext_compile which will compile the output of the Poedit application into a format that we can consume in our Angular application. Let’s configure this task in the Grunt file. Add the following snippet right after the first task configuration and save the file.

image

On line 12 we define the task we want to configure and on line 15 we declare that we want this task to load all po files in the subfolder po of the current directory and compile them into a resulting file translations.js located in the current directory.

In the console run this command

image

Again, if we did everything correctly the console output should show this

image

Opening the translations.js file in our text editor we get a (halfway minified) version of a new Angular module called gettext which contains the configuration of the gettextCatalog service with the various translations provided (in our case its only German so far).

image

Back in our index.html we have to include the above file.

image

And as a last step in the app.js file we have to declare that our app module depends on the gettext module.

image

Go back to your browser and refresh the page. Hurray our text has been translated!

image

Let’s now see how the translation plugin handles missing translations. Add another paragraph to the template

image

After saving refresh the browser and you should see this

image

The application gives us a hint by prefixing the text whose translation could not be found with [MISSING]. This is very helpful, QA can easily spot missing translations. We can even write an end-to-end test (by using protractor) to find missing translations.

More advanced scenarios

What about scenarios where we have dynamic values in the text whose value is only known at runtime through data binding? Something like this

image

will translate just fine.

Now what about situations where the usage of the translate directive is not evident like the value of the placeholder attribute of a textbox (an input of type text). Can we also have this value translated? It turns out that this can indeed be achieved by using Angular expressions and filters. Let’s assume that we have a login dialog where we display an input box for the username and password. For both we want to display a placeholder text if they are empty. Here’s how we do it

image

Please note how the expression is written using inner (single) quotes to delimit the text to be translated and outer (double) quotes do delimit the value of the placeholder attribute which is an expression.

image

In the above case the filter translate is applied to the text Enter your username.

Pluralization

An important case is the situation where we want to correctly translate the singular and plural form of a text. Take the sample

  • John gets assigned one task [singular] – e.g. in German einen Auftrag
  • John gets assigned 3 tasks [plural] – in German 3 Aufträge

Our translation library also handles this situation. We can write

image

We directly write the singular (one task) in the <span> and use the two directives translate-n and translate-plural to define which is the counter/number and which is the plural form when using the counter/number.

Now we use the nggettext_extract grunt task again to extract all new texts

image

we can use Poedit to translate the new values. In Poedit open your existing catalog (de.po in my case) and then click the menu Catalog –> Update from pot file and select the template.pot file generated by the previous grunt task. You should see this

image

The new not yet translated texts are in bold. Translate the values as follows

image

After saving we need to update our controller as follows

image

and now you can display the page in the browser

image 

If we now change the model value count to say 5 we get this

image

Changing the language on the fly

Can we change the language during run-time? Yes we can and it is very easy. Let’s first create another translation, say French using Poedit.

Add an array languages and a variable lang to the controller as follows

image

Also add a function to the controller to change the current language

image

In order to make this function work we have to inject the gettextCatalog service into our controller.

image

Add a drop-down bound to the languages array to the view

image

This is the result when selecting French as the target language

image

Conclusion

In this post I have shown how we can get first class support in Angular for globalization and localization using the angular-gettext extension. All scenarios that we encounter in our large application are covered. Translating texts is straight forward and very user friendly when using a (free) tool like Poedit. The extraction of texts that need to be translated from source code can be fully automated as well as the compilation of the translated texts into a JavaScript library.

Related Articles:

Post Footer automatically generated by Add Post Footer Plugin for wordpress.

About Gabriel Schenker

Gabriel N. Schenker started his career as a physicist. Following his passion and interest in stars and the universe he chose to write his Ph.D. thesis in astrophysics. Soon after this he dedicated all his time to his second passion, writing and architecting software. Gabriel has since been working for over 12 years as an independent consultant, trainer, and mentor mainly on the .NET platform. He is currently working as chief software architect in a mid-size US company based in Austin TX providing software and services to the pharmaceutical industry as well as to many well-known hospitals and universities throughout the US and in many other countries around the world. Gabriel is passionate about software development and tries to make the life of developers easier by providing guidelines and frameworks to reduce friction in the software development process. Gabriel is married and father of four children and during his spare time likes hiking in the mountains, cooking and reading.
This entry was posted in AngularJS, introduction. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • https://riccardo.forina.me/ Riccardo Forina

    Nice write-up, thanks. Did you try also ng-i18next?

    • gabrielschenker

      Thank you for the link. No, I did not try it. It looks interesting.

  • jlamande

    Nice
    The browser as a translation platform ! :-)

  • Gino

    if I have many languages ​​and a lot of text for each language would not be a pain for the browser to handle all that long .js file in the memory? Is there a better method? Maybe making a binding of the one language needed? I’m wrong?

    • gabrielschenker

      What we do is first let the user log-in (in our case the user explicitly chooses the language) and then generate the main page (from a server side template) that it only includes the necessary language file (say German)

      • Gino

        But how if I compile all languages (.po files) in one (translations.js)? Just the name of the file say it “translationS.js”. There is a method for binding from server just one translatioN, like in PHP, for not overhead the browser?

        • gabrielschenker

          You need to create a file per language, e.g. translations-de-DE.js

  • Nuno Santos

    HI, thank you for this step by step tutorial.

    I follow it and in the step we execute grunt nggettext_compile, the result in the js file (translate.js) is:

    angular.module(“gettext”).run(['gettextCatalog', function (gettextCatalog) {
    /* jshint -W100 */

    /* jshint +W100 */
    }]);

    Why grunt don’t generate the js translation file correctly?
    Can you put here your example project to download?

    Thanks

    • Manuel

      Same… Any solution?
      Thanks!

      • Manuel

        emzweb thanks!


        translations.js’ : ['po/*.po'] —> it should be ‘translations.js’ : ['po/*.pot'] (as you created a template.pot with t !)

  • Diego Mesa

    Hello, I was a little bit confused with the official doc http://gruntjs.com/getting-started but this tutorial is straightforward.

    I come from the silverlight world too, We are migrating a medium silverlight app, and I just want to say that angular is great!

    Thank you

  • Pingback: Localization With Angular-GetText | OutOfMemoryException

  • Laura

    It would help a lot if you’d post complete files instead of just screenshots of snippets, I lost the oversight of what to include where.

  • Pingback: Angular.js und Lokalisierung | Programmers Blog

  • http://syabro.com/ Max Syabro

    Mate, amazing article.
    But console commands as pictures is crazy shit :)

  • emzweb

    Great article. Just came across an error : ‘translations.js’ : ['po/*.po'] —> it should be ‘translations.js’ : ['po/*.pot'] (as you created a template.pot with t !)

  • summ3r

    To swiftly translate .po files, I recommend the online localization tool: http://poeditor.com/
    It’s perfect for collaborative work, and free within a 1000 string limit.

  • Niklas Rasmusson

    How should I handle changes to the original text string, which serves as the key in the pot-file? As soon as I change the original string I have to manually update all the compiled po-files?

  • pizzafroide

    Great article indeed. Just two details though:
    1. Please find a better French translation hereafter.
    2. Even though it’s not the point of your article, maybe you could mention that long internationalized strings should not be split. For instance, your pluralization example could be merged into one string. In fact, its French translation is forced to be something like “John s’est vu attribuer une tâche” (when count != 1); it is correct but a better translation would turn the sentence around, such as “Une tâche a été attribuée à John” which is not possible when the sentence is split up. Therefore, in my opinion, a better example would be “John gets assigned one task”.

    My French translation (it is my native language, btw):
    msgid “Here we don’t have a translation…”
    msgstr “”

    msgid “Enter your password”
    msgstr “Entrer votre mot de passe”

    msgid “Enter your username”
    msgstr “Entrer votre nom d’utilisateur”

    msgid “John gets assigned”
    msgstr “John s’est vu attribuer”

    msgid “Let’s welcome Mr. {{firstname}} {{lastname}} in our team.”
    msgstr “Accueillons M. {{firstname}} {{lastname}} dans notre équipe.”

    msgid “Password:”
    msgstr “Mot de passe :”

    msgid “Username:”
    msgstr “Nom d’utilisateur :”

    msgid “We want to test globalization and localization.”
    msgstr “Nous voulons tester la globalisation et la localisation.”

    msgid “Welcome ladies and gentlemen!”
    msgstr “Bienvenue mesdames et messieurs !”

    msgid “one task”
    msgid_plural “{{count tasks}}”
    msgstr[0] “une tâche”
    msgstr[1] “{{count}} tâches”

    • gabrielschenker

      Merci beaucoup pour ton réponse interessante. Francais c’est que ma deuxième langue. C’est toujours bien d’apprenre quelque chose nouveau.

  • pizzafroide

    Oops, HTML tags have been interpreted.

    A better example would be:
    “<span translate translate-n=”count” translate-plural=”John gets assigned {{count}} tasks”>John gets assigned one task</span>”.

  • LordPayso

    How do you mark text for translation in services or factories?

    In my httpinterceptor when there is a response error I popup an alert to informt the user why there has been an error. Seems I cannot inject gettextCatalog as a dependency?

    • gabrielschenker

      This is a good question for stackoverflow…

    • http://outofmemoryexception.wordpress.com Eric Dorothy

      We have created a lang.js file that has been added to the GruntFile. This will enable you to embed your error messages so they can be translated on the client side. If you look at this post: http://outofmemoryexception.wordpress.com/2014/04/02/localization-with-angular-gettext/ Your error messages would be the “product keys” if you catch my meaning. Ping me if you have questions.

  • Vahid

    Thanks for such a great info on i18n.
    I followed the rules and made it work perfectly.
    The question is what if we have “Multiple” templates? I am developing an AngularJS app containing different views (partial templates), connected together with routing facilities…
    In one html page that is fine, we use “ng-model”, but what if we want to have a Cross-app translation, moving from a view to another view ??

  • Vaughan Jackson

    Great article. An update regarding the angular-translate URL. I think this has now changed to http://angular-translate.github.io/.

  • Sandor Berki

    Thank you very much for this great article.
    I have never used Grunt before, but it was pretty easy to put everything together.

    I have found out that there is also support to change the language on the fly without a page reload. In the changeLanguage function of the controller simply call “gettextCatalog.setCurrentLanguage($scope.lang);”
    instead of “gettextCatalog.currentLanguage = $scope.lang;” This broadcasts a gettextLanguageChanged event and this will result in an update on the translate directive.

  • Joachim Werner

    Fantastic.
    As I am pretty new to angular, I am wondering how I could store the selected language and use it in all controllers. $cookies?
    Regards,
    Joachim