Skip to content
Caution: You are browsing the legacy symfony 1.x part of this website.

Día 19: Internacionalización y Localización

Symfony version
Language
ORM

Ayer se terminó la función de motor de búsqueda, haciéndola más divertida con la incorporación de algunas bondades AJAX. Hoy, vamos a hablar de internacionalización (o i18n) y localización (o l10n). Según Wikipedia:en:

La Internacionalización es un proceso a través del cual se diseñan productos de software para que puedan adaptarse a diferentes idiomas y regiones sin necesidad de cambios de ingeniería ni cambios en el código.

La Localización es el proceso de adaptación de software para una región o idioma mediante la incorporación de componentes específicos de localización y traducción de textos.

Como siempre, el framework symfony no ha reinventado la rueda y su soporte de i18n y l10n esta basado en el ICU standard.

Usuario

La internacionalización no es posible sin un usuario. Cuando su sitio web está disponible en varios idiomas o para distintas regiones del mundo, el usuario es el responsable de elegir la que mejor se ajuste a él.

note

Ya hemos hablado de la clase User de symfony durante el día 13.

La Cultura del Usuario

Las características i18n y l10n de symfony se basan en la cultura del usuario. La cultura es la combinación del lenguaje y el país del usuario. Por ejemplo, la cultura para un usuario que habla francés es fr y la cultura para un usuario de Francia es fr_FR.

Puedes manejar la cultura por el usuario llamando a los métodos setCulture() y getCulture() del objeto User:

// in an action
$this->getUser()->setCulture('fr_BE');
echo $this->getUser()->getCulture();

tip

El lenguaje está codificado en dos minúsculas, de acuerdo con la ISO 639-1 standard, y el país está codificado con dos caracteres en mayúscula, de acuerdo con la ISO 3166-1 standard.

La Preferencia de Cultura

Por defecto, la cultura del usuario es la configurada en el archivo de configuración settings.yml:

# apps/frontend/config/settings.yml
all:
  .settings:
    default_culture: it_IT

tip

Como la cultura es administrada por el objeto User, se almacena en la sesión del usuario. Durante el desarrollo, si cambias la cultura por defecto, tendrás que limpiar tus cookies de sesión para que el nuevo valor tenga efecto en tu navegador.

Cuando un usuario inicia una sesión en el sitio web Jobeet, también podemos determinar la mejor cultura, sobre la base de la información proporcionada por la cabecera HTTP Accept-Language.

El método getLanguages() del objeto de la petición devuelve un array de los idiomas aceptados para el usuario actual, ordenados por orden de preferencia:

// in an action
$languages = $request->getLanguages();

Pero la mayor parte del tiempo, tu sitio web no estará disponible en los 136 principales idiomas. El método getPreferredCulture() devuelve el mejor lenguaje mediante la comparación de los idiomas preferidos del usuario y los idiomas de tu sitio web:

// in an action
$language = $request->getPreferredCulture(array('en', 'fr'));

En la anterior llamada, el lenguaje devuelto será Inglés o Francés de acuerdo con los idiomas preferidos del usuario, o Inglés (primer idioma en el array) si no coincide ninguno.

La Cultura en la URL

El sitio web Jobeet estará disponible en Inglés y francés. Como una dirección URL sólo puede representar a un único recurso, la cultura debe estar integrada en la URL. Para ello, abre el archivo routing.yml, y agrega la variable especial :sf_culture para todas las rutas, pero no para api_jobs y homepage. Para simples rutas, agrega /:sf_culture al principio de la url. Para colección de rutas, agrega una opción prefix_path que comience con /:sf_culture.

# apps/frontend/config/routing.yml
affiliate:
  class: sfPropelRouteCollection
  options:
    model:          JobeetAffiliate
    actions:        [new, create]
    object_actions: { wait: get }
    prefix_path:    /:sf_culture/affiliate
 
category:
  url:     /:sf_culture/category/:slug.:sf_format
  class:   sfPropelRoute
  param:   { module: category, action: show, sf_format: html }
  options: { model: JobeetCategory, type: object }
  requirements:
    sf_format: (?:html|atom)
 
job_search:
  url:   /:sf_culture/search
  param: { module: job, action: search }
 
