This guide will give you an overview of the problems you face when writing an app that's used in many countries, in many languages. After reading it you should be familiar with:
You can use the Demo App to try out the concepts step by step.
Slides - use arrow keys to navigate, esc to return to page view, f for fullscreen
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).
When developing web applications for different countries, cultures, languages we are faced with many different problems:
Internationalization is mostly concerned with language and translation. Even if we focus on indo-european languages we will find differences:
Going from a mono-lingual to a multi-lingual app will add another layer of abstraction that will make both programming and testing harder.
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:
and so on.
Inside the app you can always read the current language from I18n.locale
.
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.
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!
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:
Rails provides several helper methods for constructing numbers:
number_with_delimiter
- for large numbers like 100.000.000
number_to_currency
- includes the currency like 100.000.000,00 €
number_with_precision
- like 100000000,00
number_to_percentage
- like 90,00 %
number_to_human_size
- for bytes, like 95,4 MB
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>
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!
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>
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:
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>
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.
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
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)
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:
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"
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