We recently had to localise one of the apps we’re building. Here’s how we tackled it

Background

A client had come to us wanting a site that would be available for both UK and US companies, and since the app deals with employment-related concepts, we had to deal with differing terminology – the British “CV” vs. the American “Résumé”, the UK using National Insurance numbers and the US using Social Security numbers, and so on.

So, not enough to warrant a complete overhaul of our views, but enough to mean we had to start looking at localising the app.

Some terminology before we start: “localisation” refers to the process of making your app translatable (i.e. moving strings to locale files), whereas “internationalisation” (i18n for short) is the process of providing translations for these localised strings.

ActiveRecord localisation

In our case, the best place to start localising was our database models. Two model class methods come in handy here – .model_name and .human_attribute_name.

ActiveRecord::Base.model_name

Calling .model_name on your model class returns an instance of ActiveModel::Name (which has various useful inflection methods as well), which you can then call #human on to obtain your model name. By default, this will just call #humanize on your class name, but you can override this by adding a translation:

For a Widget model:

class Widget < ActiveRecord::Base
end

and a locale file containing:

en:
  activerecord:
    model:
      widget: TranslatedWidget

we get:

Widget.model_name.human # => "TranslatedWidget"

ActiveRecord::Base.human_attribute_name

As well as the name of your model, you can provide translations for all your attributes:

en:
  activerecord:
    attributes:
      widget:
        title: Translated title

will give:

Widget.human_attribute_name(:title) # => "Translated title"

Again, this will just call #humanize on whatever you pass to .human_attribute_name if you haven’t provided a translation. In fact, the attribute doesn’t even need to be a database field – ActiveRecord will just try to find a matching translation entry for the attribute name. You can also namespace your attributes:

Widget.human_attribute_name("title.short")

and ActiveRecord will find the appropriate translation:

en:
  activerecord:
    attributes:
      widget:
        title:
          short: Translated title

View templates

Within your views and helpers, Rails gives you two useful helpers.

t (or translate) takes a string or symbol and returns the translation for that key. If you preface your key with a dot, Rails will lookup the translation using the path of the current view/partial (saving you on having type it all out!). So with a locale file like:

en:
  widgets:
    index:
      message: "Hello index template!"
    show:
      message: "Hello show template!"

the same call to t in different templates will give different results:

# app/views/widgets/index.html.erb
t(".message") # => "Hello index template!"

# app/views/widgets/show.html.erb
t(".message") # => "Hello show template!"

l (or localise) allows you to localise dates and times, specifying the format in a locale file. (in strftime format). In our case, our locale file looked something like this:

en:
  date:
    formats:
      my_format: "%d %B %y"

en-US:
  date:
    formats:
      my_format: "%B %d, %y"

so in our views:

I18n.locale = :en
l(Date.today, format: :my_format) # => "07 April 2014"

I18n.locale = :'en-US' # note that the locale has to be a symbol!
l(Date.today, format: :my_format) # => "April 07, 2014"

In addition, you can also localise validation errors, ActionMailer email subjects and even your submit buttons!

SimpleForm localisation

We’re big fans of SimpleForm at CookiesHQ, and it comes as no surprise that it comes with excellent support for localisation out of the box. The i18n lookup is structured much like that of ActiveRecord, and allows you to provide translations for labels, placeholders and hints.

Particulary useful is the ability to put common attribute names under a defaults key, saving you from having to repeat yourself across several models!

Locale file organization

Once you start localising, you’ll soon run into the problem of how to organise your locale files. Luckily, the i18n gem (the powerhouse behind Rails localisation) is pretty flexible, and will take any number of YAML files and combine them together to produce the giant nested hash used to lookup translations.

We decided to organise everything into separate folders per locale, with a base.yml file that defined ActiveRecord translations, then YAML files per product, as well as a shared.yml file for translations used across the different products. For us, that looked something like this:

app/config/locales
|__ en
|   |__ base.yml
|   |__ product1.yml
|   |__ product2.yml
|   |__ shared.yml
|
|__ en-US
    |__ base.yml
    |__ product1.yml
    |__ product2.yml
    |__ shared.yml

Notice that we use en as our default locale, rather than en-GB. This meant that: 1) we didn’t have to provide translations for everything in Rails and 2) that any translations not found in the en-US locale would use the en locale as a fallback.

Localising from the start

About the time we started localising the app, we had begun working on a related product for the same client. Given the time it took to localise the first app, we decided to localise everything from the start – every string in a view, helper or controller had to be in a locale file.

Although this process took a little longer (mostly thinking up suitable locale keys!), we found the process helped us to centralize all our domain terminology in one place, and keep everything consistent. If the client thought of a better term for something, or changed their mind, we only had one place to change it, rather than grepping through the entire codebase and potentially missing something.

Even for apps intended for a single country, localising your strings is definitely worth considering, as it goes a long way to DRYing up your views and controllers, and helps keep your domain terms consistent. To my eye, it also makes for cleaner-looking views, though potentially at the cost of another layer of redirection – having to look up in a locale file where a string in your browser is coming from. I’ve found this can be mitigated, however, with a well-thought-through locale file structure (that your team agrees on upfront), as well as using clear (if slightly verbose) locale keys.

Whether we start localising new apps from the start will depend on the nature of the app, but we’ll definitely be doing it for all apps that have a potentially international audience, even if it’s just across the Atlantic!

Photo credit: Ron Lute (CC BY-NC 2.0)