martes, 2 de septiembre de 2014
Symfony - Día 19: Internacionalización y Localización
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únWikipedia: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.
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();
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
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: sfDoctrineRouteCollection options: model: JobeetAffiliate actions: [new, create] object_actions: { wait: get } prefix_path: /:sf_culture/affiliate category: url: /:sf_culture/category/:slug.:sf_format class: sfDoctrineRoute 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: sfDoctrineRouteCollection 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: sfDoctrineRoute options: model: JobeetJob type: object method_for_query: retrieveActiveJob 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); } $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 = Doctrine_Core::getTable('JobeetCategory')->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 variablesf_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 archivolib/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('/')-> with('response')->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('/')-> with('response')->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
Or via Subversion with the following command:
$ svn co http://svn.symfony-project.org/plugins/sfFormExtraPlugin/branches/1.3/ plugins/sfFormExtraPlugin
In order for plugin's classes to be loaded, the
sfFormExtraPlugin
plugin must be activated in the config/ProjectConfiguration.class.php
file as shown below:// config/ProjectConfiguration.class.php public function setup() { $this->enablePlugins(array( 'sfDoctrinePlugin', 'sfDoctrineGuardPlugin', 'sfFormExtraPlugin' )); }
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í:
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.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
entrue
dentro de settings.yml
:# apps/frontend/config/settings.yml all: .settings: i18n: true
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', array('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>
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.
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ónfrontend
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>
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.
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 archivomessages.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="6"> <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 count($pager) ?></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>'.count($pager).'</strong>'), count($pager) ) ?>
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="7"> <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.
Lamentablemente, la tarea
i18n:extract
aún no analiza las clases form para cadenas sin traducir.Objetos Doctrine
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 Doctrine 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/doctrine/schema.yml JobeetCategory: actAs: Timestampable: ~ I18n: fields: [name] actAs: Sluggable: { fields: [name], uniqueBy: [lang, name] } columns: name: { type: string(255), notnull: true }
Al encender el comportamiento
I18n
, un modelo llamado JobeetCategoryTranslation
se creará automáticamente y los especificos campos
se trasladarán a ese modelo.
Nota que simplemente volvimos sobre el comportamiento
I18n
y movimos el comportamientoSluggable
para ser adjuntado al modelo JobeetCategoryTranslation
que se crea automáticamente. La opción uniqueBy
le dice al comportamiento Sluggable
campos que determinan si una slug es único o no. En este caso, cada slug debe ser único para cada par lang
y name
.
Y actualiza los archivos de datos para las categorías:
# data/fixtures/categories.yml JobeetCategory: design: Translation: en: name: Design fr: name: design programming: Translation: en: name: Programming fr: name: Programmation manager: Translation: en: name: Manager fr: name: Manager administrator: Translation: en: name: Administrator fr: name: Administrateur
También tenemos que sobreescribir el método
findOneBySlug()
en JobeetCategoryTable
. Desde que Doctrine da algo de buscadores mágicos para todas las columnas en un modelo, simplemente tenemos que crear el método findOneBySlug()
para que por defecto anular la magia de la funcionalidad que Doctrine proporciona.
Tenemos que hacer algunos cambios a fin de que la categoría es recuperada sobre la base del slug Inglés en la tabla
JobeetCategoryTranslation
.// lib/model/doctrine/JobeetCategoryTable.cass.php public function findOneBySlug($slug) { $q = $this->createQuery('a') ->leftJoin('a.Translation t') ->andWhere('t.lang = ?', 'en') ->andWhere('t.slug = ?', $slug); return $q->fetchOne(); }
Reconstruye el modelo:
$ php symfony doctrine:build --all --and-load --no-confirmation
$ php symfony cc
Como
doctrine:build --all --and-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 usamos el comportamieto
I18n
, proxis son creados entre el objeto JobeetCategory
y el objeto JobeetCategoryTranslation
de modo que todas las antiguas funciones de recuperación por el nombre de la categoría seguirá trabajando y podrás recuperar el valor para la cultura actual.$category = new JobeetCategory(); $category->setName('foo'); // sets the name for the current culture $category->getName(); // gets the name for the current culture $this->getUser()->setCulture('fr'); // from your actions class $category->setName('foo'); // sets the name for French echo $category->getName(); // gets the name for French
Para reducir el número de solicitudes a la bases de datos, haz el join de
JobeetCategoryTranslation
en tus queries. Esto te traerá el objeto principal y el i18n en una petición.$categories = Doctrine_Query::create() ->from('JobeetCategory c') ->leftJoin('c.Translation t WITH t.lang = ?', $culture) ->execute();
El
WITH
anterior agrega una condición para automáticamente agregar un ON
al query.LEFT JOIN c.Translation t ON c.id = t.id AND t.lang = ?
Como la ruta
category
es apunta a la modelo de clase JobeetCategory
porque el slug
es ahora parte de JobeetCategoryTranslation
, 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:
Puesto que ya sobreescribimos el
findOneBySlug()
vamos a refactorizar un poco más estos métodos para que pueden ser compartidos. Vamos a crear un nuevos métodosfindOneBySlugAndCulture()
y doSelectForSlug()
y cambiar el findOneBySlug()
para simplemente usar el findOneBySlugAndCulture()
.// lib/model/doctrine/JobeetCategoryTable.class.php public function doSelectForSlug($parameters) { return $this->findOneBySlugAndCulture($parameters['slug'], $parameters['sf_culture']); } public function findOneBySlugAndCulture($slug, $culture = 'en') { $q = $this->createQuery('a') ->leftJoin('a.Translation t') ->andWhere('t.lang = ?', $culture) ->andWhere('t.slug = ?', $slug); return $q->fetchOne(); } public function findOneBySlug($slug) { return $this->findOneBySlugAndCulture($slug, 'en'); }
A continuación, utiliza la opción
method
para decirle a la ruta category
que use el métododoSelectForSlug()
para recuperar el objeto:# apps/frontend/config/routing.yml category: url: /:sf_culture/category/:slug.:sf_format class: sfDoctrineRoute 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 doctrine: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
Para el backend, queremos que las traducciones de el francés y el Inglés sean editadas en el mismo formulario:
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_affiliates_list'], $this['created_at'], $this['updated_at'] ); $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/sfDoctrinePlugin/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álogojobeet
.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/categories.yml
.
Don't forget to update methods in the
lib/test/JobeetTestFunctional.class.php
file in order to care of our modifications concerning the JobeetCategory
's internationalization.public function getMostRecentProgrammingJob() { $q = Doctrine_Query::create() ->select('j.*') ->from('JobeetJob j') ->leftJoin('j.JobeetCategory c') ->leftJoin('c.Translation t') ->where('t.slug = ?', 'programming'); $q = Doctrine_Core::getTable('JobeetJob')->addActiveJobsQuery($q); return $q->fetchOne(); }
Reconstruir el modelo para el entorno
test
:$ php symfony doctrine:build --all --and-load --env=test
Ahora puedes lanzar todas las pruebas para comprobar que están funcionando bien:
$ php symfony test:all
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:
sfWidgetFormI18nDate
sfWidgetFormI18nDateTime
sfWidgetFormI18nChoiceCurrency
sfWidgetFormI18nChoiceLanguage
sfValidatorI18nChoiceLanguage
sfValidatorI18nChoiceTimezone
Referencia: http://symfony.com/legacy/doc/jobeet/1_4/es/19?orm=Doctrine
Suscribirse a:
Enviar comentarios
(
Atom
)
Sígueme en las Redes Sociales
Donaciones
Datos personales
Entradas populares
-
En este apartado vamos a explicar como ejercutar archivos PHP a través del terminal de Ubuntu. Lo primero que tendríamos que hacer es inst...
-
En este blog voy a comentar un tema que se utilizan en casi todas las páginas web que existen, y es el tema de la paginación. La paginaci...
-
Este post trata de la integración de la librería PHPExcel en Codeigniter, aunque se podría aplicar a cualquier librería, como por ejemplo mP...
-
Ejemplo para añadir o sumar un número determinado de hora/s, minuto/s, segundo/s a una fecha en php. Con la función strtotime se puede ...
-
Este tema es uno de los temas primordiales sobre el framework Codeigniter, ya que en alguna ocación nos hemos visto obligados a recoger dato...
© Espacio Daycry - Espacio de programación 2013 . Powered by Bootstrap , Blogger templates and RWD Testing Tool
No hay comentarios :
Publicar un comentario