1 Internationalization and Localization
Internationalization is the process of designing a software application so that it can be adapted to various languages and regions. It is often written as I18n (to avoid typing 18 letters).
Localization is the process of adapting internationalized software for a specific region or language by translating text and adding locale-specific components. It is often written as L10n (to avoid typing 10 letters).
Localization (which is potentially performed multiple times, for different locales) uses the infrastructure or flexibility provided by internationalization (which is ideally performed only once before localization, or as an integral part of ongoing development).
2 The big picture
When developing web applications for different countries, cultures, languages we are faced with many different problems:
- Different cultures will have very different ideas of what a "good" website looks like. See Jenny Shen's talk at concat
- Different writing systems will use a diffrent amount of space, leading to diffrent layouts
- Writing direction (left-to-right, right-to-left or top-down) will need to be handled by HTML. See w3c
- The app might need to handle different currencies and methods of payment
3 Focus on Language
Internationalization is mostly concerned with language and translation. Even if we focus on indo-european languages we will find differences:
- Example: Plurals in Polish Polnisch für Anfänger by stach_mat, Teil 5
Going from a mono-lingual to a multi-lingual app will add another layer of abstraction that will make both programming and testing harder.
4 I18n in Rails
Rails Apps are ready for Internationalization. To get started,
find the file config/locale/en.yml
. It only contains one example
of a translation string:
en:
hello: "Hello world"
This means, that in the :en
locale, the key hello
will map to the Hello world
string
The Demo App is already prepared to switch between locales through folders:
- http://localhost:3000/ - Home page and order page - english
- http://localhost:3000/de/ - Home page and order page - german
- http://localhost:3000/dk/ - Home page and order page - danish
- http://localhost:3000/es/ - Home page and order page - spanish
- http://localhost:3000/shirts - List of all shirts - english
- http://localhost:3000/de/shirts - List of all shirts - english
- http://localhost:3000/dk/shirts - List of all shirts - danish
- http://localhost:3000/en/shirts - List of all shirts - spanish
and so on.
Inside the app you can always read the current language from I18n.locale
.
5 keys and translations
The method I18n.translate
or I18n.t
can be used to look up translation.
In our example before the key was hello
. In english t 'hello'
will return "Hello World"
and in German it would be "Hallo Welt".
In the view we can use the even short form:
<%= t 'hello' %>
If no translation can be found - because there is no yaml file or because
the key is missing, then a span with class translation_missing
will be wrapped
around the text. The key itself will be used in place of a translation:
<span class="translation_missing" title="translation missing: dk.hello">Hello</span>
Texts in views all need to be replaced by translation keys.
6 locale files included with the I18n gem
The I18n gem comes with translations for many languages in the locale folder.
You can copy the ones you need to your own apps config/locale
folder
to use them.
If you are following along with the demo app, do this now for english, german, danish and spanish!
7 Numbers
There are different Rules for displaying numbers in different languages. You may be familiar with the European Million being translated into an US Billion. So different words are used for numbers.
But also the numbers themselves are formatted differently:
<p><%= number_with_delimiter(100000000) %></p>
In many languages this will be displayed as 100.000.000
or 100,000,000
.
Some examples of languages with different number formats are:
- Indian Numbering System: 10,00,00,000 or 10 lakh.
- Japanese numerals 1 0000 0000 or 1億.
Rails provides several helper methods for constructing numbers:
number_with_delimiter
- for large numbers like100.000.000
number_to_currency
- includes the currency like100.000.000,00 €
number_with_precision
- like100000000,00
number_to_percentage
- like90,00 %
number_to_human_size
- for bytes, like95,4 MB
8 Dates
Dates are a special case for translation: there are complex and different rules for displaying dates in different languages.
The page /order_items
gives a list of all orders, with a timestamp when each order was placed:
<p>
<strong>Order Date:</strong>
<%= order_item.created_at %>
</p>
We can use the method I18n.localize
or I18n.l
in the view to
display this in a language appropriate format:
<p>
<strong>Order Date:</strong>
<%=l order_item.created_at %>
</p>
In German this will be displayed as:
<p>
<strong>Order Date:</strong>
Montag, 01. April 2024, 21:25 Uhr
</p>
8.1 Format definitions
You can find the definition for this date in de.yml
under de.time.formats.default
:
time:
formats:
default: "%A, %d. %B %Y, %H:%M Uhr"
long: "%A, %d. %B %Y, %H:%M Uhr"
short: "%d. %b, %H:%M Uhr"
To choose another format, you can add call l
with the format option:
<p>
<strong>Order Date:</strong>
<%=l order_item.created_at, format: :short %>
</p>
Different languages will have different format, here a comparison of :short
:
en:
short: "%d %b %H:%M"
es:
short: "%-d de %b %H:%M"
da:
short: "%e. %b %Y, %H.%M"
de:
short: "%d. %b, %H:%M Uhr"
As you can see, it is not only the names of weekdays and months that are translated, but also the whole format changes from language to language.
You could add other formats than ':long' and ':short' to your translations
by adding to the .yml
files. Just make sure to keep the keys consistent through all languages!
8.2 Relative Dates
The helper time_ago_in_words
is already localized, you
can use it directly:
<p>
<strong>Order Date:</strong>
<%= time_ago_in_words order_item.created_at %>
</p>
9 Pluralization
The page /order_items
gives a list of all orders, and also the number of orders.
Before internationalization this looked like this:
<p>There are <%= @order_items.length %> orders in the system.</p>
When translating this we also have to think about pluralization rules. English is simple: just add an 's' to the end of the noun for more then one order. Other languages follow more complex rules.
You can see how the example from polish in the video above is handled in the standard translation for dates:
- Rails Pluralisation Rules for Polish pluralization/pl.rb
- Use in locale/pl.rb
To build our own pluralisation rules we can add translations with several cases. How many cases there are, and what they are called, differs from language to language. Make sure to look it up in pluralization/*
orders_in_the_system:
zero: Es gibt keine Bestellungen im System
one: Es gibt eine Bestellung im System
other: Es gibt %{count} Bestellungen im System
In the view you use it like this:
<p><%=t('orders_in_the_system', count: @order_items.length) %></p>
10 Translating Model Names and Attributes
The names of models and their attributes will be used in many places in the app: from labels for form fields to error messages in validation.
So it makes sense to store the names of models and their attributes just once and then reuse them.
In the demo app there are three models: Shirt, Statement and OrderItem. This is how to specify translations for shirts:
activerecord:
models:
shirt:
one: Hemd
other: Hemden
attributes:
shirt:
sizes:
one: Größe
other: Größen
colors:
one: Farbe
other: Farben
To refer to a model use .model_name.human
, for example Shirt.model_name.human
.
To refer to an attribute use human_attribute_name
, for example Shirt.human_attribute_name("colors").
Both methods can take an attribute
count` for pluralization.
10.1 Automatic texts for models
The combination of model names and prepared translations from the locale folder of the I18n gem is quite powerful.
For example the helper for building labels automatically uses human_attribute_names:
<%= form.label :sizes %>
When using a the submit button in a form builder:
<%= form_for @shirt do |f| %>
<%= f.submit %>
<% end %>
The label will automatically be generated from these keys:
en:
helpers:
submit:
create: "Create a %{model}"
update: "Confirm changes to %{model}"
so
<%= f.submit %>
is a short version for
<%= form.submit t('helpers.submit.create', model: Shirt.model_name.human ) %>
Source: Ruby on Rails Documentation, Action View Form Builder
10.2 Automatic texts for validations and error messages
Setting the name for models and attributes is enough to translate validations and their error messages:
class Shirt
validates :name, presence: true, length: { minimum: 3 }
Will automatically generate error messages like:
Name muss ausgefüllt werden Name ist zu kurz (weniger als 3 Zeichen)
11 Text stored in tables
The demo app is a shop for shirts with slogans printed on them. In the first implementation all the slogans are in english.
When we think about selling to different markets we might want to translate text stored and tables for different purposes:
- translate the shirts names, for example "generic t-shirt" and "fine t-shirt"
- keep the english slogans, but provide translation into several languages to make searching easier
- for example when you search for "freie software" in the app, you should find the slogans related to "free software"
- offer slogans in different languages
- should these be different products with different ids? or should there be several columns for one product?
11.1 the Mobility gem
The gem Mobility adds translations for table data. In the demo app Mobility is already installed and configured.
To make the name of a shirt translatable we add the following lines to the model:
extend Mobility
translates :name, type: :string
From now on the gem will enable us to store different names. For example in the seed file we could do this:
I18n.locale = :en # switch to english
s1 = Shirt.create!(
name: 'Polo Shirt',
outline: File.read(Rails.root.join('db/seeds/Shirt-type_Polo-omg.svg')),
colors: '#e0ffcd #fdffcd #ffebbb #ffcab0 white',
sizes: 'S M L XL XXL'
)
I18n.locale = :de # switch to german
s1.update(name: 'Polohemd')
Now every time the object is read from the database and the name is retrieved, the locale is used. For example in the rails console this could look like this:
irb(main):001> s1 = Shirt.first
Shirt Load (1.5ms) SELECT "shirts".* FROM "shirts" ORDER BY "shirts"."id" ASC LIMIT $1 [["LIMIT", 1]]
=>
#<Shirt:0x00000001271fc570
...
irb(main):002> s1.name
Mobility::Backends::ActiveRecord::KeyValue::StringTranslation Load (1.3ms) SELECT "mobility_string_translations".* FROM "mobility_string_translations" WHERE "mobility_string_translations"."translatable_id" = $1 AND "mobility_string_translations"."translatable_type" = $2 AND "mobility_string_translations"."key" = $3 [["translatable_id", 1], ["translatable_type", "Shirt"], ["key", "name"]]
=> "Polo Shirt"
irb(main):003> I18n.locale = :de
=> :de
irb(main):004> s1.name
=> "Polohemd"
12.1 url and path helpers
A first hurdle when running your test might be generating the right urls and paths. This happens in both controller tests and system tests. The errors might look something like this:
Error:
StatementsTest#test_should_destroy_Statement:
ActionController::UrlGenerationError: No route matches {:action=>"show", :controller=>"statements", :locale=>#<Statement ...>}, missing required keys: [:id]
test/system/statements_test.rb:42:in `block in <class:StatementsTest>'
bin/rails test test/system/statements_test.rb:41