job:
  class: sfPropelRouteCollection
  options:
    model:          JobeetJob
    column:         token
    object_actions: { publish: put, extend: put }
    prefix_path:    /:sf_culture/job
  requirements:
    token: \w+
 
job_show_user:
  url:     /:sf_culture/job/:company_slug/:location_slug/:id/:position_slug
  class:   sfPropelRoute
  options:
    model: JobeetJob
    type: object
    method_for_criteria: doSelectActive
  param:   { module: job, action: show }
  requirements:
    id:        \d+
    sf_method: get

Cuando la variable sf_culture se utiliza en una ruta, symfony automáticamente usa su valor para cambiar la cultura del usuario.

Como necesitamos muchas páginas de inicio como idiomas soportemos (/en/, /fr/, ...), la página de inicio predeterminada (/) deben redirijirnos a la página apropiada, de acuerdo con la cultura del usuario. Pero si el usuario no tiene todavía una cultura, porque él viene a Jobeet por primera vez, la mejor cultura serán elegidos para él.

En primer lugar, añade el método isFirstRequest() a myUser. Devuelve true sólo para la primer petición de una sesión de usuario:

// apps/frontend/lib/myUser.class.php
public function isFirstRequest($boolean = null)
{
  if (is_null($boolean))
  {
    return $this->getAttribute('first_request', true);
  }
  else
  {
    $this->setAttribute('first_request', $boolean);
  }
}

Agrega una ruta localized_homepage:

# apps/frontend/config/routing.yml
localized_homepage:
  url:   /:sf_culture/
  param: { module: job, action: index }
  requirements:
    sf_culture: (?:fr|en)

Cambia la acción index del módulo job para aplicar la lógica para redirigir al usuario a la "mejor" página de inicio la primer petición de una sesión:

// apps/frontend/modules/job/actions/actions.class.php
public function executeIndex(sfWebRequest $request)
{
  if (!$request->getParameter('sf_culture'))
  {
    if ($this->getUser()->isFirstRequest())
    {
      $culture = $request->getPreferredCulture(array('en', 'fr'));
      $this->getUser()->setCulture($culture);
      $this->getUser()->isFirstRequest(false);
    }
    else
    {
      $culture = $this->getUser()->getCulture();
    }
 
    $this->redirect('@localized_homepage');
  }
 
  $this->categories = JobeetCategoryPeer::getWithJobs();
}

Si la variable sf_culture no está presente en la petición, esto significa que el usuario tiene que ir a la URL /. Si este es el caso y la sesión es nueva, la cultura preferida es usada como la cultura del usuario. De lo contrario, se utiliza la cultura actual del usuario.

El último paso es redirigir al usuario a la URL localized_homepage. Nota que la variable sf_culture no ha sido pasada en la redirección ya que symfony la agrega automáticamente por tí.

Ahora, si tratas de ir a la URL /it/, symfony devolverá un error 404 ya que restringimos la variable sf_culture a en, o fr. Agrega este requisito para todas las rutas que incluyan la cultura:

requirements:
  sf_culture: (?:fr|en)

Probando la Cultura

Es hora de poner a prueba nuestra aplicación. Pero antes de añadir más pruebas, tenemos que arreglar los ya existentes. Como han cambiado todas las direcciones URL, edita los archivos de todas las prueba funcionales en test/functional/frontend/ y agrega /en al principio de todas las URLs. No olvides de cambiar las URLs en el archivo lib/test/JobeetTestFunctional.class.php. Poner en marcha el conjunto de pruebas para comprobar que has arreglado correctamente las pruebas:

$ php symfony test:functional frontend

El user tester da un método isCulture() que prueba la cultura del usuario actual. Abre el archivo jobActionsTest y añade las siguientes pruebas:

// test/functional/frontend/jobActionsTest.php
$browser->setHttpHeader('ACCEPT_LANGUAGE', 'fr_FR,fr,en;q=0.7');
$browser->
  info('6 - User culture')->
 
  restart()->
 
  info('  6.1 - For the first request, symfony guesses the best culture')->
  get('/')->
  isRedirected()->followRedirect()->
  with('user')->isCulture('fr')->
 
  info('  6.2 - Available cultures are en and fr')->
  get('/it/')->
  with('response')->isStatusCode(404)
