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)