Symfony 1.4, la guía definitiva

13.4. Traducción de la interfaz

La interfaz de usuario es otro de los elementos que se deben adaptar en las aplicaciones i18n. Las plantillas tienen que poder mostrar las etiquetas, los mensajes y la navegación en diferentes idiomas pero manteniendo la misma presentación. Symfony recomienda que las plantillas se construyan con el lenguaje por defecto de la aplicación y que se defina la traducción de las frases en un archivo de diccionario. De esta forma, no es necesario modificar las plantillas cada vez que se añade, modifica o elimina una traducción.

13.4.1. Configurando la traducción

Las plantillas no se traducen automáticamente, lo que significa que antes que nada, se debe activar la opción de traducción de las plantillas en el archivo settings.yml, como se muestra en el listado 13-11.

Listado 13-11 - Activando la traducción de la interfaz, en frontend/config/settings.yml

all:
  .settings:
    i18n: true

13.4.2. Usando el helper de traducción

En esta sección se va a considerar que se quiere construir un sitio web en inglés y en francés, siendo el inglés el idioma por defecto. Antes de empezar con la traducción del sitio web, una de las plantillas del sitio podría ser similar a la del listado 13-12.

Listado 13-12 - Plantilla con un único idioma

Welcome to our website. Today's date is <?php echo format_date(date()) ?>

Para que Symfony pueda traducir las frases de una plantilla, estas deben identificarse como "texto traducible". Para ello se ha definido el helper __() (dos guiones bajos seguidos), que es parte del grupo de helpers llamado I18N. De esa forma, todas las plantillas deben encerrar las frases que se van a traducir en llamadas a ese helper. El listado 13-11 por ejemplo se puede modificar para que tenga el aspecto del listado 13-13 (como se verá más adelante en la sección "Cómo realizar traducciones complejas", existe una forma mejor para llamar al helper de traducción).

Listado 13-13 - Plantilla preparada para múltiples idiomas

<?php use_helper('I18N') ?>

<?php echo __('Welcome to our website.') ?>
<?php echo __("Today's date is ") ?>
<?php echo format_date(date()) ?>

Nota Si la aplicación hace uso del grupo de helpers I18N en todas sus páginas, puede ser una buena idea incluirlo en la opción standard_helpers del archivo settings.yml, de forma que no sea necesario incluir use_helper('I18N') en cada plantilla.

13.4.3. Utilizando un archivo de diccionario

Cuando se invoca la función __(), Symfony busca la traducción del argumento que se le pasa en el diccionario correspondiente a la cultura del usuario. Si se encuentra una frase equivalente, la función devuelve la traducción y se muestra en la respuesta. De esta forma, la traducción de la interfaz se basa en los archivos de diccionario.

Los archivos de diccionario se crean siguiendo el formato XLIFF (XML Localization Interchange File Format), sus nombres siguen el patrón messages.[codigo de idioma].xml y se guardan en el directorio i18n/ de la aplicación.

XLIFF es un formato estándar basado en XML. Como se trata de un formato muy utilizado, se pueden emplear herramientas externas que facilitan la traducción del sitio web entero. Las empresas que se encargan de realizar traducciones manejan este tipo de archivos y saben cómo traducir un sitio web entero añadiendo un nuevo archivo XLIFF.

Nota Además del estándar XLIFF, Symfony también permite utilizar otros sistemas para guardar los diccionarios: gettext, MySQL y SQLite. La documentación de la API contiene toda la información sobre la configuración de estos métodos alternativos.

El listado 13-14 muestra un ejemplo de la sintaxis XLIFF necesaria para crear el archivo messages.fr.xml que traduce al francés los contenidos del listado 13-13.

Listado 13-14 - Diccionario en formato XLIFF, en frontend/i18n/messages.fr.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE xliff PUBLIC "-//XLIFF//DTD XLIFF//EN"
 "http://www.oasis-open.org/committees/xliff/documents/xliff.dtd" >
<xliff version="1.0">
  <file original="global" source-language="en_US" datatype="plaintext">
    <body>
      <trans-unit id="1">
        <source>Welcome to our website.</source>
        <target>Bienvenue sur notre site web.</target>
      </trans-unit>
      <trans-unit id="2">
        <source>Today's date is </source>
        <target>La date d'aujourd'hui est </target>
      </trans-unit>
    </body>
  </file>
</xliff>

El atributo source-language siempre contiene el código ISO completo correspondiente a la cultura por defecto. Cada frase o elemento que se traduce, se indica en una etiqueta trans-unit con un atributo id único.

