#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)
En el episodio 138 [verlo, leerlo] tratamos el problema de la internacionalización. Por defecto Rails guarda la información de internacionalización en ficheros YAML, pero en este episodio vamos ver cómo se puede almacenar de diferente forma.
A continuación vemos una página de un sitio Rails muy sencillo del que queremos internacionalizar el texto de la cabecera.
Es muy fácil internacionalizar un fragmento de texto en una aplicación Rails utilizando el método t
, pero si lo hacemos así tenemos que editar un fichero YAML para cada lenguaje que soporte nuestra aplicación y añadir ahí la clave y el texto para dicho lenguaje. Esto puede hacerse muy tedioso para aplicaciones grandes, y de todas formas no es trabajo del desarrollador escribir todos los textos traducidos. Sería mucho mejor que existiese una interfaz web que permitiese a los administradores añadir y editar los textos traducibles.
Afortunadamente la gema de internacionalización soporta diferentes tipos de almacenamiento, lo que quiere decir que no tenemos por qué ceñirnos a usar YAML sino que podemos utilizar cualquier otro tipo de base de datos. Por defecto Rails utiliza el Simple backend que carga los ficheros YAML para gestionar con ellos las traducciones. En este episodio nos centraremos en Key-Value backend que nos permite usar cualquier almacenamiento de pares clave-valor para guardar las traducciones. También hay un soporte para almacenamiento sobre ActiveRecord que ha sido extraído a su propia gema, que funciona pero realmente no es la mejor manera de hacerlo. Es mejor que las claves estén en memoria en lugar de en una base de datos porque recuperar todas las cadenas traducibles de cada página supondrían muchos accesos a la base de datos. Aunque se podría utilizar caché para mejorar el rendimiento tendríamos entonces el problema de expirar esta caché cuando se modifiquen las claves de traducción, de ahí que sea mejor utilizar un almacenamiento de clave-valor, enfoque que veremos en este episodio.
Cambiando el motor
Primero queremos cambiar el texto estático de la cabecera para que utilice una traducción. Haremos que apunte a una clave llamada welcome
.
<h1><%= t('welcome') %></h1>
A continuación podemos añadir el texto traducido al YAML de inglés.
en: welcome: "Welcome"
Al refrescar la página veremos que se muestra el texto del archivo YAML.
Ahora que ya tenemos al menos una cadena de texto traducida utilizando el motor de internacionalización por defecto vamos a cambiarlo. En el fichero key_value.rb
hay instrucciones sobre cómo hacerlo, incluso dando un ejemplo de implementación de almacenamiento alternativo.
# I18n.backend = I18n::Backend::KeyValue.new(Rufus::Tokyo::Cabinet.new('*'))
Tenemos que crear un nuevo I18n::Backend::KeyValue
y pasarle el almacén de clave-valor que queremos usar para alojar las traducciones. Dicho almacén tiene que responder a tres métodos: uno para recuperar el valor de una clave, otro para establecerlo, y otro para listar todas las claves disponibles, tal y como se lee en los comentarios. La mayor parte de los almacenes de claves y valores de Ruby soportan estos métodos, por lo que podemos usar uno cualquiera sin hacer nada.
# * store#[](key) - Used to get a value # * store#[]=(key, value) - Used to set a value # * store#keys - Used to get all keys
Ya sabemos cómo empezar a mover el motor de internacionalización de nuestra aplicación. Primero vamos a crear un nuevo fichero en el directorio /config/initializers
llamado i18n_backend
.
I18n.backend = I18n::Backend::KeyValue.new({})
El ejemplo de los comentarios utiliza Tokyo Cabinet pero para empezar rápidamente nosotros hemos usado un hash vacío. Obviamente en una aplicación en producción no se nos ocurriría hacer esto, pero dado que es la estructura de datos más simple que soporta estos tres métodos nos servirá como demostración. Si reiniciamos la aplicación y vamos a la página pricipal veremos que el titulo ya no aparece.
De hecho en el código fuente de la página veremos que nos falta la traducción.
<h1><span class="translation_missing">en, welcome</span></h1>
Vemos que la traducción se marca como inexistente aunque sí que está en el fichero YAML. Vamos a crear la interfaz web que permita a nuestros usuarios añadir las traducciones según les vayan haciendo falta. Esto lo gestionaremos con un TranslationsController
que tendrá una única acción, index
.
$ rails g controller translations index
Queremos que este controlador se comporte como un recurso, por lo que cambiaremos la ruta generada (get "translations/index"
) con una llamada a resources
.
Intn::Application.routes.draw do resources :translations root :to => "home#index" end
En la acción index
queremos mostrar las traducciones disponibles, así que vamos a recuperar todas las traducciones del hash, lo que podemos hacer con I18n.backend.store
.
class TranslationsController < ApplicationController def index @translations = I18n.backend.store end end
En la vista podemos iterar sobre las traducciones para mostrarlas.
/app/views/translations/index.html.erb
<h1>Translations</h1> <ul> <% @translations.each do |key, value| %> <li><%= key %>: <%= value %></li> <% end %> </ul>
Con este código recorremos todas las traducciones en el hash y mostraremos cada clave y su valor en una lista. Necesitamos poder añadir traducciones, así que le añadiremos un formulario.
<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 %>
Con este formulario haremos un POST a la acción create
del controlador TranslationController
. El formulario tiene tres campos: uno para el idioma de la traducción, por ejemplo en
para el inglés, otro para la clave que es como se identificará la traducción en las vistas, y otro para el texto traducido en sí.
En la acción create
queremos añadir una nueva traducción basándonos en los valores del formulario, lo que podemos hacer llamando a I18n.backend.store_translations
que recibe tres argumentos: el primero es el idioma, y el segundo es un hash que puede contener cualquier cosa. Pasaremos la clave y el valor recibidos del formulario. El argumento escape
determina si se escapan los puntos en la clave o no. Como queremos que los puntos separen diferentes segmentos de la clave, este valor tendrá que ser false
.
def create I18n.backend.store_translations(params[:locale], {params[:key] => params[:value]}, :escape => false) redirect_to translations_url, :notice => "Added translations" end
Ya podemos probar el nuevo formulario para añadir la clave que falta.
Si ahora volvemos a la página principal veremos que ya se traduce el campo de la cabecera según los valores almacenados en nuestra nueva implementación
Redis como almacén
Nuestro nuevo motor de traducciones funciona muy bien pero como estamos alojando los valores en un hash de Ruby cuando reiniciemos el servidor web los cambios se perderán. Necesitamos un almacén más persistente para las traducciones y en esta aplicación vamos a utilizar Redis que es un almacén de claves y valores persistente muy sencillo.
Los que usen Mac pueden instalar Redis utilizando HomeBrew, con lo que la instalación sería tan sencilla como
$ brew install redis
Una vez instalado se deben seguir las instrucciones para arrancar el servidor Redis. Por lo general esto es tan simple como lanzar redis-server
.
Tenemos que instalar la gema de Redis en nuestra aplicación, añadiendo una referencia en el Gemfile
y luego ejecutando el comando bundle
.
source 'http://rubygems.org' gem 'rails', '3.0.5' gem 'sqlite3' gem 'nifty-generators' gem 'redis'
Ahora podemos cambiar el hash del inicializador por la base de datos Redis.
I18n.backend = I18n::Backend::KeyValue.new(Redis.new)
Y ya no tenemos que hacer nada más, aunque probablemente nos interesaría utilizar la opción :db
para especificar una base de datos y usar una u otra dependiendo de si la aplicación están en desarrollo, pruebas o producción.
Como hemos cambiado el almacén de claves y valores tendremos que cambiar otra vez el código de la vista. Ahora mismo itera sobre un hash, pero como ahora lo hará sobre una base de datos Redis cambiaremos el siguiente código:
<ul> <% @translations.each do |key, value| %> <li><%= key %>: <%= value %></li> <% end %> </ul>
<ul> <% @translations.keys.each do |key| %> <li><%= key %>: <%= @translations[key] %></li> <% end %> </ul>
Ahora @translations
es una base de datos Redis que no responde al método each
, por lo que tenemos que iterar utilizando el método keys
. En el bloque podemos mostrar la clave y su valor.
Ya tenemos un almacén persistente para las traducciones de nuestra aplicación y cualquier cosa que añadamos utilizando la interfaz sobrevivirá a un reinicio.
Cómo añadir un almacén de respaldo
Ya tenemos nuestro motor alternativo de traducciones en su sitio pero, ¿y si queremos seguir utilizando YAML para las traducciones que no se encuentren en el almacén principal de claves y valores? Ahora veremos cómo hacerlo.
Tendremos que hacer algunos cambios en la forma en que definimos el motor en el inicializador. En lugar de utilizar el KeyValue
directamente, vamos a utilizar otro motor llamado Chain
, que lo que hace es ir invocando todos los motores que recibe como parámetro hasta que uno de ellos responde con la clave.
I18n.backend = I18n::Backend::Chain.new(I18n::Backend::KeyValue.new(Redis.new), I18n.backend)
Pasamos primero nuestro almacén Redis y luego el almacén por defecto. Nuestra aplicacón ahora buscará las traducciones en la base de datos Redis y luego, si no encuentra la clave, la buscará en el fichero YAML correspondiente.
Pero de esta manera se hace más difícil acceder directente al almacén de claves y valores directamente. Para soslayar esto podemos mover nuestra base de datos a una constante.
TRANSLATION_STORE = Redis.new I18n.backend = I18n::Backend::Chain.new(I18n::Backend::KeyValue.new(TRANSLATION_STORE), I18n.backend)
Y ya podemos usar esta constante donde quiera que necesitemos acceder al almacén de claves y valores en nuestra aplicación, por ejemplo dentro de TranslationsController
donde en lugar de llamar a I18n.backend.store
lo haremos invocando a la constante.
def index @translations = TRANSLATION_STORE end
Si eliminamos las traducciones del almacén Redis y vamos ahora a la página principal veremos el texto traducido que se recupera del YAML.
Cuando añadamos un nuevo valor, éste tendrá precedencia y será el que se muestre.
Y con esto concluye este episodio. Ya tenemos un sistema que nos permite editar las traducciones mediante una interfaz web en lugar de tener que editar manualmente los ficheros YAML. Si tuviéramos que hacer algo como esto en una aplicación de producción tendríamos mucho que mejorar en cuanto a experiencia de usuario, pero la base ya está puesta.
Para ampliar más información sobre este tema se puede leer el próximo libro de José Valim llamado “Crafting Rails Applications” que está en versión beta y que ha sido de mucha ayuda a la hora de escribir este episodio.