El término «internacionalización» (frecuentemente abreviado como i18n) se refiere al proceso de abstraer cadenas y otros elementos específicos de la configuración regional de tu aplicación a una capa donde se puedan traducir y convertir basándose en la configuración regional del usuario (es decir, idioma y país). Para el texto, esto significa envolver cada uno con una función capaz de traducir el texto (o «mensaje») al idioma del usuario:
// el texto *siempre* se imprime en Inglés
echo 'Hello World';
// el texto se puede traducir al idioma del
// usuario final o predeterminado en Inglés
echo $translator->trans('Hello World');
Nota
El término locale se refiere más o menos al lenguaje y país del usuario. Este puede ser cualquier cadena que tu aplicación utilice para manejar las traducciones y otras diferencias de formato (por ejemplo, el formato de moneda). Recomendamos el código de idioma ISO639-1, un guión bajo (_), luego el código de país ISO3166 Alpha-2 (p. ej. fr_FR para Francés/Francia).
En este capítulo, aprenderas cómo preparar una aplicación para apoyar varias configuraciones regionales, y luego, cómo crear traducciones para múltiples regiones. En general, el proceso tiene varios pasos comunes:
La traducción está a cargo de un servicio Traductor que utiliza la configuración regional del usuario para buscar y devolver mensajes traducidos. Antes de usarlo, habilita el Traductor en tu configuración:
# app/config/config.yml
framework:
translator: { fallback: en }
<!-- app/config/config.xml -->
<framework:config>
<framework:translator fallback="en" />
</framework:config>
// app/config/config.php
$container->loadFromExtension('framework', array(
'translator' => array('fallback' => 'en'),
));
La opción fallback define la configuración regional de reserva cuando la traducción no existe en la configuración regional del usuario.
Truco
Cuando la traducción no existe para una configuración regional, el traductor primero intenta encontrar la traducción para el idioma («es» si la región es «es_MX» por ejemplo). Si esto también falla, busca una traducción utilizando la configuración regional de reserva.
La región utilizada en las traducciones es la almacenada en la petición. Esta, normalmente se establece en el atributo _locale de tus rutas (ve Locale y la URL).
La traducción de texto se hace a través del servicio traductor (Symfony\Component\Translation\Translator). Para traducir un bloque de texto (llamado un mensaje), utiliza el método trans(). Supongamos, por ejemplo, que estás traduciendo un simple mensaje desde el interior de un controlador:
// ...
use Symfony\Component\HttpFoundation\Response;
public function indexAction()
{
$t = $this->get('translator')->trans('Symfony2 is great');
return new Response($t);
}
Al ejecutar este código, Symfony2 tratará de traducir el mensaje «Symfony2 is great», basándose en la variable locale del usuario. Para que esto funcione, tenemos que decirle a Symfony2 la manera de traducir el mensaje a través de un «recurso de traducción», el cual es una colección de mensajes traducidos para una determinada configuración regional. Este «diccionario» de traducciones se puede crear en varios formatos diferentes, XLIFF es el formato recomendado:
<!-- messages.fr.xliff -->
<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="1">
<source>Symfony2 is great</source>
<target>J'aime Symfony2</target>
</trans-unit>
</body>
</file>
</xliff>
// messages.fr.php
return array(
'Symfony2 is great' => 'J\'aime Symfony2',
);
# messages.fr.yml
Symfony2 is great: J'aime Symfony2
Ahora, si el idioma de la configuración regional del usuario es el Francés (por ejemplo, fr_FR o fr_BE), el mensaje será traducido a J'aime Symfony2.
Para empezar a traducir el mensaje, Symfony2 utiliza un sencillo proceso:
Cuando se usa el método trans(), Symfony2 busca la cadena exacta dentro del catálogo de mensajes apropiado y la devuelve (si existe).
A veces, debes traducir un mensaje que contiene una variable:
// ...
use Symfony\Component\HttpFoundation\Response;
public function indexAction($name)
{
$t = $this->get('translator')->trans('Hello '.$name);
return new Response($t);
}
Sin embargo, la creación de una traducción de esta cadena es imposible, ya que el traductor tratará de buscar el mensaje exacto, incluyendo las porciones variables (por ejemplo, «Hello Ryan» o «Hello Fabien»). En lugar de escribir una traducción de cada posible iteración de la variable $name, podemos reemplazar la variable con un «marcador de posición»:
// ...
use Symfony\Component\HttpFoundation\Response;
public function indexAction($name)
{
$t = $this->get('translator')->trans(
'Hello %name%',
array('%name%' => $name)
);
return new Response($t);
}
Symfony2 ahora busca una traducción del mensaje en bruto (Hello %name%) y después reemplaza los marcadores de posición con sus valores. La creación de una traducción se hace igual que antes:
<!-- messages.fr.xliff -->
<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="1">
<source>Hello %name%</source>
<target>Bonjour %name%</target>
</trans-unit>
</body>
</file>
</xliff>
// messages.fr.php
return array(
'Hello %name%' => 'Bonjour %name%',
);
# messages.fr.yml
'Hello %name%': Bonjour %name%
Nota
Los marcadores de posición pueden tomar cualquier forma, el mensaje completo se reconstruye usando la función strtr de PHP. Sin embargo, se requiere la notación %var% cuando se traduce en plantillas Twig, y en general es un convenio razonable a seguir.
Como ya viste, la creación de una traducción es un proceso de dos pasos:
El segundo paso se realiza creando catálogos de mensajes que definen las traducciones para cualquier número de regiones diferentes.
Cuando se traduce un mensaje, Symfony2 compila un catálogo de mensajes para la configuración regional del usuario y busca en ese una traducción del mensaje. Un catálogo de mensajes es como un diccionario de traducciones para una configuración regional específica. Por ejemplo, el catálogo de la configuración regional fr_FR podría contener la siguiente traducción:
Symfony2 is Great => J'aime Symfony2
Es responsabilidad del desarrollador (o traductor) de una aplicación internacionalizada crear estas traducciones. Las traducciones son almacenadas en el sistema de archivos y descubiertas por Symfony, gracias a algunos convenios.
Truco
Cada vez que creas un nuevo recurso de traducción (o instalas un paquete que incluye un recurso de traducción), para que Symfony pueda descubrir el nuevo recurso de traducción, asegúrate de borrar la caché con la siguiente orden:
$ php app/console cache:clear
Symfony2 busca archivos de mensajes (es decir, traducciones) en los siguientes lugares:
Las ubicaciones están enumeradas basándose en su prioridad. Por lo tanto puedes redefinir la traducción de los mensajes de un paquete en cualquiera de los 2 directorios superiores.
El mecanismo de sustitución trabaja a nivel de claves: sólo es necesario enumerar las claves remplazadas en un archivo de mensajes de alta prioridad. Cuando una clave no se encuentra en un archivo de mensajes, el traductor automáticamente vuelve a los archivos de mensajes de reserva de menor prioridad.
El nombre del archivo de las traducciones también es importante ya que Symfony2 utiliza una convención para determinar los detalles sobre las traducciones. Cada archivo de mensaje se tiene que denominar de acuerdo a la siguiente ruta: dominio.región.cargador:
El cargador puede ser el nombre de cualquier gestor registrado. De manera predeterminada, Symfony incluye los siguientes cargadores:
La elección del cargador a utilizar es totalmente tuya y es una cuestión de gusto.
Nota
También puedes almacenar las traducciones en una base de datos, o cualquier otro almacenamiento, proporcionando una clase personalizada que implemente la interfaz Symfony\Component\Translation\Loader\LoaderInterface.
<!-- src/Acme/DemoBundle/Resources/translations/messages.fr.xliff -->
<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="1">
<source>Symfony2 is great</source>
<target>J'aime Symfony2</target>
</trans-unit>
<trans-unit id="2">
<source>symfony2.great</source>
<target>J'aime Symfony2</target>
</trans-unit>
</body>
</file>
</xliff>
// src/Acme/DemoBundle/Resources/translations/messages.fr.php
return array(
'Symfony2 is great' => 'J\'aime Symfony2',
'symfony2.great' => 'J\'aime Symfony2',
);
# src/Acme/DemoBundle/Resources/translations/messages.fr.yml
Symfony2 is great: J'aime Symfony2
symfony2.great: J'aime Symfony2
Symfony2 descubrirá estos archivos y los utilizará cuando traduce o bien «Symfony2 is great» o «symfony2.great» en un idioma regional de Francés (por ejemplo, fr_FR o fr_BE).
Como ya viste, los archivos de mensajes se organizan en las diferentes regiones en que se traducirán. Los archivos de mensajes también se pueden organizar en «dominios». Al crear archivos de mensajes, el dominio es la primera porción del nombre de archivo. El dominio predeterminado es messages. Por ejemplo, supongamos que, por organización, las traducciones se dividieron en tres diferentes dominios: messages, admin y navigation. La traducción española debe tener los siguientes archivos de mensaje:
Al traducir las cadenas que no están en el dominio predeterminado (messages), debes especificar el dominio como tercer argumento de trans():
$this->get('translator')->trans('Symfony2 is great', array(), 'admin');
Symfony2 ahora buscará el mensaje en el dominio admin de la configuración regional del usuario.
La configuración regional del usuario actual se almacena en la petición y se puede acceder a través del objeto Petición:
// accede al objeto request en un controlador estándar
$request = $this->getRequest();
$locale = $request->getLocale();
$request->setLocale('en_US');
También es posible almacenar la región en la sesión en lugar de en base a cada petición. Si lo haces, cada subsecuente petición tendrá esa región.
$this->get('session')->set('_locale', 'en_US');
En la sección Locale y la URL, más adelante, puedes ver cómo configurar la región a través del enrutado.
Si la configuración regional no se ha establecido explícitamente en la sesión, el parámetro de configuración fallback_locale será utilizado por el Traductor. El valor predeterminado del parámetro es «en» (consulta la sección Configurando).
Alternativamente, puedes garantizar que hay una región establecida en cada petición definiendo una opción default_locale en la clave framework:
# app/config/config.yml
framework:
default_locale: en
<!-- app/config/config.xml -->
<framework:config>
<framework:default-locale>en</framework:default-locale>
</framework:config>
// app/config/config.php
$container->loadFromExtension('framework', array(
'default_locale' => 'en',
));
Nuevo en la versión 2.1: El parámetro default_locale originalmente se definía bajo la clave session, sin embargo, se movió en la versión 2.1. Esto se debe a que la región ahora se establece en la petición en lugar de en la sesión.
Dado que la configuración regional del usuario se almacena en la sesión, puede ser tentador utilizar la misma URL para mostrar un recurso en muchos idiomas diferentes en función de la región del usuario. Por ejemplo, http://www.ejemplo.com/contact podría mostrar el contenido en Inglés para un usuario y en Francés para otro. Por desgracia, esto viola una norma fundamental de la Web: que una URL particular devuelve el mismo recurso, independientemente del usuario. Para acabar de enturbiar el problema, ¿cual sería la versión del contenido indexado por los motores de búsqueda?
Una mejor política es incluir la configuración regional en la URL. Esto es totalmente compatible con el sistema de enrutado mediante el parámetro especial _locale:
contact:
path: /{_locale}/contact
defaults: { _controller: AcmeDemoBundle:Contact:index, _locale: en }
requirements:
_locale: en|fr|de
<route id="contact" path="/{_locale}/contact">
<default key="_controller">AcmeDemoBundle:Contact:index</default>
<default key="_locale">en</default>
<requirement key="_locale">en|fr|de</requirement>
</route>
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add('contact', new Route('/{_locale}/contact', array(
'_controller' => 'AcmeDemoBundle:Contact:index',
'_locale' => 'en',
), array(
'_locale' => 'en|fr|de',
)));
return $collection;
Cuando utilizas el parámetro especial _locale en una ruta, la configuración regional emparejada automáticamente se establece en la sesión del usuario. En otras palabras, si un usuario visita la URI /es/contact, la región «es» se ajustará automáticamente según la configuración regional de la sesión del usuario.
Ahora puedes utilizar la configuración regional del usuario para crear rutas hacia otras páginas traducidas en tu aplicación.
La pluralización de mensajes es un tema difícil puesto que las reglas pueden ser bastante complejas. Por ejemplo, aquí tienes la representación matemática de las reglas de pluralización de Rusia:
(($number % 10 == 1) && ($number % 100 != 11))
? 0
: ((($number % 10 >= 2)
&& ($number % 10 <= 4)
&& (($number % 100 < 10)
|| ($number % 100 >= 20)))
? 1
: 2
);
Como puedes ver, en Ruso, puedes tener tres formas diferentes del plural, dando a cada una un índice de 0, 1 o 2. Para todas las formas, el plural es diferente, por lo que la traducción también es diferente.
Cuando una traducción tiene diferentes formas debido a la pluralización, puedes proporcionar todas las formas como una cadena separada por una línea vertical (|):
'Hay una manzana|Hay %count% manzanas'
Para traducir mensajes pluralizados, utiliza el método transChoice():
$t = $this->get('translator')->transChoice(
'There is one apple|There are %count% apples',
10,
array('%count%' => 10)
);
El segundo argumento (10 en este ejemplo), es el número de objetos descrito y se utiliza para determinar cual traducción usar y también para rellenar el marcador de posición %count%.
En base al número dado, el traductor elige la forma plural adecuada. En Inglés, la mayoría de las palabras tienen una forma singular cuando hay exactamente un objeto y una forma plural para todos los otros números (0, 2, 3...). Así pues, si count es 1, el traductor utilizará la primera cadena (Hay una manzana) como la traducción. De lo contrario, utilizará Hay %count% manzanas.
Aquí está la traducción al Francés:
'Il y a %count% pomme|Il y a %count% pommes'
Incluso si la cadena tiene una apariencia similar (se compone de dos subcadenas separadas por una línea vertical), las reglas francesas son diferentes: la primera forma (no plural) se utiliza cuando count es 0 o 1. Por lo tanto, el traductor utilizará automáticamente la primera cadena (Il y a %count% pomme) cuando count es 0 o 1.
Cada región tiene su propio conjunto de reglas, con algunas que tienen hasta seis formas diferentes de plural con reglas complejas detrás de las cuales los números asignan a tal forma plural. Las reglas son bastante simples para Inglés y Francés, pero para el Ruso, puedes querer una pista para saber qué regla coincide con qué cadena. Para ayudar a los traductores, puedes «etiquetar» cada cadena:
'one: There is one apple|some: There are %count% apples'
'none_or_one: Il y a %count% pomme|some: Il y a %count% pommes'
Las etiquetas realmente son pistas sólo para los traductores y no afectan a la lógica utilizada para determinar qué forma plural usar. Las etiquetas pueden ser cualquier cadena descriptiva que termine con dos puntos (:). Las etiquetas además no necesitan ser las mismas en el mensaje original cómo en la traducción.
Truco
Como las etiquetas son opcionales, el traductor no las utiliza (el traductor únicamente obtendrá una cadena basada en su posición en la cadena).
La forma más fácil de pluralizar un mensaje es dejar que Symfony2 utilice su lógica interna para elegir qué cadena se utiliza en base a un número dado. A veces, tendrás más control o quieres una traducción diferente para casos específicos (por 0, o cuando el número es negativo, por ejemplo). Para estos casos, puedes utilizar intervalos matemáticos explícitos:
'{0} There are no apples|{1} There is one apple|]1,19] There are %count% apples|[20,Inf] There are many apples'
Los intervalos siguen la notación ISO 31-11. La cadena anterior especifica cuatro intervalos diferentes: exactamente 0, exactamente 1, 2-19 y 20 y superior.
También puedes mezclar reglas matemáticas explícitas y estándar. En este caso, si la cuenta no corresponde con un intervalo específico, las reglas estándar entran en vigor después de remover las reglas explícitas:
'{0} There are no apples|[20,Inf] There are many apples|There is one apple|a_few: There are %count% apples'
Por ejemplo, para 1 apple, la regla estándar There is one apple será utilizada. Para 2-19 apples, la segunda regla estándar There are %count% apples será seleccionada.
Un Symfony\Component\Translation\Interval puede representar un conjunto finito de números:
{1,2,3,4}
O números entre otros dos números:
[1, +Inf[
]-1,2[
El delimitador izquierdo puede ser [ (inclusive) o ] (exclusivo). El delimitador derecho puede ser [ (exclusivo) o ] (inclusive). Más allá de los números, puedes usar -Inf y +Inf para el infinito.
La mayoría de las veces, la traducción ocurre en las plantillas. Symfony2 proporciona apoyo nativo para ambas plantillas Twig y PHP.
Symfony2 proporciona etiquetas Twig especializadas (trans y transchoice) para ayudar con la traducción de los mensajes de bloques de texto estático:
{% trans %}Hello %name%{% endtrans %}
{% transchoice count %}
{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples
{% endtranschoice %}
La etiqueta transchoice obtiene automáticamente la variable %count% a partir del contexto actual y la pasa al traductor. Este mecanismo sólo funciona cuando se utiliza un marcador de posición después del patrón %var%.
Truco
Si necesitas utilizar el carácter de porcentaje (%) en una cadena, lo tienes que escapar duplicando el siguiente: {% trans %}Porcentaje: %percent%%%{% endtrans %}
También puedes especificar el dominio del mensaje y pasar algunas variables adicionales:
{% trans with {'%name%': 'Fabien'} from "app" %}Hello %name%{% endtrans %}
{% trans with {'%name%': 'Fabien'} from "app" into "fr" %}Hello %name%{% endtrans %}
{% transchoice count with {'%name%': 'Fabien'} from "app" %}
{0} %name%, there are no apples|{1} %name%, there is one apple|]1,Inf] %name%, there are %count% apples
{% endtranschoice %}
Los filtros trans y transchoice se pueden utilizar para traducir texto variable y expresiones complejas:
{{ message|trans }}
{{ message|transchoice(5) }}
{{ message|trans({'%name%': 'Fabien'}, "app") }}
{{ message|transchoice(5, {'%name%': 'Fabien'}, 'app') }}
Truco
Usar etiquetas de traducción o filtros tiene el mismo efecto, pero con una sutil diferencia: la salida escapada automáticamente sólo se aplica a las traducciones usando un filtro. En otras palabras, si necesitas garantizar que tu traducción no se escape en la salida, debes aplicar el filtro raw después del filtro de traducción:
{# el texto traducido entre etiquetas nunca se escapa #}
{% trans %}
<h3>foo</h3>
{% endtrans %}
{% set message = '<h3>foo</h3>' %}
{# Las cadenas y variables traducidas vía un filtro se escapan por omisión #}
{{ message|trans|raw }}
{{ '<h3>bar</h3>'|trans|raw }}
Truco
Puedes establecer el ámbito de traducción para una plantilla Twig entera con una sola etiqueta:
{% trans_default_domain "app" %}
Ten en cuenta que esto sólo influye en la plantilla actual, no en las plantillas «incluidas» (con el fin de evitar efectos secundarios).
Nuevo en la versión 2.1: La etiqueta trans_default_domain es nueva en Symfony2.1
El servicio traductor es accesible en plantillas PHP a través del ayudante translator:
<?php echo $view['translator']->trans('Symfony2 is great') ?>
<?php echo $view['translator']->transChoice(
'{0} There is no apples|{1} There is one apple|]1,Inf[ There are %count% apples',
10,
array('%count%' => 10)
) ?>
Al traducir un mensaje, Symfony2 utiliza la configuración regional de la petición o la configuración regional de reserva si es necesario. También puedes especificar manualmente la configuración regional utilizada para la traducción:
$this->get('translator')->trans(
'Symfony2 is great',
array(),
'messages',
'fr_FR'
);
$this->get('translator')->transChoice(
'{0} There are no apples|{1} There is one apple|]1,Inf[ There are %count% apples',
10,
array('%count%' => 10),
'messages',
'fr_FR'
);
La traducción del contenido de la base de datos la debe manejar Doctrine a través de la Extensión Translatable. Para más información, consulta la documentación de la biblioteca.
La mejor manera de entender la traducción de las restricciones es verla en acción. Para empezar, supongamos que creaste un sencillo objeto PHP el cual en algún lugar tiene que utilizar tu aplicación:
// src/Acme/BlogBundle/Entity/Author.php
namespace Acme\BlogBundle\Entity;
class Author
{
public $name;
}
Añade las restricciones con cualquiera de los métodos admitidos. Configura la opción de mensaje al texto fuente traducido. Por ejemplo, para garantizar que la propiedad $name no esté vacía, agrega lo siguiente:
# src/Acme/BlogBundle/Resources/config/validation.yml
Acme\BlogBundle\Entity\Author:
properties:
name:
- NotBlank: { message: "author.name.not_blank" }
// src/Acme/BlogBundle/Entity/Author.php
use Symfony\Component\Validator\Constraints as Assert;
class Autor
{
/**
* @Assert\NotBlank(message = "author.name.not_blank")
*/
public $name;
}
<!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
<class name="Acme\BlogBundle\Entity\Author">
<property name="name">
<constraint name="NotBlank">
<option name="message">author.name.not_blank</option>
</constraint>
</property>
</class>
</constraint-mapping>
// src/Acme/BlogBundle/Entity/Author.php
// ...
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Constraints\NotBlank;
class Autor
{
public $name;
public static function loadValidatorMetadata(ClassMetadata $metadata)
{
$metadata->addPropertyConstraint('name', new NotBlank(array(
'message' => 'author.name.not_blank',
)));
}
}
Crea un archivo de traducción bajo el catálogo validators para los mensajes de restricción, por lo general en el directorio Resources/translations/ del paquete. Consulta Catálogos de mensajes para más detalles;
<!-- validators.en.xliff -->
<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="1">
<source>author.name.not_blank</source>
<target>Por favor, ingresa un nombre de autor.</target>
</trans-unit>
</body>
</file>
</xliff>
// validators.en.php
return array(
'author.name.not_blank' => 'Por favor, ingresa un nombre de autor.',
);
# validators.en.yml
author.name.not_blank: Por favor, ingresa un nombre de autor..
Con el componente Translation de Symfony2, la creación de una aplicación internacionalizada ya no tiene que ser un proceso doloroso y se reduce a sólo algunos pasos básicos: