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
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
To install Bootstrap (which we use for our layout) locally enter
Finally we want to install the angular-gettext module, thus enter
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
This uses npm to install the Grunt command line interface. Now we install Grunt locally with this command
and then we also need the node module containing the Grunt tasks for text extraction and compilation
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
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
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.
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
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
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
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
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
If we have done everything right then we should see the following output
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
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
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
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.
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
Again, if we did everything correctly the console output should show this
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).
Back in our index.html we have to include the above file.
And as a last step in the app.js file we have to declare that our app module depends on the gettext module.
Go back to your browser and refresh the page. Hurray our text has been translated!
Let’s now see how the translation plugin handles missing translations. Add another paragraph to the template
After saving refresh the browser and you should see this
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
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
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.
In the above case the filter translate is applied to the text Enter your username.
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
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
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
The new not yet translated texts are in bold. Translate the values as follows
After saving we need to update our controller as follows
and now you can display the page in the browser
If we now change the model value count to say 5 we get this
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
Also add a function to the controller to change the current language
In order to make this function work we have to inject the gettextCatalog service into our controller.
Add a drop-down bound to the languages array to the view
This is the result when selecting French as the target language