Localization in Action, part 3 of 3 – A Node.js holiday season, part 11

This is episode 11, out of a total 12, in the A Node.JS Holiday Season series from Mozilla’s Identity team. It’s the last part about localization, hopefully making you feel all ready to handle that now!

Using Our Strings

So first we added the i18n-abide module to our code, then our Localization (L10n) team did some string wrangling, now we’ve got several locales with translated strings…

Let’s get these strings ready for Node.js and see this puppy in action!

The next step is that we’ll need your PO files, typically in a file system like this:

locale
  en
    LC_MESSAGES
      messages.po
  de
    LC_MESSAGES
      messages.po
  es
    LC_MESSAGES
      messages.po

We need a way to get strings from our PO files into our application at runtime. There are a few ways you can do this:

  1. Have server side strings and the gettext function provided by i18n-abide will work it’s magic.
  2. Have client side strings and you’ll include a gettext.js script in your code. This is distributed with i18n-abide.

Both of these methods require the strings to be in a JSON file format.
The server side translation loads them on application startup, and the client side translation loads them via HTTP (or you can put them into your built and minified JavaScript).

Since this system is compatible with GNU Gettext, a third option for server side strings is to use node-gettext. It’s quite efficient for doing server side translation.

We’ll use the first option in this blog post, as it is the most common way to use i18n-abide.

compile-json

So, how do we get our strings out of the PO files and into JSON files?

Our build script is called compile-json.

Assuming out files are in a top level directory locale of our project, and we want the .json files to go into static/i18n, we’d do this:

Example:

$ mkdir -p static/i18n
$ ./node_modules/.bin/compile-json locale static/i18n

And we get a file structure like:

static
  i18n
    en
      messages.json
      messages.js
    de
      messages.json
      messages.js
    es
      messages.json
      messages.js

compile-json loops over each of our .po files and calls po2json.js on it, producing .json and .js files. po2json.js is another program provided by i18n-abide.

If we take the Spanish messages.po we have so far from these blog posts, we’d see:

# Spanish translations for PACKAGE package.
# Copyright (C) 2013 THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Austin King <ozten@localhost>, 2013.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSIONn"
"Report-Msgid-Bugs-To: n"
"POT-Creation-Date: 2012-06-24 09:50+0200n"
"PO-Revision-Date: 2013-04-24 16:42-0700n"
"Last-Translator: Austin King <ozten@nutria.localdomain>n"
"Language-Team: Spanishn"
"Language: esn"
"MIME-Version: 1.0n"
"Content-Type: text/plain; charset=UTF-8n"
"Content-Transfer-Encoding: 8bitn"
"Plural-Forms: nplurals=2; plural=(n != 1);n"
 
#: /home/ozten/abide-demo/views/homepage.ejs:3
msgid "Mozilla Persona"
msgstr "Mozilla Personidada"

which would be converted into

messages": {
      "": {
         "Project-Id-Version": " PACKAGE VERSIONnReport-Msgid-Bugs-To: nPOT-Creation-Date: 2012-06-24 09:50+0200nPO-Revision-Date: 2013-04-24 16:42-0700nLast-Translator: Austin King <ozten@nutria.localdomain>nLanguage-Team: GermannLanguage: denMIME-Version: 1.0nContent-Type: text/plain; charset=UTF-8nContent-Transfer-Encoding: 8bitnPlural-Forms: nplurals=2; plural=(n != 1);n"
      },
      "Mozilla Persona": [
         null,
         "Mozilla Personidada"
      ]
   }
}

So we can use these .json files server side form Node code, or client side by requesting them via AJAX.

The static directory is exposed to web traffic, so a request to /i18n/es/messages.json would get the Spanish JSON file.

This static directory is an express convention, you can store these files where ever you wish. You can serve up static files this via Node.js or a web server such as nginx.

Note: You don’t need the .PO files to be deployed to production, but it doesn’t hurt to ship them.

Configuration

i18n-abide requires some configuration to decide which languages are supported and to know where to find our JSON files.

As we saw in the first installment, here is the required configuration for our application

app.use(i18n.abide({
  supported_languages: ['en-US', 'de', 'es', 'zh-TW'],
  default_lang: 'en-US',
  translation_directory: 'static/i18n'
}));
  • supported_languages tells the app that it supports English, German, Spanish, Chinese (Traditional).
  • The translation_directory config says that the translated JSON files are under static/i18n.
  • Note that translation_directory is needed for server side gettext only.

We explained in the first post that i18n-abide will do its best to serve up an appropriate localized string.

It will look at supported_languages in the configuration to find the best language match.

You should only put languages in supported_languages, where you have a locale JSON file ready to go.

Start your engines

Okay, now that configs are in place and we have at least one locale translated, let’s fire it up!

npm start

In your web browser, change your preferred language to one which you have localized.

Now load a page for your application. You should see it translated now.

For a real world example, here is Mozilla Persona in Greek. So, cool!

gobbledygook

If you want to test your L10n setup, before you have real translations done, we’re built a great test locale. It is inspired by David Bowie’s Labyrinth.

To use it, just add it-CH or another locale you’re not currently using to your config under both supported_languages as well as the debug_lang setting.

Example partial config:

app.use(i18n.abide({
  supported_languages: ['en-US', 'de', 'es', 'zh-TW', 'it-CH'],
  debug_lang: 'it-CH',
  ...

Now if you set your browser’s preferred language to Italian/Switzerland (it-CH), i18n-abide will use gobbledygook to localize the content.

This is a handy way to ensure your visual design and prose work for bi-directional languages like Hebrew. Your web designer can test their RTL CSS, before you have the resources to create actual Hebrew strings.

Going Deeper

We’ve just scratched the surface of i18n and l10n. If you ship a Node.js based service in multiple locales, you’ll find many gotchas and interesting nuances.

Here is a heads up on a few more topics.

String interpolation

i18n-abide provides a format function which can be used in client or server side JavaScript code.

Format takes a formatted string and replaces parameters with actual values at runtime. This function can be used in one of two flavors of parameter replacements.

Formats

  • %s – format is called with a format string and then an array of strings. Each will be replaced in order.
  • %(named)s – format is called with a format string and then an object where the keys match the named parameters.

You can use format to keep HTML in your strings to a minimum.

Consider these three examples

<%= gettext('<p>Buy <a href="/buy?prod=blue&amp;tyep=ticket">Blue Tickets</a> Now!</p>') %>
<p><%= format(gettext('Buy <a href="%s">Blue Tickets</a> Now!'), ['/buy?prod=blue&amp;tyep=ticket']) %></p>
<p><%= format(gettext('Buy <a href="%(url)s">Blue Tickets</a> Now!'), {url: '/buy?prod=blue&amp;tyep=ticket'}) %></p>

In the PO file, they produce these strings:

<p>Buy <a href="/buy?prod=blue&amp;tyep=ticket">Blue Tickets</a> Now!</p>"
msgid "Buy <a href="%s">Blue Tickets</a> Now!"
msgid "Buy <a href="%(url)s">Blue Tickets</a> Now!"

The first example has a paragraph tag that shows up in the PO file. Yuck. If you ever change the markup… you may have to update it in every locale!

Also, look at that ugly URL.

Reasons to use format:

  • Avoid confusing localizers who aren’t familiar with HTML and may break your code accidentally
  • Avoid maintenance issues

The named parameters are nice, in that they are self documenting. The localizer knows that the variable is a URL. String interpolation is quite common in localizing software.

Another example is runtime data injected into your strings.

<p><%= format(gettext('Welcome back, %(user_name)s'), {user_name: user.name}) %></p></p>

Avoid Inflexible Designs

We need to put our L10n hats on early. As early as when we review the initial graphic design of the website.

  • Avoid putting text into images. Use CSS to keep words as plain text positioned over images.

  • Make sure CSS is bulletproof. An English word in German can be many times larger and destroy a
    poorly planned design.

  • Try this bookmarklet: Fauxgermanhausen das Pagen!

Database-backed websites have already taught us to think about design in a systematic way, but designers may not be used to allowing for variable length labels or buttons.

String Freeze

Remember our build step to prepare files for localizers to translate? And in this post we learned about po2json.js for using these strings in our app… Well, this means we’re going to need to coordinate our software releases with our L10n community.

Continuous deployment isn’t a solved problem with L10n. Either you have to block on getting 100%
of your strings translated before deploying, or be comfortable with a partially translated app in some locales.

L10n teams may need 1, 2 or even 3 weeks to localize your app, depending on how many strings there are. Schedule this to happen during the QA cycle.

Provide a live preview site, so that localizers can check their work.

Wrapping up

In these three blog posts, we’ve seen how to develop a localized app with i18n-abide, how to add a L10n phase to our release build, and lastly, how to test our work.

Localizing your website or application will make your site valuable to an even larger global audience.

Node.js hackers, go make your services accessible to the World!

Previous articles in the series

This was part eleven in a series with a total of 12 posts about Node.js. The previous ones are:

About Austin King

Seattle based non-dogmatic Artist / Programmer type human. Rogue web developer with the Apps Engineering team. Spell check is for the week.

More articles by Austin King…

About Robert Nyman [Editor emeritus]

Technical Evangelist & Editor of Mozilla Hacks. Gives talks & blogs about HTML5, JavaScript & the Open Web. Robert is a strong believer in HTML5 and the Open Web and has been working since 1999 with Front End development for the web - in Sweden and in New York City. He regularly also blogs at http://robertnyman.com and loves to travel and meet people.

More articles by Robert Nyman [Editor emeritus]…


5 comments

  1. Álvaro G. Vicario

    The bookmarklet triggers this:

    Error: SyntaxError: missing ) after argument list
    athResult=document.evaluate(‘.//text()[normalize-space(.)!=”]’,document.body,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYP

    April 30th, 2013 at 12:02

  2. Austin King

    Sorry about that Alvaro, here is the raw Bookmarklet
    https://gist.github.com/ozten/5492240

    I’ll try to get this post fixed as well.

    April 30th, 2013 at 14:55

  3. Edgar Orozco

    Austin thanks for this articles. I have a express 3, ejs, passport app and i want to localize the strings for the auth in passport this strings are out of the scope of the request nor the template. How i can implement localization in this scenario?

    I try to use gettext with a variable but i have this error:
    >> 9|
    Cannot read property ‘length’ of undefined
    at Object.gt [as gettext] (/home/workspace/sf2/example/node_modules/i18n-abide/lib/i18n.js:143:70)

    May 11th, 2013 at 01:26

  4. Austin King

    Edgar – I’d love to understand your use case better. Also, it sounds like a bug in i18n-abide. Can you put details of how to reproduce this in https://github.com/mozilla/i18n-abide/issues/new

    If you’re code is public, a pointer to it would be helpful in the bug, also.

    thanks for helping with i18n-abide.

    May 11th, 2013 at 07:19

    1. Edgar Orozco

      Austin i filled out the issue
      https://github.com/mozilla/i18n-abide/issues/32

      Thanks!

      May 13th, 2013 at 11:12

Comments are closed for this article.