Si en la aplicación se utiliza la cultura por defecto (en este ejemplo en_US), las frases no se traducen y por tanto se muestran directamente los argumentos indicados en las llamadas a __(). El resultado del listado 13-12 es similar al listado 13-11. Sin embargo, si se modifica la cultura a fr_FR o fr_BE, se muestran las traducciones del archivo messages.fr.xml, y el resultado es el que se muestra en el listado 13-15.

Listado 13-15 - Una plantilla traducida

Bienvenue sur notre site web. La date d'aujourd'hui est
<?php echo format_date(date()) ?>

Si se necesita añadir una nueva traducción, solamente es preciso crear un nuevo archivo messages.XX.xml de traducción en el mismo directorio que el resto de traducciones.

Nota Como procesar los archivos de los diccionarios y buscar las traducciones es un proceso que consume un tiempo apreciable, Symfony utiliza una cache interna para mejorar el rendimiento. Por defecto esta cache utiliza archivos, pero es posible configurarla en el archivo de configuración factories.yml (ver capítulo 19). De esta forma es posible por ejemplo compartir una misma cache entre diferentes servidores.

13.4.4. Trabajando con diccionarios

Si el archivo messages.XX.xml aumenta tanto de tamaño como para hacerlo difícil de manejar, se pueden dividir sus contenidos en varios archivos de diccionarios ordenados por temas. De esta forma, es posible por ejemplo dividir el archivo messages.fr.xml en los siguientes tres archivos dentro del directorio i18n/:

  • navegacion.fr.xml
  • terminos_de_servicio.fr.xml
  • busqueda.fr.xml

Siempre que una traducción no se encuentre en el archivo messages.XX.xml por defecto, se debe indicar como tercer argumento en la llamada al helper __() el archivo de diccionario que debe utilizarse. Para traducir una cadena de texto que se encuentra en el archivo navegacion.fr.xml, se utilizaría la siguiente instrucción:

<?php echo __('Welcome to our website', null, 'navegacion') ?>

Otra forma de organizar los diccionarios es mediante su división en módulos. En vez de crear un solo archivo messages.XX.xml para toda la aplicación, se crea un archivo en cada directorio modules/[nombre_modulo]/i18n/. Así se consigue que los módulos sean más independientes de la aplicación, lo que es necesario para reutilizarlos, como por ejemplo en los plugins (ver Capítulo 17).

Como actualizar los diccionarios a mano es un proceso muy propenso a cometer errores, Symfony incluye a partir de su versión 1.1 una tarea que permite automatizar todo este proceso. La tarea i18n:extract procesa una aplicación Symfony completa y extrae todas las cadenas de texto que se tienen que traducir. Los argumentos que se pasan a esta tarea son el nombre de la aplicación y una cultura:

$ php symfony i18n:extract frontend en

Por defecto esta tarea no modifica los diccionarios, sino que simplemente muestra el número de cadenas anteriores y actuales de i18n. Para añadir las nuevas cadenas de texto al diccionario, se debe utiliza la opción --auto-save:

$ php symfony i18n:extract --auto-save frontend en

También es posible borrar las cadenas de texto anteriores utilizando la opción --auto-delete:

$ php symfony i18n:extract --auto-save --auto-delete frontend en

Nota La tarea i18n:extract presenta algunas limitaciones actualmente. En primer lugar sólo funciona con el diccionario por defecto messages y sólo es capaz de manejar traducciones basadas en archivos (XLIFF y gettext). Además, esta tarea sólo guarda y borra cadenas de texto en el archivo principal apps/frontend/i18n/messages.XX.xml

13.4.5. Trabajando con otros elementos que requieren traducción

Otros elementos también pueden requerir ser traducidos:

  • Las imágenes, documentos y cualquier otro tipo de contenido estático pueden variar en función de la cultura del usuario. Un ejemplo típico es el de las imágenes que se utilizan para mostrar un contenido de texto con una tipografía muy especial. En este caso, se pueden crear subdirectorios para cada una de las culturas disponibles (utilizando el valor culture para el nombre de cada subdirectorio):
<?php echo image_tag($sf_user->getCulture().'/miTexto.gif') ?>
  • Los mensajes de error de los archivos de validación se muestran automáticamente mediante __(), por lo que para traducirlos, solo es necesario añadirlos a los archivos de diccionario.
  • Las páginas por defecto de Symfony (página no encontrada, error interno de servidor, acceso restringido, etc.) están escritas en inglés y tienen que reescribirse para las aplicaciones i18n. Probablemente, la solución consiste en crear un módulo default propio en la aplicación y utilizar __() en las plantillas. El Capítulo 19 explica cómo personalizar estas páginas.