;
 
$browser->setHttpHeader('ACCEPT_LANGUAGE', 'en,fr;q=0.7');
$browser->
  info('  6.3 - The culture guessing is only for the first request')->
 
  get('/')->
  isRedirected()->followRedirect()->
  with('user')->isCulture('fr')
;

Cambiando de idioma

Para que el usuario pueda cambiar la cultura, un formulario de idioma hay que añadir en el layout. El framework de formularios no proporciona una formulario de fabrica pero como la necesidad es muy común para los sitios web internacionalizados, el symfony core team mantiene el sfFormExtraPlugin, que contiene los validadores, widgets, y formularios que no pueden ser incluidos con el paquete principal symfony ya que son demasiado específicas o tienen dependencias externas, pero no obstante son muy útil.

Instala el plugin con la tarea plugin:install:

$ php symfony plugin:install sfFormExtraPlugin

Limpia el cache ya que el plugin define nuevas clases:

$ php symfony cc

note

El sfFormExtraPlugin tiene widgets que requieran dependencias externas como bibliotecas JavaScript. Encontrarás un widget para seleccionar fechas, un para un editor WYSIWYG, y mucho más. Tóma un tiempo para leer la documentación ya que encontrarás un montón de cosas útiles.

El plugin sfFormExtraPlugin da un formulario sfFormLanguage para gestionar la selección de idioma. Añadiendo el formulario de idiomas se puede hacer en el layout así:

note

El código a continuación no pretende ser aplicado. Es aquí que te mostramos cómo podrías tener la tentación de aplicar algo de forma equivocada. Vamos a mostrarte cómo aplicarlo correctamente utilizando symfony.

// apps/frontend/templates/layout.php
<div id="footer">
  <div class="content">
    <!-- footer content -->
 
    <?php $form = new sfFormLanguage(
      $sf_user,
      array('languages' => array('en', 'fr'))
      )
    ?>
    <form action="<?php echo url_for('@change_language') ?>">
      <?php echo $form ?><input type="submit" value="ok" />
    </form>
  </div>
</div>

¿Detectas el problema? Así es, la creación de un objeto form no pertenece a la capa de la Vista. Debe ser creado en una acción. Pero como el código está en el layout, el formulario debe crearse para cada acción, que está lejos de ser práctico. En tales casos, debes usar un componente. Un componente es como un partial pero con algo de código en él. Consideralo una acción ligera.

Incluyendo un componente en una plantilla se puede hacer mediante el uso del helper include_component():

// apps/frontend/templates/layout.php
<div id="footer">
  <div class="content">
    <!-- footer content -->
 
    <?php include_component('language', 'language') ?>
  </div>
</div>

El helper toma el módulo y la acción como argumentos. El tercer argumento se puede utilizar para pasar parámetros a los componentes.

Crea un módulo language para alojar el componente y la acción que realmente cambiará el idioma del usuario:

$ php symfony generate:module frontend language

Los Componentes se definirán en el archivo actions/components.class.php.

Crear este archivo ahora:

// apps/frontend/modules/language/actions/components.class.php
class languageComponents extends sfComponents
{
  public function executeLanguage(sfWebRequest $request)
  {
    $this->form = new sfFormLanguage(
      $this->getUser(),
      array('languages' => array('en', 'fr'))
    );
  }
}

Como puedes ver, una clase de componentes es muy similar a una clase de acciones.

La plantilla para un componente utiliza la misma convención de nombres como lo hace un partial: un guión bajo (_) seguido por el nombre del componente:

// apps/frontend/modules/language/templates/_language.php
<form action="<?php echo url_for('@change_language') ?>">
  <?php echo $form ?><input type="submit" value="ok" />
</form>

Como el plugin no proporciona la acción que en realidad cambia la cultura del usuario, edita el archivo routing.yml para crear la ruta change_language:

# apps/frontend/config/routing.yml
change_language:
  url:   /change_language
  param: { module: language, action: changeLanguage }

Y crea la acción correspondiente:

