Phoenix I18n
Spanish (Español) translation by Rafael Chavarría (you can also view the original English article)
En mis artículos anteriores cubrí los diversos aspectos de Elixir---un lenguaje de programación moderno y funcional. El día de hoy, sin embargo, me gustaría alejarme un poco del lenguaje mismo y discutir un marco de trabajo MVC muy rápido y confiable llamado Phoenix que está escrito con Elixir.
Este marco de trabajo surgió hace cerca de cinco años y ha recibido algo de tracción desde entonces. Por supuesto, no es tan popular como Rails o Django todavía, pero tiene un gran potencial y realmente me agrada.
En este artículo vamos a ver cómo introducir l18n en aplicaciones Phoenix. ¿Qué es l18n, preguntas? Bueno, es un numerónimo que significa "internacionalización", ya que hay exactamente 18 caracteres entre la primera letra "i" y la última "n". Probablemente, también has conocido un numerónimo L10n el cuál significa "localización". Los desarrolladores estos días son tan flojos que ni siquiera pueden escribir un par de caracteres extra, ¿no?
Internacionalización es un proceso muy importante, especialmente si prevees que la aplicación sea usada por personas de todo el mundo. Después de todo, no todos saben bien Inglés, y tener tu aplicación traducida en el lenguaje nativo del usuario da una buena impresión.
Parece que el proceso de traducir aplicaciones Phoenix es algo diferente de, digamos, traducir aplicaciones Rails (pero bastante similar al mismo proceso en Django). Para traducir aplicaciones Phoenix, usamos una solución bastante popular llamada Gettext, la cuál ha existido por alrededor de más de 25 años. Gettex funciona con tipos especiales de archivo, llamados PO y POT, y soporta características como alcance, pluralización, y otras ventajas.
Así que en este artículo te voy a explicar lo que es Gettext, cómo difiere PO de POT, cómo localizar mensajes en Phoenix, y en donde almacenar traducciones. También vamos a ver cómo cambiar la localización de la aplicación y cómo trabajar con reglas de pluralización y dominios.
¿Comenzamos?
Internacionalización Con Gettext
Gettext es una herramienta probada de código libre para internacionalización introducida inicialmente por Sun Microsystems en 1990. En 1995, GNU lanzó su propia versión de Gettext, la cuál es ahora considerada la más popular ahí afuera (la versión más reciente fue 0.19.8 al momento de escribir este artículo). Gettext podría ser usado para crear sistemas multilingües de cualquier tamaño y tipo, desde aplicaciones web hasta sistemas operativos. Esta solución es bastante compleja, y no vamos a discutir todas sus características, por supuesto. La documentación completa Gettext puede ser encontrada en gnu.org.
Gettext te proporciona todas las herramientas necesarias para realizar localización y presenta algunos requerimientos sobre cómo deberían ser nombrados y organizados los archivos de traducción. Dos tipos de archivos son usados para albergar traducciones: PO y MO.
Los archivos PO (Portable Object, Objeto Portable) almacenan traducciones para cadenas dadas así como reglas de pluralización y metadatos. Estos archivos tienen una estructura simple y pueden ser editados fácilmente por un humano, así que en este artículo nos apegaremos a ellos. Cada archivo PO contiene traducciones (o parte de las traducciones) para un solo lenguaje y debería ser almacenado en un directorio llamado como su lenguaje: en, fr, de, etc.
Los archivos MO (Machine Object, Objeto Máquina) contienen datos binarios que no están hechos para ser editados directamente por un humano. Son más difíciles para trabajar, y discutirlos está fuera del alcance de este artículo.
Para hacer las cosas más complejas, también hay archivos POT (Portable Object Template, Plantilla de Objeto Portable). Estos albergan solo cadenas de texto para traducir, pero no las traducciones mismas. Básicamente, los archivos POT son usados solo como planos para crear archivos PO para varias locaciones.
Aplicación Phoenix Simple
Está bien, ¡ahora procedamos a la práctica! Si quisieras seguir a la par, asegúrate de tener instalado lo siguiente:
- OTP (versión 18 o superior)
- Elixir (1.4+)
- Marco de trabajo Phoenix (voy a estar usando la versión 1.3)
Crea una nueva aplicación de muestra sin una base de datos ejecutando:
mix phx.new i18ndemo --no-ecto
--no-ecto
dice que la base de datos no debería ser utilizada por la aplicación (Ecto es una herramienta para comunicarse con la BD misma). Nota que el generador podría requerir un par de minutos para preparar todo.
Ahora usa cd
para ir a la recién creada carpeta i18ndemo
y ejecuta el siguiente comando para iniciar el servidor:
mix phx.server
Después, abre el navegador y ve a http://localhost:4000
, en donde deberías ver un mensaje "Welcome to Phoenix!".
¡Hola, Gettext!
Lo que es interesante acerca de nuestra aplicación Phoenix, específicamente, el mensaje de bienvenida es que Gettext ya está siendo usado por defecto. Continua y abre el archivo demo/lib/demo_web/templates/page/index.html.eex
que actúa como una página de inicio por defecto. Quita todo excepto este código:
<div class="jumbotron"> <h2><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h2> </div>
Este mensaje de bienvenida utiliza una función gettext
la cuál acepta una cadena para traducir como el primer argumento. Esta cadena puede ser considerada como una llave de traducción, aunque es algo diferente de las llaves usadas en Rails l18n y algunos otros marcos de trabajo. En Rails habríamos usado una llave como page.welcome
, mientras que aquí la cadena traducida es una llave por si misma. Así qué, si la traducción no puede se encontrada, podemos mostrar esta cadena directamente. Incluso si un usuario sabe muy poco Inglés puede al menos tener un entendimiento básico de qué está pasando.
Esta aproximación es de hecho bastante útil---detente por un segundo y piensa sobre eso. Tienes una aplicación en donde todos los mensajes están en Inglés. Si quisieras internacionalizarla, en el caso más simple todo lo que tienes que hacer es envolver tus mensajes con la función gettext
y proporcionar traducciones para estos (después veremos que el proceso de extraer las claves puede ser automatizado fácilmente, lo cuál acelera las cosas incluso más).
Está bien, regresemos a nuestro pequeño código y echa un vistazo al segundo argumento pasado a gettext
: name: "Phoenix"
. Esto es un llamado binding---un parámetro envuelto con %{}
que nos gustaría interpolar a una traducción dada. En este ejemplo, solo hay un parámetro llamado name
.
También podemos agregar un mensaje más a esta página para propósitos demostrativos:
<div class="jumbotron"> <h2><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h2> <h3><%= gettext "We are using version %{version}", version: "1.3" %></h3> </div>
Agregando una Nueva Traducción
Ahora que tenemos dos mensajes en la página raíz, ¿deberíamos agregar traducciones para ellos? Parece que todas las traducciones están almacenadas bajo la carpeta priv/gettext
, la cual tiene una estructura predefinida. Tomemos un momento para discutir cómo deberían estar organizados los archivos Gettext (esto aplica no solo a Phonenix sino a cualquier aplicación usando Gettext).
Primero que nada, deberíamos crear una carpeta llamada como la ubicación para la que va a guardar traducciones. Dentro, debería haber una carpeta llamada LC_MESSAGES
conteniendo uno o varios archivos .po
con las traducciones. En el caso más sencillo, tendrías un archivo default.po
por ubicación. default
aquí es el nombre del dominio (o alcance). Los dominios son usados para dividir traducciones en varios grupos: por ejemplo, podrías tener dominios nombrados admin
, wysiwig
, cart
, y otro. Esto es conveniente cuando tienes una aplicación grande con cientos de mensajes. Para aplicaciones más pequeñas, sin embargo, tener un solo dominio default
es suficiente.
Así que nuestra estructura de archivo luciría así:
- en
- LC_MESSAGES
- default.po
- admin.po
- LC_MESSAGES
- ru
- LC_MESSAGES
- default.po
- admin.po
- LC_MESSAGES
Para comenzar a crear archivos PO, primero necesitamos la plantilla correspondiente (POT). Podemos crearla de manera manual, pero soy muy flojo para hacerlo de esta manera. Ejecutemos el siguiente comando en su lugar:
mix gettext.extract
Es una herramienta muy útil que escanea los archivos del proyecto y revisa si Gettext es usado en algún lugar. Después de que el script termina su trabajo, un nuevo archivo priv/gettext/default.pot
conteniendo cadenas para traducir será creado.
Como ya hemos aprendido, los archivos POT son plantillas, así que almacenan solo las llaves mismas, no las traducciones, así que no modifiques tales archivos de manera manual. Abre un archivo recién creado y echa un vistazo a sus contenidos:
## This file is a PO Template file. ## ## `msgid`s here are often extracted from source code. ## Add new translations manually only if they're dynamic ## translations that can't be statically extracted. ## ## Run `mix gettext.extract` to bring this file up to ## date. Leave `msgstr`s empty as changing them here as no ## effect: edit them in PO (`.po`) files instead. msgid "" msgstr "" #: lib/demo_web/templates/page/index.html.eex:3 msgid "We are using version %{version}" msgstr "" #: lib/demo_web/templates/page/index.html.eex:2 msgid "Welcome to %{name}!" msgstr ""
Conveniente, ¿no es así? Todos nuestros mensajes fueron insertados de manera automática, y podemos ver fácilmente en donde están ubicados. msgid
, como probablemente adivinaste, es la llave, mientras que msgstr
va a contener la traducción.
El siguiente paso es, por supuesto, generar un archivo PO. Ejecuta:
mix gettext.merge priv/gettext
Este script va a utiliza la plantilla default.pot
y crear un archivo default.po
en la carpeta priv/gettext/en/LC_MESSAGES
. Por ahora, tenemos solo una ubicación English, pero será agregado soporte para otros lenguajes en la siguiente sección también.
Por cierto, es posible crear o actualizar la plantilla POT y todos los archivos PO de una vez usando el siguiente comando:
mix gettext.extract --merge
Ahora abramos el archivo priv/gettext/en/LC_MESSAGES/default.po
, el cuál tiene los siguientes contenidos:
## `msgid`s in this file come from POT (.pot) files. ## ## Do not add, change, or remove `msgid`s manually here as ## they're tied to the ones in the corresponding POT file ## (with the same domain). ## ## Use `mix gettext.extract --merge` or `mix gettext.merge` ## to merge POT files into PO files. msgid "" msgstr "" "Language: en\n" #: lib/demo_web/templates/page/index.html.eex:3 msgid "We are using version %{version}" msgstr "" #: lib/demo_web/templates/page/index.html.eex:2 msgid "Welcome to %{name}!" msgstr ""
Este es el archivo en donde deberíamos realizar la traducción. Por supuesto, tiene poco sentido hacerlo porque los mensajes ya están en Inglés, así que procedamos a la siguiente sección y agreguemos soporte para un segundo lenguaje.
Múltiples Ubicaciones
Naturalmente, la ubicación por defecto de aplicaciones Phoenix es Inglés, pero esta configuración puede ser cambiada fácilmente modificando el archivo config/config.exs
. Por ejemplo, establezcamos la ubicación por defecto a Ruso (siéntete libre de quedarte con cualquier otro lenguaje de tu elección):
config :demo, I18ndemoWeb.Gettext, default_locale: "ru"
También es una buena idea especificar la lista completa de todas las ubicaciones soportadas:
config :demo, I18ndemoWeb.Gettext, default_locale: "ru", locales: ~w(en ru)
Ahora lo que necesitamos hacer es generar un nuevo archivo PO que contenga traducciones para la ubicación de Ruso. Puede ser realizado ejecutando el script gettext.merge
de nuevo, pero con un cambio de --locale
:
mix gettext.merge priv/gettext --locale ru
Obviamente, una carpeta priv/gettext/ru/LC_MESSAGES
con los archivos .po
dentro será generada. Nota, por cierto, que además del archivo default.po
, también tenemos errors.po
. Este es un lugar por defecto para traducir mensajes de error, pero en este artículo vamos a ignorarlo.
Ahora modifica el priv/gettext/ru/LC_MESSAGES/default.po
agregando algunas traducciones:
#: lib/demo_web/templates/page/index.html.eex:3 msgid "We are using version %{version}" msgstr "Используется версия %{version}" #: lib/demo_web/templates/page/index.html.eex:2 msgid "Welcome to %{name}!" msgstr "Добро пожаловать в приложение %{name}!"
Ahora, dependiendo de la ubicación elegida, Phoenix generará ya sea las traducciones en Inglés o Ruso. ¡Pero espera! ¿Cómo podemos cambiar realmente entre ubicaciones en nuestra aplicación? ¡Procedamos a la siguiente sección y averigüemoslo!
Cambiando Entre Ubicaciones
Ahora que algunas traducciones están presentes, necesitamos habilitar a nuestros usuarios para que cambien entre ubicaciones. Parece que hay un complemento para eso llamado set_locale. Funciona extrayendo la ubicación elegida de la URL o el encabezado HTTP Accept-Language
. Así que, para especificar una ubicación en la URL, teclearías http://localhost:4000/en/alguna_ruta
. Si la ubicación no es especificada (o si un lenguaje no soportado fue solicitado), una de dos cosas sucederá:
- Si la petición contiene un encabezado HTTP
Accept-Language
y su ubicación es soportada, el usuario será redirigido a una página con la ubicación correspondiente. - De otro modo, el usuario será redirigido de manera automática a una URL que contenga el código para la ubicación por defecto.
Abre el archivo mix.exs
y anexa el set_locale
a la función deps
:
defp deps do [ # ... {:set_locale, "~> 0.2.1"} ] end
También debemos agregarlo a la función application
:
def application do [ mod: {Demo.Application, []}, extra_applications: [:logger, :runtime_tools, :set_locale] ] end
Después, instala todo:
mix deps.get
Nuestro ruteador localizado en lib/demo_web/router.ex
requiere algunos cambios también. Específicamente, necesitamos agregar un nuevo enchufe a :browser
.
pipeline :browser do # ... plug SetLocale, gettext: DemoWeb.Gettext, default_locale: "ru" end
También, crea un nuevo alcance:
scope "/:locale", DemoWeb do pipe_through :browser get "/", PageController, :index end
¡Y eso es todo! Puedes iniciar el servidor y navegar a http://localhost:4000/ru
y http://localhost:4000/en
. Nota que todos los mensajes están traducidos apropiadamente, ¡lo cuál es exactamente lo que necesitamos!
De manera alternativa, podrías codificar una característica similar utilizando un enchufe de Módulo. Un pequeño ejemplo puede ser encontrado en la guía oficial Phonenix.
Una última cosa para mencionar es que en algunos casos podrías necesitar reforzar una ubicación específica. Para hacer eso, simplemente utiliza una función with_locale
:
Gettext.with_locale I18ndemoWeb.Gettext, "en", fn -> MyApp.I18ndemoWeb.gettext("test") end
Pluralización
Hemos aprendido los fundamentos de usar Gettext con Phoenix, así que ha llegado la hora de discutir cosas ligeramente más complejas. La Pluralización es una de ellas. Básicamente, trabajar con formas plurales y singulares es una tarea muy común pero potencialmente compleja. Las cosas son memos obvias en Ingles ya que tienes "1 manzana", "2 manzanas", "9000 manzanas" etc.
Desafortunadamente, en algunos otros lenguajes como Ruso o Polaco, las reglas son más complejas. Por ejemplo, en el caso de manzanas, dirías "1 яблоко", "2 яблока", "9000 яблок". Pero afortunadamente para nosotros, Phoenix tiene un comportamiento Gettext.Plural
(podrás ver el comportamiento en acción en uno de mis artículos previos) que soporta muchos lenguajes diferentes. De ahí que todo lo que tenemos que hacer es sacar ventaja de la función ngettext
.
Esta función acepta tres argumentos requeridos: una cadena en forma singular, una cadena en forma plural, y cuenta. El cuarto argumento es opcional y puede contener enlaces que deberían ser interpolados en la traducción.
Veamos ngettext
en acción diciendo cuánto dinero tiene el usuario modificando el archivo demo/lib/demo_web/templates/page/index.html.eex
:
<p> <%= ngettext "You have one buck. Ow :(", "You have %{count} bucks", 540 %> </p>
%{count}
es una interpolación que será reemplazada con un número (540
en este caso). No olvides actualizar la plantilla y todos los archivo PO después de agregar la cadena de arriba.
mix gettext.extract --merge
Verás que un nuevo bloque ha sido agregado a ambos archivos default.po
:
msgid "You have one buck. Ow :(" msgid_plural "You have %{count} bucks" msgstr[0] "" msgstr[1] ""
No tenemos una sino dos llaves aquí a la vez: en formas singular y plural. msgstr[0]
va a contener algún texto para mostrar cuando solo hay un mensaje. msgstr[1]
, por supuesto, contiene el texto para mostrar cuando hay varios mensajes. Está bien para inglés. pero no lo suficiente para Ruso en donde necesitamos introducir un tercer caso:
msgid "You have one buck. Ow :(" msgid_plural "You have %{count} bucks" msgstr[0] "У 1 доллар. Маловато будет!" msgstr[1] "У вас %{count} доллара" msgstr[2] "У вас %{count} долларов"
Caso 0
es usado para 1 dólar, y caso 1
para cero o pocos dólares. Caso 2
es usado en caso contrario.
Alcance de Traducciones Con Dominios
Otro tema que quería discutir en este artículo está dedicado a dominios. Como ya sabemos, los dominios son usados para extender traducciones, principalmente en aplicaciones grandes. Básicamente, estos actúan como namespaces.
Después de todo, podrías terminar en una situación cuando la misma llave es usada en múltiples lugares, pero deberían ser traducidas un poco diferente. O cuando tienes demasiadas traducciones en un solo archivo default.po
y quisieras partirlas de algún modo. Ahí es cuando los dominios pueden ser realmente útiles.
Gettext soporta múltiples dominios por defeco. Todo lo que tienes que hacer es utilizar la función dgettext
, la cuál funciona casi igual que gettext
. La única diferencia es que esta acepta el nombre de dominio como primer argumento. Por ejemplo, introduzcamos un dominio de notificación a, buen, mostrar notificaciones. Agrega tres líneas más de código al archivo demo/lib/demo_web/templates/page/index.html.eex
:
<p> <%= dgettext "notifications", "Heads up: %{msg}", msg: "something has happened!" %> </p>
Ahora necesitamos crear nuevos archivos POT y PO:
mix gettext.extract --merge
Después de que el script termina de hacer su trabajo, notifications.pot
así como dos archivos notifications.po
serán creados. Nota que una vez más que son nombrados como el dominio. Todo lo que tienes que hacer ahora es agregar traducción para el lenguaje Ruso modificando el archivo priv/ru/LC_MESSAGES/notifications.po
:
msgid "Heads up: %{msg}}" msgstr "Внимание: %{msg}"
¿Y si quisieras pluralizar un mensaje almacenado bajo un dominio dado? Esto es tan simple como utilizar una función dngettext
. Funciona como ngettext
pero también acepta el nombre de un dominio como el primer argumento.
dgettext "domain", "Singular string %{msg}", "Plural string %{msg}", 10, msg: "demo"
Conclusión
En este artículo, hemos visto cómo introducir internacionalización en una aplicación Phoenix con la ayuda de Gettext. Has aprendido qué es Gettext y con qué tipo de archivos trabaja. Tenemos esta solución en acción, hemos trabajado con archivos PO y POT, y visualizado varias funciones Gettext.
También hemos visto una manera de agregar soporte a múltiples ubicaciones y agregado una manera de cambiar fácilmente entre ellas. Por último, hemos visto como emplear reglas de pluralización y cómo extender traducciones con la ayuda de dominios.
Ojalá, ¡este artículo haya sido de utilidad para ti! Si quisieras aprender más sobre Gettext en el marco de trabajo Phoenix, podrías referirte a la guía oficial, la cuál proporciona ejemplos útiles y referencia de API para todas las funciones disponibles.
¡Te agradezco por quedarte conmigo y te veo pronto!