#138 I18n (revised)
- Download:
- source codeProject Files in Zip (85.2 KB)
- mp4Full Size H.264 Video (27.9 MB)
- m4vSmaller H.264 Video (12.1 MB)
- webmFull Size VP8 Video (14.3 MB)
- ogvFull Size Theora Video (28.3 MB)
Below is a page from an application which has some static text at the top. We want to translate this text into other languages and allow the user to select the language they want to view it in and there’s a placeholder in the top righthand corner of the page where the language options will be displayed.
This is known as internationalization (or i18n for short). To make this page display text in different languages we’re going to have to find the static text we want to internationalize in our view template and extract it out so that it can be loaded dynamically based on the user’s selected language. This is easy to do in Rails by replacing each piece of static text with a call to the t
method and passing in a unique key to identify that text. We’ll give our heading the key products.title
.
<h1><%= t "products.title" %></h1> <em>Thank you for visiting our store. Now take out your wallet and buy something.</em>
If we reload the page now the page heading will just say “Title” and if we look at the source we’ll see that there’s a span
element with a class of translation_missing
.
<h1><span class="translation_missing" title="translation missing: en.products.title">Title</span></h1>
The span’s title
attribute tells us that the translation is missing for the en.products.title
key. We need to add a translation for this key. By default translations are stored in YAML files under the application’s config/locales
directory and in a there’s already one for English. There’s a default “Hello World” translation in this file; we’ll remove this and replace it with our products title. Note that the full stop in the key is represented with nesting in the YAML file.
# Sample localization file for English. Add more files in this directory for other locales. # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. en: products: title: "Browse Products"
When we reload the page now the title has returned to “Browse Products”.
If the text we want to translate is unique to a template we can start the key with a full stop and we’ll do that now to the title and description on the page.
<h1><%= t ".title" %></h1> <em><%= t ".description" %></em>
This key will be prefixed with the path to the template so the key for the description will become products.index.description
. We’ll need to move the key for these items now so that they’re nested under en.products.index
.
en: products: index: title: "Browse Products" description: "Thank you for visiting our store. Now take out your wallet and buy something."
Adding Other Languages
Now both pieces of text come from the YAML file and we can easily display this text in more languages by adding new YAML files. Before we do this we’ll add links at the top of the page for changing the current language by replacing the placeholder text in the layout file with links from swapping the language between English and Wookieespeak. We’ll use the locale code wk
for Wookieespeak.
<!DOCTYPE html> <html> <head> <title>Store</title> <%= stylesheet_link_tag "application", media: "all" %> <%= javascript_include_tag "application" %> <%= csrf_meta_tag %> </head> <body> <div id="container"> <div id="nav"> <%= link_to_unless_current "English", locale: "en" %> <%= link_to_unless_current "Wookieespeak", locale: "wk" %> </div> <% flash.each do |name, msg| %> <%= content_tag :div, msg, id: "flash_#{name}" %> <% end %> <%= yield %> </div> </body> </html>
Reloading the page now shows the two links and clicking one of them will set the locale in the URL’s querystring.
We need to check for this parameter and set the language based on it’s value. We’ll do this in a before filter in our ApplicationController
so that it happens before each action.
class ApplicationController < ActionController::Base protect_from_forgery before_filter :set_locale private def set_locale I18n.locale = params[:locale] if params[:locale].present? end end
In this action we set the current language by setting I18n.locale
and we set this to the value in the locale
parameter, if that parameter is present. There are other ways that we can use to determine the user’s locale. If our application allows users to login we could add a locale property to the User
model and use something like current_user.locale
to fetch the locale. Alternatively we could use a different subdomain for each locale and set the locale from that with request.subdomain
. We could also use the user’s preferred language from their browser by reading request.env["HTTP_ACCEPT_LANGUAGE"]
or determine it from the remote IP address using a Geolocation service. Once we’ve set the locale Rails will look for a matching YAML file. We’ll need to create a new wk.yml
file for Wookieespeak with the same keys as the English one.
wk: products: index: title: "Wyaaaaaa Ruh" description: "Huwaa muaa mumwa. Wua ga ma ma ahuma ooma."
We’ll need to restart our application for the new file to be picked up. When we do then reload the page and click the “Wookieespeak” link we’ll see the translated text.
Persisting The Selected Language
The locale
parameter won’t persist between page requests so if we visit another page then click back the language reverts back to the default. We can persist the selected language in a number of ways, for example by storing it in a cookie, but in this episode we’ll persist it in the URL and make the first part of the domain’s path hold the language. This means that the page above in Wookieespeak would have a path of /wk/products
. Changing anything to do with URLs generally means modifying the routes file and that’s what we’ll do here. Our routes files currently defines two routes: a products
resource and root route that points to the ProductsController
’s index
action.
Store::Application.routes.draw do resources :products root to: 'products#index' end
We’ll use the scope method to scope the locale parameter in before these routes.
Store::Application.routes.draw do scope ":locale" do resources :products root to: 'products#index' end end
Each route will now be prefixed with a locale scope. If we visit /wk/products
now, though, we’ll get a routing error.
Our application seems to be trying to visit the show page for a product and failing. The problem is that the product is being passed in as the locale
option. There’s a method that we can define in the ApplicationController
to help with this called default_url_options
. Rails will call this method to determine the default options that should be passed in to URL generators. We need to set the locale
option so that this is automatically set whenever a URL is generated.
def default_url_options(options = {}) {locale: I18n.locale} end
When we reload the page now it works as we expect it to as it no longer has a problem generating the URLs for the links for each product. The current locale is now correctly added to each generated URL and so the selected locale persists across pages. If we try visiting a page without entering a locale in the URL the localized text doesn’t display correctly as our application thinks that the first part of the path, in this case /products
, is the locale and so it tries to find a products.yml
file and fails.
We need to make this :locale
option more restrictive and we can do so by making sure that it matches one of a given set of values. We could hard-code a list of locales here but that would mean that we’d need to change the routes file each time we add a new language but instead we can use I18n.available_locales
. This will return an array so we’ll need to join its values with a pipe to generate the correct regular expression.
Store::Application.routes.draw do scope ":locale", locale: /#{I18n.available_locales.join("|")}/ do resources :products root to: 'products#index' end end
When we visit /products
now we’ll be redirected to /en/product
s as this is the default locale.
Localizing Model Attributes
There’s some text on the page that we haven’t translated yet, for example the word “Price” next to the price of each item. We’ll translate this now and while we could use the t
method again there’s a better way to do this. As we’re translating the name of a model’s attribute, in this case the Product
model’s price
we can use human_attribute_name
.
<div class="details"> <%= Product.human_attribute_name(:price) %> <%= number_to_currency(product.price) %> </div>
This will provide an English default but still allow us to override this text in other languages. We’ll need to provide the Wookieespeak for “Price” in the wk.yml
file as as we’re localizing an ActiveRecord attribute we’ll need to nest this text under a different location. It needs to go under activerecord
, attributes
followed by the name of the model and the attribute.
wk: products: index: title: "Wyaaaaaa Ruh" description: "Huwaa muaa mumwa. Wua ga ma ma ahuma ooma." activerecord: attributes: product: price: "Aurhaa"
When we reload the page now we should see the translated text.
The nice thing about this is that it will show up anywhere we display the attribute so if we go to edit a product now we’ll see that the label for the price field is already translated.
Example YAML Files For Common Translations
There are many examples of what we can do with locale files inside the Github project that’s linked to in the comments at the top of the en.yml file. Here we’ll find a long list of example locale files that make a great starting point. These include useful translations for date and month names along with time formats, error messages, number and currency formatting and much more.
Sometimes managing translations through YAML files can be a pain. In these cases it would be good to have an alternative back end with a nice UI to manage it. There are a number of tools that do this such a Copycopter which we demonstrated in episode 336. If you’d rather make your own backend and admin interface take a look at episode 256.One thing we haven’t shown in this episode is how to translate the text in the database such as the name of each product. The Globalize3 gem, covered in episode 338, demonstrates how to do this. Finally you should take a look at the internationalization section of the Rails Guides which covers a lot of details that we haven’t shown here.