// apps/frontend/modules/language/actions/actions.class.php
class languageActions extends sfActions
{
  public function executeChangeLanguage(sfWebRequest $request)
  {
    $form = new sfFormLanguage(
      $this->getUser(),
      array('languages' => array('en', 'fr'))
    );
 
    $form->process($request);
 
    return $this->redirect('@localized_homepage');
  }
}

El método process() de sfFormLanguage se encarga de cambiar la cultura del usuario, basado en el formulario envíado por el usuario.

Internationalized Footer

Internacionalización

Idiomas, Caracteres, y Codificación

Diferentes idiomas tienen diferentes conjuntos de caracteres. El Inglés es el idioma más simple ya que sólo usa los caracteres ASCII, el idioma francés es un poco más complejo, con caracteres acentuados como "é", y las lenguas como el ruso, chino o árabe son mucho más complejos que todos sus caracteres ya que están fuera del rango ASCII. Esos idiomas se definen con diferentes conjuntos de caracteres.

Cuando se trate de datos internacionalizado, es mejor utilizar la norma Unicode. La idea detrás de Unicode es establecer un conjunto universal de caracteres que contiene todos los caracteres de todos los idiomas. El problema con Unicode es que un solo carácter se puede representar con una cantidad de 21 bits. Por lo tanto, para la web, usamos UTF-8, que mapea el código Unicode apuntandolo a una secuencias de longitud variable de octetos. En UTF-8, la mayoría de las lenguas tienen sus caracteres codificados con menos de 3 bits.

UTF-8 es el utilizado por defecto en symfony, y se define en el archivo de configuración settings.yml:

# apps/frontend/config/settings.yml
all:
  .settings:
    charset: utf-8

Además, para habilitar la capa de internacionalización de symfony, debes establecer i18n en on dentro de settings.yml:

# apps/frontend/config/settings.yml
all:
  .settings:
    i18n: on

Plantillas

Un sitio web internacionalizado significa que la interfaz de usuario está traducida a varios idiomas.

En una plantilla, todas las cadenas que dependen del idioma deben ser envueltas con el helper __() (nota que hay dos guiones bajos).

El helper __() es parte del grupo de helpers I18N, que contiene helpers que facilitan la gestión i18n en plantillas. Como este grupo de helper no está cargado por defecto, es necesario agregar manualmente en cada plantilla use_helper('I18N') como ya hizo para el grupo de helper Text, o cargalo a nivel global mediante standard_helpers:

# apps/frontend/config/settings.yml
all:
  .settings:
    standard_helpers: [Partial, Cache, I18N]

Aquí está cómo usa el helper __() para el pie de página de Jobeet:

// apps/frontend/templates/layout.php
<div id="footer">
  <div class="content">
    <span class="symfony">
      <img src="/legacy/images/jobeet-mini.png" />
      powered by <a href="/">
      <img src="/legacy/images/symfony.gif" alt="symfony framework" /></a>
    </span>
    <ul>
      <li>
        <a href=""><?php echo __('About Jobeet') ?></a>
      </li>
      <li class="feed">
        <?php echo link_to(__('Full feed'), '@job?sf_format=atom') ?>
      </li>
      <li>
        <a href=""><?php echo __('Jobeet API') ?></a>
      </li>
      <li class="last">
        <?php echo link_to(__('Become an affiliate'), '@affiliate_new') ?>
      </li>
    </ul>
    <?php include_component('language', 'language') ?>
  </div>
</div>

note

El helper __() puede tomar la cadena para el idioma por defecto o se puede utilizar también un identificador único para cada cadena. Es sólo una cuestión de gusto. Para Jobeet, haremos uso de la antigua estrategia para tener plantillas más legibles.

Cuando symfony muestra una plantilla, cada vez que el helper __() es llamado, symfony busca por una traducción para la cultura del usuario actual. Si se encuentra una traducción, se utiliza, si no, el primer argumento se devuelve como un valor fallback.

Todas las traducciones se almacenan en un catálogo. El framework i18n proporciona una gran cantidad de estrategias diferentes para almacenar las traducciones. Vamos a utilizar el formato "XLIFF", que es un estándar y el más flexible. También es el utilizado por el admin generator y demás symfony plugins.