13.4.6. Cómo realizar traducciones complejas

La traducción mediante __() requiere que se se le pase como argumento una frase completa. Sin embargo, es muy común tener variables mezcladas con el texto en una frase. Aunque puede ser tentador intentar cortar las frases en varios trozos, el resultado es que las llamadas al helper pierden su significado. Afortunadamente, el helper __() dispone de una opción para reemplazar el valor de las variables y que permite crear diccionarios que conservan su significado y simplifican la traducción. Las etiquetas HTML también se pueden incluir en la llamada al helper. El listado 13-16 muestra un ejemplo.

Listado 13-16 - Traduciendo frases con etiquetas HTML y código PHP

// Frases originales
Welcome to all the <strong>new</strong> users.<br />
There are <?php echo count_logged() ?> persons logged.

// Ejemplo malo de como traducir las frases anteriores
<?php echo __('Welcome to all the') ?>
<strong><?php echo __('new') ?></strong>
<?php echo __('users') ?>.<br />
<?php echo __('There are') ?>
<?php echo count_logged() ?>
<?php echo __('persons logged') ?>

// Ejemplo correcto para traducir las frases anteriores
<?php echo __('Welcome to all the <strong>new</strong> users') ?> <br />
<?php echo __('There are %1% persons logged', array('%1%' => count_logged())) ?>

En este ejemplo, el nombre que se utiliza para la sustitución es %1%, pero puede utilizarse cualquier nombre, ya que el reemplazo se realiza en el helper mediante la función strtr() de PHP.

Otro de los problemas habituales de las traducciones es el uso del plural. En función del número de resultados, el texto cambia, pero no lo hace de la misma forma en todos los idiomas. La última frase del listado 13-16 por ejemplo no es correcta si count_logged() devuelve 0 o 1. Aunque es posible comprobar el valor devuelto por la función y seleccionar la frase adecuada mediante código PHP, esta forma de trabajar es bastante tediosa. Además, cada idioma tiene sus propias reglas gramaticales, por lo que intentar inferir el plural de las palabras puede ser muy complicado. Como se trata de un problema muy habitual, Symfony incluye un helper llamado format_number_choice(). El listado 13-17 muestra cómo utilizar este helper.

Listado 13-17 - Traduciendo las frases en función del valor de los parámetros

<?php echo format_number_choice(
      '[0]Nobody is logged|[1]There is 1 person logged|(1,+Inf]There are %1% persons logged', array('%1%' => count_logged()), count_logged()) ?>

El primer argumento está formado por las diferentes posibilidades de frases. El segundo parámetro es el patrón utilizado para reemplazar variables (como con el helper __()) y es opcional. El tercer argumento es el número utilizado para determinar la frase que se utiliza.

Las frases de las diferentes posibilidades se separan mediante el carácter | seguido de un array de valores, utilizando la siguiente sintaxis:

  • [1,2]: acepta valores entre 1 y 2, ambos incluidos.
  • (1,2): acepta valores entre 1 y 2, ambos excluidos.
  • {1,2,3,4}: sólo se aceptan los valores definidos en este conjunto.
  • [-Inf,0): acepta valores mayores o iguales que -infinito y que son estrictamente menores que 0.
  • {n: n % 10 > 1 && n % 10 < 5}: la condición se cumple para números como 2, 3, 4, 22, 23, 24 (muy útil en idiomas como el polaco y el ruso).

Se puede utilizar cualquier combinación no vacía de paréntesis y corchetes.

Para que la traducción funcione correctamente, el archivo XLIFF debe contener el mensaje tal y como aparece en la llamada al helper format_number_choice(). El listado 13-18 muestra un ejemplo.

Listado 13-18 - diccionario XLIFF para un argumento de format_number_choice()

...
<trans-unit id="3">
  <source>[0]Nobody is logged|[1]There is 1 person logged|(1,+Inf]There are %1% persons logged</source>
  <target>[0]Personne n'est connecté|[1]Une personne est connectée|(1,+Inf]Ily a %1% personnes en ligne</target>
</trans-unit>
...

13.4.7. Utilizando el helper de traducción fuera de una plantilla

No todo el texto que se muestra en las páginas viene de las plantillas. Por este motivo, es habitual tener que utilizar el helper __() en otras partes de la aplicación: acciones, filtros, clases del modelo, etc. El listado 13-19 muestra cómo utilizar el helper en una acción mediante la instancia del objeto I18N obtenida a través del singleton de contexto.

Listado 13-19 - Utilizando __() en una acción

$this->getContext()->getI18N()->__($texto, $argumentos, 'mensajes');