#256 I18n Backends
- Download:
- source codeProject Files in Zip (99.2 KB)
- mp4Full Size H.264 Video (19.2 MB)
- m4vSmaller H.264 Video (12.4 MB)
- webmFull Size VP8 Video (28.2 MB)
- ogvFull Size Theora Video (26.1 MB)
The topic of internationalization was last covered in episode 138 [watch, read]. Por padrão, o Rails guarda as informações sobre internacionalização em arquivos YAML, mas neste episódio mostraremos a você como usar um diferente back-end para isso.
Abaixo está uma página de um site em Rails muito simples que queremos internacionalizar o texto do cabeçalho.
É fácil internacionalizar um pedaço de texto em uma aplicação Rails usando o método tradução, t
, mas então precisamos editar um arquivo YAML para cada idioma que nossa aplicação suporta e adicionar uma chave e o texto para aquele idioma. Para aplicações grandes, isto pode rapidamente se tornar tedioso e, de qualquer maneira, não é realmente o trabalho do desenvolvedor escrever todo o texto internacionalizado. Seria muito melhor ofereceer uma interface web que permite ao administrador da aplicação adicionar e editar o texto internacionalizado.
Thankfully the internationalization gem supports several different backends which means that we’re not restricted to using YAML, we can choose to use any database backend we want. By default Rails uses a Simple backend which handles loading the YAML files and managing the translations through them. The backend we’ll focus on in this episode is the Key-Value backend which allows us to use any key-value store for managing the translations. There is also an ActiveRecord store which has been extracted out into a separate gem. This works but as the translations on the page are accessed frequently in every page of the application, ActiveRecord isn’t really the best approach. We really want the translations in memory rather than in a SQL database. We could use caching to get around this issue but then we need to worry about expiring the cache when the translations are modified. For these reasons a key-value store is the way to go and its this approach that we’ll be demonstrating in this episode.
Mudando o Back-end
Primeiro precisamos mudar o texto estático no cabeçalho para que ele use uma tradução no lugar. Vamos apontar para uma chave chamada welcome
.
<h1><%= t('welcome') %></h1>
Então podemos adicionar o texto traduzido no arquivo YAML em inglês.
en: welcome: "Welcome"
Se nós atualizarmos a página agora, veremos o texto do arquivo YAML sendo mostrado.
Agora que temos algum texto traduzido usando o back-end padrão, vamos mudar ele. No topo do arquivo key_value.rb
estão alguns cometários explicando como ele deve ser usado, incluindo um exemplo de como implementar um back-end alternativo.
# I18n.backend = I18n::Backend::KeyValue.new(Rufus::Tokyo::Cabinet.new('*'))
We need to create a new I18n::Backend::KeyValue
and pass in the key-value store that we want to use. This will then be used as the backend for the translations. The store needs to be able to respond to three methods, one to get a key’s value, one to set it and one to list all of the keys. These are shown in the comments. Most key-value stores in Ruby support these methods and so we can use one them out of the box.
# * store#[](key) - Used to get a value # * store#[]=(key, value) - Used to set a value # * store#keys - Used to get all keys
We have enough information now to start moving our application’s internationalization backend so let’s begin. First we’ll create a new file in the /config/initializers
directory called i18n_backend.rb
.
I18n.backend = I18n::Backend::KeyValue.new({})
The example in the comments uses a Tokyo Cabinet backend but to get the application up and running we’ve used an empty hash. Obviously we wouldn’t do this in a real application but as it’s the simplest thing that supports the three methods listed above it’ll do for demonstration purposes. If we restart our application now and go to the home page we’ll see that the title doesn’t look right any more.
If we look at the source code we’ll see that we now have a missing translation.
<h1><span class="translation_missing">en, welcome</span></h1>
As the application is now using its new backend, the translation is marked as missing. Even though it’s still there in the YAML file, it’s not in our new backend. We’ll create the web interface now to enable users to add translations to the new backend. This will all be handled by a new TranslationsController
with one action, index
.
$ rails g controller translations index
We want this new controller to behave like a resource so we’ll replace the generated route (get "translations/index"
) in the routes file with a call to resources
.
Intn::Application.routes.draw do resources :translations root :to => "home#index" end
In the index
action we want to display the existing translations and so we’ll need to get the translations hash from the new backend store. We can do that by called I18n.backend.store
.
class TranslationsController < ApplicationController def index @translations = I18n.backend.store end end
In the view we can then iterate through the translations and display them.
/app/views/translations/index.html.erb
<h1>Translations</h1> <ul> <% @translations.each do |key, value| %> <li><%= key %>: <%= value %></li> <% end %> </ul>
This code loops through all of the translations in the hash and displays each key and value in a list. We’ll need a way to add new translations so we’ll add a form below the list.
<h1>Translations</h1> <ul> <% @translations.each do |key, value| %> <li><%= key %>: <%= value %></li> <% end %> </ul> <h2>Add Translation</h2> <%= form_tag translations_path do %> <p> <%= label_tag :locale %><br /> <%= text_field_tag :locale %> </p> <p> <%= label_tag :key %><br /> <%= text_field_tag :key %> </p> <p> <%= label_tag :value %><br /> <%= text_field_tag :value %> </p> <p><%= submit_tag "Submit" %></p> <% end %>
This form will POST to the TranslationController
’s create
action. The form has three fields: one for the translation’s locale, such as en
for English; one for the key, which is how we’ll identify the translation in the view files, and one for the translated text itself.
In the create
action we want to add a new translation based on the supplied form values and we can do that by calling I18n.backend.store_translations
. This takes three arguments: the first is the locale, which we get from the form and the second is a hash and can be anything we want. We’ll pass in the key and value from the form fields. The final argument, escape
, determines if the full stops are escaped in the key or not. We’ll use full stops to separate different parts of the key so we’ll set it to false
.
def create I18n.backend.store_translations(params[:locale], {params[:key] => params[:value]}, :escape => false) redirect_to translations_url, :notice => "Added translations" end
We can try the new form out by adding the missing translation from the home page.
When we go back to the home page now we’ll see the translated header fetched from our new backend.
Using Redis For The Backend
Our new backend works well now but as we’re storing the values in a Ruby hash they’re all lost when we restart the web server. We need a more persistent store for the translations and for this application we’re going to use Redis which is a simple persistent key-value store.
If you’re using a Mac the easiest way to install Redis is via HomeBrew. To install it this way we need to run
$ brew install redis
Once its installed follow the instructions to start the Redis server. This is generally as straightforward as running redis-server
.
We’ll have to install the Redis gem to use Redit in our application. This is done by adding a reference to the gem in our Gemfile
and then running the bundle
command.
source 'http://rubygems.org' gem 'rails', '3.0.5' gem 'sqlite3' gem 'nifty-generators' gem 'redis'
We can now replace the hash in the backend initializer with a Redis database.
I18n.backend = I18n::Backend::KeyValue.new(Redis.new)
That’s all that’s really necessary, although we might want to use the :db
option to specify a database and to change the database that’s used depending on whether the application’s running in development, test or production mode.
As we’ve changed the key-value store we’ll need to change the translations view code, too. Currently it loops through a hash, but it now will be looping through a Redis database so we’ll need to change the following piece of code:
<ul> <% @translations.each do |key, value| %> <li><%= key %>: <%= value %></li> <% end %> </ul>to this: /app/views/translations/index.html.erb <ul> <% @translations.keys.each do |key| %> <li><%= key %>: <%= @translations[key] %></li> <% end %> </ul>
Now @translations
points to a Redis database instance. which doesn’t respond to each
, so we’ll need to iterate instead over its keys
. In the block we can show the key and the value for that key.
We now have a persistent store for our translations and any that we add through the form will survive when the application’s server is restarted.
Adding a Fallback Backend
We now have our alternative backend in place but what if we’d still like to use the YAML files for some of the translations? We’ll show you next how to use the YAML file as a fallback for values that aren’t available in the key-value store.
To do this we need to make some changes to the way we define the backend in the initializer file that we created earlier. Instead of using a KeyValue
backend directly we’ll use a Chain
backend. Any backends that we pass in to the Chain
backend will be called in order until one of them responds to a given translation key.
I18n.backend = I18n::Backend::Chain.new(I18n::Backend::KeyValue.new(Redis.new), I18n.backend)
We pass in our Redis backend first and then the default backend. Our application will now look for translations in the Redis database and, if it fails to find the appropriate key there, will look in the appropriate YAML file.
Initalizing the backend this way makes it more difficult to access the key-value store directly. To get around this we can move our database out into a constant.
TRANSLATION_STORE = Redis.new I18n.backend = I18n::Backend::Chain.new(I18n::Backend::KeyValue.new(TRANSLATION_STORE), I18n.backend)
We can now use this constant wherever we need to access the key-value store in our application, such as inside the TranslationsController
. We can no longer call I18n.backend.store
here.
def index @translations = TRANSLATION_STORE end
If we remove the stored translations from our Redis store and go to the home page now we’ll see the translated text from the YAML file.
When we add the translation back in it will take precedence and we’ll see the value from the Redis database.
That’s it for this episode. We now have a system in place that lets us edit translations through a web interface rather than by having to manually edit YAML files. If we were to do something like this in a production application there’s plenty that could be done to improve the user experience, but the basics are there.
If you want information about this topic then it’s well worth taking a look at José Valim’s upcoming book “Crafting Rails Applications” which is currently in beta and which was a great help in writing this episode.