note

Oros Catálogos son gettext, MySQL, y SQLite. Como siempre, echa una mirada a la i18n API para más detalles.

i18n:extract

En lugar de crear el catálogo de archivos a mano, utiliza la tarea de serie i18n:extract:

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

La tarea i18n:extract encuentra todas las cadenas que deben traducirse en fr en la aplicación frontend y crea o actualiza el correspondiente catálogo. La opción --auto-save guarda las nuevas cadenas de en el catálogo. También puedes utilizar la opción --auto-delete para eliminar automáticamente las cadenas que ya no existen.

En nuestro caso, rellena el archivo que hemos creado:

<!-- apps/frontend/i18n/fr/messages.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 source-language="EN" target-language="fr" datatype="plaintext"
      original="messages" date="2008-12-14T12:11:22Z"
      product-name="messages">
    <header/>
    <body>
      <trans-unit id="1">
        <source>About Jobeet</source>
        <target/>
      </trans-unit>
      <trans-unit id="2">
        <source>Feed</source>
        <target/>
      </trans-unit>
      <trans-unit id="3">
        <source>Jobeet API</source>
        <target/>
      </trans-unit>
      <trans-unit id="4">
        <source>Become an affiliate</source>
        <target/>
      </trans-unit>
    </body>
  </file>
</xliff>

Cada traducción es administrada por una etiqueta trans-unit que tiene un único atributo id. Ahora puedes editar este archivo y añadir las traducciones de la lengua francesa:

<!-- apps/frontend/i18n/fr/messages.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 source-language="EN" target-language="fr" datatype="plaintext"
      original="messages" date="2008-12-14T12:11:22Z"
      product-name="messages">
    <header/>
    <body>
      <trans-unit id="1">
        <source>About Jobeet</source>
        <target>A propos de Jobeet</target>
      </trans-unit>
      <trans-unit id="2">
        <source>Feed</source>
        <target>Fil RSS</target>
      </trans-unit>
      <trans-unit id="3">
        <source>Jobeet API</source>
        <target>API Jobeet</target>
      </trans-unit>
      <trans-unit id="4">
        <source>Become an affiliate</source>
        <target>Devenir un affilié</target>
      </trans-unit>
    </body>
  </file>
</xliff>

tip

Como XLIFF es un formato estándar, una gran cantidad de herramientas existentes facilitan el proceso de traducción. Open Language Tools es un proyecto Java Open-Source con un editor integrado XLIFF.

tip

Como XLIFF es un formato de archivo, la misma prioridad y la lógica de las normas que existen para otros archivos de configuración de symfony son también aplicables. Los archivos I18n puede existir en un proyecto, una aplicación o un módulo, y los más específicos archivos sobreescriben las traducciones que se encuentran en la más global.

Traducciones con Argumentos

El principio fundamental detrás de la internacionalización es traducir frases. Sin embargo, algunas frases incluyen valores dinámicos. En Jobeet, este es el caso en la página de inicio para el enlace "and X more...":

<!-- apps/frontend/modules/job/templates/indexSuccess.php -->
<div class="more_jobs">
  and <?php echo link_to($count, 'category', $category) ?> more...
</div>

El número de puestos de trabajo es una variable que debe ser utilizada para la traducción:

<!-- apps/frontend/modules/job/templates/indexSuccess.php -->
<div class="more_jobs">
  <?php echo __('and %count% more...', array('%count%' => link_to($count, 'category', $category))) ?>
</div>

La cadena a traducir es ahora "and %count% more...", y el %count% es la variable que será sustituido por el número real en tiempo de ejecución, gracias a el valor dado como segundo argumento al helper __().

Añadir la nueva cadena manualmente insertando una etiqueta trans-unit en el archivo messages.xml, o usa la tarea i18n:extract para actualizar automáticamente el archivo:

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

Después de ejecutar la tarea, abre el archivo XLIFF para añadir la traducción al francés:

<trans-unit id="5">
  <source>and %count% more...</source>
  <target>et %count% autres...</target>
</trans-unit>

El único requisito en la tradución de la cadena es utilizar el contenedor/variable %count% en algún lugar.

Algunas otras cadenas son aún más complejas ya que implican plurales. Según algunoss números, la frases cambian, pero no necesariamente del mismo modo para todos los idiomas. Algunos idiomas tienen reglas gramaticales muy complejas para los plurales, como el Polaco o el Ruso.

En la página de categoría, el número de puestos de trabajo en la categoría actual se muestra:

<!-- apps/frontend/modules/category/templates/showSuccess.php -->
<strong><?php echo $pager->getNbResults() ?></strong> jobs in this category

Cuando una oración tiene diferentes traducciones de acuerdo con un número, el helper format_number_choice() debe utilizarse:

<?php echo format_number_choice(
    '[0]No job in this category|[1]One job in this category|(1,+Inf]%count% jobs in this category',
    array('%count%' => '<strong>'.$pager->getNbResults().'</strong>'),
    $pager->getNbResults()
  )
?>

El helper format_number_choice() tiene tres argumentos:

  • La cadena a utilizar en función del número
  • Un array de variables a reemplazar
  • El número a usar que determina qué texto usar

La cadena que describe las diferentes traducciones de acuerdo con el número tiene un formato de la siguiente manera:

  • Cada posibilidad está separado por un carácter barra vertical (|)
  • Cada cadena se compone de un rango seguida de la traducción

El rango puede describirse con cualquier serie de números:

  • [1,2]: Acepta valores entre 1 y 2, inclusive
  • (1,2): Acepta valores entre 1 y 2, con exclusión de 1 y 2
  • {1,2,3,4}: Sólo los valores definidos en el juego son aceptadas
  • [-Inf,0): Acepta los valores mayores o iguales a menos infinito y estrictamente inferior a 0
  • {n: n % 10 > 1 && n % 10 < 5}: Coincide con los números 2, 3, 4, 22, 23, 24

Traducir la cadena es similar a otras cadenas de mensajes:

<trans-unit id="6">
  <source>[0]No job in this category|[1]One job in this category|(1,+Inf]%count% jobs in this category</source>
  <target>[0]Aucune annonce dans cette catégorie|[1]Une annonce dans cette catégorie|(1,+Inf]%count% annonces dans cette catégorie</target>
</trans-unit>

Ahora que sabes cómo internacionalizar todo tipo de cadenas, tomate un tiempo para agregar una llamada al __() para todas las plantillas de la aplicación frontend. No vamos a internacionalizar la aplicación backend.

Formularios

Las clases form contienen muchas cadenas que deben ser traducidas, como etiquetas, mensajes de error y mensajes de ayuda. Todas estas cadenas son automáticamente internacionalizadas por symfony, por lo que sólo tendrá que proporcionar las traducciones en los archivos XLIFF.

note

Lamentablemente, la tarea i18n:extract aún no analiza las clases form para cadenas sin traducir.

Objetos Propel

Por el sitio web Jobeet, no internacionalizaremos todas las tablas porque no tiene sentido pedir a los usuarios que envían puestos que lo hagan junto con las traducciones en todos los idiomas disponibles. Sin embargo, la tabla category definitivamente debe traducirse.

El plugin Propel da soporte a tablas i18n en forma nativa. Para cada tabla que contiene datos localizados, dos tablas deben crearse: una para las columnas que sean i18n-independent, y la otra para las columnas que deben ser internacionalizadas. Las dos tablas están vinculadas por una relación de uno-a-muchos.

Actualiza el schema.yml como sigue:

# config/schema.yml
jobeet_category:
  _attributes:  { isI18N: true, i18nTable: jobeet_category_i18n }
  id:           ~
 
jobeet_category_i18n:
  id:           { type: integer, required: true, primaryKey: true, foreignTable: jobeet_category, foreignReference: id }
  culture:      { isCulture: true, type: varchar, size: 7, required: true, primaryKey: true }
  name:         { type: varchar(255), required: true }
  slug:         { type: varchar(255), required: true }

La opción _attributes define las opciones para la tabla.

Y actualiza los archivos de datos para las categorías:

# data/fixtures/010_categories.yml
JobeetCategory:
  design:        { }
  programming:   { }
  manager:       { }
  administrator: { }
 
JobeetCategoryI18n:
  design_en:        { id: design, culture: en, name: Design }
  programming_en:   { id: programming, culture: en, name: Programming }
  manager_en:       { id: manager, culture: en, name: Manager }
  administrator_en: { id: administrator, culture: en, name: Administrator }
 
  design_fr:        { id: design, culture: fr, name: Design }
  programming_fr:   { id: programming, culture: fr, name: Programmation }
  manager_fr:       { id: manager, culture: fr, name: Manager }
  administrator_fr: { id: administrator, culture: fr, name: Administrateur }

Reconstruye el modelo para crear las clase relacionadas i18n:

$ php symfony propel:build-all --no-confirmation
$ php symfony cc

Como las columnas name y slug se han trasladado a la tabla i18n, mueve el método setName() de JobeetCategory a JobeetCategoryI18n:

// lib/model/JobeetCategoryI18n.php
public function setName($name)
{
  parent::setName($name);
 
  $this->setSlug(Jobeet::slugify($name));
}

También tenemos que qrreglar el método getForSlug() en JobeetCategoryPeer:

// lib/model/JobeetCategoryPeer.php
static public function getForSlug($slug)
{
  $criteria = new Criteria();
  $criteria->addJoin(JobeetCategoryI18nPeer::ID, self::ID);
  $criteria->add(JobeetCategoryI18nPeer::CULTURE, 'en');
  $criteria->add(JobeetCategoryI18nPeer::SLUG, $slug);
 
  return self::doSelectOne($criteria);
}

tip

Como propel:build-all-load remueve todas las tablas y los datos de la base de datos, no olvides de volver a crear un usuario para acceder al Jobeet backend con la tarea guard:create-user. Si lo prefieres, puedes añadir un archivo de datos para añadirlo automáticamente.

Cuando hacemos la construcción del modelo, symfony crea métodos proxy en el objeto JobeetCategory para convenientemente acceder a las columnas i18n definidas en JobeetCategoryI18n:

$category = new JobeetCategory();
 
$category->setName('foo');       // sets the name for the current culture
$category->setName('foo', 'fr'); // sets the name for French
 
echo $category->getName();     // gets the name for the current culture
echo $category->getName('fr'); // gets the name for French

tip

Para reducir el número de solicitudes a la bases de datos, utiliza el método doSelectWithI18n() en lugar del doSelect(). Recuperarás el objeto principal y el i18n en una petición.

$categories = JobeetCategoryPeer::doSelectWithI18n($c, $culture);

Como la ruta category es apunta a la modelo de clase JobeetCategory porque el slug es ahora parte de JobeetCategoryI18n, la ruta no está disponible para traer el objeto Category automáticamente. Para ayudar al routing, vamos a crear un método que se encargará de la recuperación del objeto:

// lib/model/JobeetCategoryPeer.php
class JobeetCategoryPeer extends BaseJobeetCategoryPeer
{
  static public function doSelectForSlug($parameters)
  {
    $criteria = new Criteria();
    $criteria->addJoin(JobeetCategoryI18nPeer::ID, JobeetCategoryPeer::ID);
    $criteria->add(JobeetCategoryI18nPeer::CULTURE, $parameters['sf_culture']);
    $criteria->add(JobeetCategoryI18nPeer::SLUG, $parameters['slug']);
 
    return self::doSelectOne($criteria);
  }
}

A continuación, utiliza la opción method para decirle a la ruta category que use el método doSelectForSlug() para recuperar el objeto:

# apps/frontend/config/routing.yml
category:
  url:     /:sf_culture/category/:slug.:sf_format
  class:   sfPropelRoute
  param:   { module: category, action: show, sf_format: html }
  options: { model: JobeetCategory, type: object, method: doSelectForSlug }
  requirements:
    sf_format: (?:html|atom)

Necsesitamos recargar los datos para regenerar los slugs correctos para las categoríaes:

$ php symfony propel:data-load

Ahora la ruta category se encuentra internacionalizado y la URL de una categoría incluye las traducciones del slug:

/frontend_dev.php/fr/category/programmation
/frontend_dev.php/en/category/programming

Admin Generador

Debido a un error en symfony 1.2.1, es necesario comentar el titleen la secciónedit`:

# apps/backend/modules/category/config/generator.yml
edit:
  #title: Editing Category "%%name%%" (#%%id%%)

Para el backend, queremos que las traducciones de el francés y el Inglés sean editadas en el mismo formulario:

Backend categories

Incluir un formulario i18n se puede hacer mediante el uso del método embedI18N():

// lib/form/JobeetCategoryForm.class.php
class JobeetCategoryForm extends BaseJobeetCategoryForm
{
  public function configure()
  {
    unset($this['jobeet_category_affiliate_list']);
 
    $this->embedI18n(array('en', 'fr'));
    $this->widgetSchema->setLabel('en', 'English');
    $this->widgetSchema->setLabel('fr', 'French');
  }
}

La interfaz del admin generator soporta internacionalización de fabrica. Viene con traducciones a más de 20 idiomas, y es muy fácil de añadir uno nuevo, o para personalizar una existente. Copie el archivo para el idioma que desea personalizar de symfony (las traducciones admin se encuentran en lib/vendor/symfony/lib/plugins/sfPropelPlugin/i18n/) de la aplicación en el dir i18n. Como el archivo en tu aplicación se fusionará con el de symfony, mantiene sólo las cadenas modificadas en el archivo de la aplicación.

Notarás que los traducciones del admin generator se nombran como sf_admin.fr.xml, en lugar de fr/messages.xml. Como cuestión de hecho, messages es el nombre del catálogo por defecto usado por Symfony, y que puede ser modificado para permitir una mejor separación entre las distintas partes de tu aplicación. Usar un catálogo que no sea el predeterminado require que lo especifique cuando usas el helper __():

<?php echo __('About Jobeet', array(), 'jobeet') ?>

En el anterior código __(), symfony buscará por la cadena "About Jobeet" en el Catálogo jobeet.

Tests

Las pruebas es una parte integrante de la migración de internacionalización. En primer lugar, actualiza los archivos de datos para pruebas de las categorías copiando los archivos de datos que teniamos definidos antes en test/fixtures/010_categories.yml.

Reconstruir el modelo para el entorno test:

$ php symfony propel:build-all-load --no-confirmation --env=test

Ahora puedes lanzar todas las pruebas para comprobar que están funcionando bien:

$ php symfony test:all

note

Cuando hemos desarrollado la interfaz de backend para Jobeet, no hemos escrito pruebas funcionales. Pero cada vez que creas un módulo con el comando de linea symfony symfony también generan las pruebas. Estás son seguras para eliminarlas.

Localización

Plantillas

Soportando diferentes culturas también significa soportar a las diferentes manera de formatear fechas y números. En una plantilla, varios helpers están a tut disposición para ayudar a tomar en cuenta todas estas diferencias, basado en la actual cultura del usuario:

En el grupo de helper Date :

Helper Descripción
format_date() Formatos de fecha
format_datetime() Formatos de fecha
time_ago_in_words() Muestra el tiempo transcurrido entre una fecha y ahora en palabras
distance_of_time_in_words() Muestra el tiempo transcurrido entre dos fechas en palabras
format_daterange() Formatos de un rango de fechas

En el grupo de helper Number :

Helper Descripción
format_number() Formatos un número
format_currency() Formatos de moneda

En el grupo de helper I18N :

Helper Descripción
format_country() Muestra el nombre de un país
format_language() Muestra el nombre de un idioma

Formularios

El framework de formualrios da varios widgets y los validadores para datos localizados:

Nos vemos mañana

Internacionalización y localización son clases de first-class en symfony. Dar un sitio web localizado para tus usuarios es muy fácil con lo que symfony da con todas las herramientas básicas e incluso le da la línea de comandos para realizar tareas rápidamente.

Preparate para un especial tutorial de mañana puesto que se va a mover un montón de archivos y la exploración en torno a un enfoque diferente para la organización de un proyecto symfony.

Feedback

tip

Este capítulo ha sido traducido por Roberto Germán Puentes Díaz. Si encuentras algún error que deseas corregir o realizar algún comentario, no dudes en enviarlo por correo a puentesdiaz [arroba] gmail.com

This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.