Hier, nous avons terminé la fonctionnalité du moteur de recherche en la rendant encore plus fun avec l'ajout de quelques AJAX de qualité.
Aujourd'hui, nous allons parler de l'internationalisation (ou i18n) de Jobeet et la régionalisation (ou l10n).
Extrait de Wikipedia :
L'internationalisation est le processus de conception d'un logiciel afin qu'il puisse être adapté aux différentes langues et régions sans modifications techniques.
La régionalisation est le processus d'adaptation des logiciels à une région spécifique ou à une langue en y ajoutant des éléments spécifiques locaux et la traduction du texte.
Comme toujours, le framework symfony n'a pas réinventé la roue, son support sur i18n et sur l10n est basé sur le standard ICU.
User
Aucune internationalisation n'est possible sans utilisateur. Quand votre site Web est disponible dans plusieurs langues ou pour différentes régions du monde, l'utilisateur est responsable de choisir celle qui lui convient le mieux.
note
Nous avons déjà parlé de la classe User de symfony pendant la journée 13.
La Culture de l'utilisateur
Les caractéristiques i18n et l10n de symfony sont basées sur la culture de l'utilisateur.
La culture est la combinaison de la langue et du pays de l'utilisateur. Par exemple, la
culture pour un utilisateur qui parle français est fr
et la culture pour un utilisateur
de la France est fr_FR
.
Vous pouvez gérer la culture de l'utilisateur en appelant les méthodes
setCulture()
et getCulture()
sur l'objet User :
// in an action $this->getUser()->setCulture('fr_BE'); echo $this->getUser()->getCulture();
tip
La langue est codée par deux caractères minuscules, selon la norme ISO 639-1 et le pays est codé par deux caractères en majuscules, selon la norme ISO 3166-1.
La culture préférée
Par défaut, la culture de l'utilisateur est celle configurée dans le fichier de
configuration settings.yml
:
# apps/frontend/config/settings.yml all: .settings: default_culture: it_IT
tip
Comme la culture est géré par l'objet User, il est stocké dans la session utilisateur. Au cours du développement, si vous changez la culture par défaut, vous devrez effacer le cookie de votre session pour que le nouveau paramètre ait une influence dans votre navigateur.
Lorsqu'un utilisateur démarre une session sur le site Jobeet, nous pouvons également
déterminer la meilleure culture, sur la base des informations fournies par le Accept-Language
de l'entête HTTP.
La méthode getLanguages()
de l'objet de requête renvoie un tableau des langues
acceptées par l'utilisateur actuel, triées par ordre de préférence :
// in an action $languages = $request->getLanguages();
Mais la plupart du temps, votre site ne sera pas disponible dans les 136 langues
majeures du monde. La méthode getPreferredCulture()
retourne le meilleur langage en
comparant les langues préférées de l'utilisateur et les langues prises en charge par
votre site web :
// in an action $language = $request->getPreferredCulture(array('en', 'fr'));
Dans l'appel précédent, la langue retournée sera anglais ou français selon les langues préférées de l'utilisateur, ou en anglais (la première langue dans le tableau) si aucune ne correspond.
La culture dans l'URL
Le site Web Jobeet sera disponible en anglais et en français. Comme une URL ne peut
que représenter une ressource unique, la culture doit être incorporée dans l'URL. Pour
ce faire, ouvrez le fichier routing.yml
, et ajoutez la variable spéciale :sf_culture
pour toutes les routes sauf pour api_jobs
et homepage
. Pour les routes simples, ajoutez
/:sf_culture
devant url
. Pour les collections de routes, ajoutez une option
prefix_path
qui commence avec /: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
Lorsque la variable sf_culture
est utilisée dans une route, symfony utilisera
automatiquement sa valeur pour changer la culture de l'utilisateur.
Comme nous avons besoin d'autant de pages d'accueil que de langue supportées
(/en/
, /fr/
, ...), la page d'accueil par défaut (/
) doit rediriger vers celle
appropriée, conformément à la culture de l'utilisateur. Mais si l'utilisateur n'a pas
encore de culture, parce qu'il agit pour la première fois sur Jobeet, la culture privilégiée
sera choisie pour lui.
D'abord, ajoutez la méthode isFirstRequest()
à myUser
. Elle retourne true
seulement
pour la première requête d'une session utilisateur :
// 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); }
Ajoutez la route localized_homepage
:
# apps/frontend/config/routing.yml localized_homepage: url: /:sf_culture/ param: { module: job, action: index } requirements: sf_culture: (?:fr|en)
Modifiez l'action index
du module job
pour implémenter la logique afin de rediriger
l'utilisateur vers la «meilleure» page d'accueil lors de la première requête d'une session:
// 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
n'est pas présente dans la requête, cela signifie que
l'utilisateur est venu sur l'URL /
. Si tel est le cas et que la session est nouvelle,
la culture préférée est utilisée comme culture de l'utilisateur. Sinon, la culture actuelle
de l'utilisateur est utilisée.
La dernière étape consiste à rediriger l'utilisateur vers l'URL localized_homepage
. Notez
que la variable sf_culture
n'a pas été passée dans l'appel de redirection puisque symfony
l'ajoute automatiquement pour vous.
Maintenant, si vous essayez d'aller à l'URL /it/
, symfony va retourner une erreur
404 car nous avons limité la variable sf_culture
en
ou fr
. Ajouter
cette exigence à toutes les routes qui intègrent la culture :
requirements: sf_culture: (?:fr|en)
Culture Testing
Il est temps de tester notre implémentation. Mais avant d'ajouter plus de tests, nous avons
besoin de corriger des objets existants. Comme toutes les URL ont changé, modifiez tous les fichiers
de test fonctionnel dans test/functional/frontend/
et ajoutez /en
devant toutes les URLs. N'oubliez
pas de changer également les URL dans le fichier
lib/test/JobeetTestFunctional.class.php
. Lancez la suite de test pour vérifier
que vous avez correctement corrigé les tests :
$ php symfony test:functional frontend
Le testeur de User donne une méthode isCulture()
qui teste la culture de l'utilisateur
actuel. Ouvrez le fichier jobActionsTest
et ajoutez les tests suivants :
// 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') ;
Changer de langue
Pour l'utilisateur qui veut changer la culture, un formulaire linguistique
doit être ajoutée dans le layout. Le framework de formulaire ne prévoit pas une tel formulaire,
mais comme le besoin est assez fréquent pour des sites web internationalisé, l'équipe de
symfony maintient le
sfFormExtraPlugin
,
qui contient les validateurs, les widgets et les formulaires qui ne peuvent pas
être inclus dans le package symfony principal car ils sont trop spécifiques ou qu'ils ont des dépendances
externes mais ils sont néanmoins très utiles.
Installez le plugin avec la tâche plugin:install
:
$ php symfony plugin:install sfFormExtraPlugin
Ou bien depuis Subversion avec la commande suivante:
$ svn co http://svn.symfony-project.org/plugins/sfFormExtraPlugin/branches/1.3/ plugins/sfFormExtraPlugin
Afin que les classes du plugin puissent être chargées par symfony, le plugin sfFormExtraPlugin
doit être activé dans le fichier config/ProjectConfiguration.class.php
comme le montre le code ci-dessous:
// config/ProjectConfiguration.class.php public function setup() { $this->enablePlugins(array( 'sfPropelPlugin', 'sfPropelGuardPlugin', 'sfFormExtraPlugin' )); }
note
Le sfFormExtraPlugin
contient des widgets qui nécessitent des dépendances externes,
comme les bibliothèques JavaScript. Vous trouverez un widget pour la sélection de date,
un pour un éditeur WYSIWYG et d'autres encore. Prenez le temps de lire la documentation
où vous trouverez une foule de trucs utiles.
Le plugin sfFormExtraPlugin
offre un formulaire sfFormLanguage
pour gérer la
sélection de la langue. L'ajout du formulaire linguistique peut être fait dans le
layout comme ceci :
note
Le code ci-dessous n'est pas destiné à être mis en œuvre. Il est là pour vous montrer comment vous pourriez être tenté de mettre en œuvre quelque chose d'une mauvaise manière. Nous allons continuer à vous montrer comment l'implémenter correctement en utilisant 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>
Repérez vous un problème ? À droite, la création d'un objet de formulaire n'appartient pas à la couche de la Vue. Il doit être créé à partir d'une action. Mais, comme le code est dans le layout, le formulaire doit être créé pour chaque action, ce qui est loin d'être pratique. Dans de tels cas, vous devez utiliser un component. Un component est comme un partial, mais avec du code qui s'y rattachent. Considérez cela comme une action légère.
L'inclusion d'un component à partir d'un Template peut être fait en utilisant le
Helper include_component()
:
// apps/frontend/templates/layout.php <div id="footer"> <div class="content"> <!-- footer content --> <?php include_component('language', 'language') ?> </div> </div>
Le helper prend le module et l'action comme arguments. Le troisième argument peut être utilisé pour passer des paramètres au component.
Créez un module language
pour accueillir le component et l'action qui va
effectivement changer la langue de l'utilisateur :
$ php symfony generate:module frontend language
Les components sont à définir dans le fichier actions/components.class.php
.
Créer ce fichier maintenant :
// 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')) ); } }
Comme vous pouvez le voir, une classe components est très similaire à la classe des actions.
Le Template pour un component utilise les mêmes conventions de nommage qu'un
partial : un trait de soulignement (_
), suivi par le nom du component :
// apps/frontend/modules/language/templates/_language.php <form action="<?php echo url_for('change_language') ?>"> <?php echo $form ?><input type="submit" value="ok" /> </form>
Puisque le plugin ne prévoit pas l'action qui change effectivement la culture
des utilisateurs, modifiez le fichier routing.yml
pour créer la route
de change_language
:
# apps/frontend/config/routing.yml change_language: url: /change_language param: { module: language, action: changeLanguage }
Et créez l'action correspondante :
// 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'); } }
La méthode process()
de sfFormLanguage
prend soin de changer la culture de
l'utilisateur, basé sur la soumission du formulaire de l'utilisateur.
Internationalisation
Langues, Jeu de caractère et Encodage
Plusieurs langues ont des jeux de caractères différents. La langue anglaise est la plus simple car elle n'utilise que les caractères ASCII, la langue française est un peu plus complexe avec des caractères accentués comme "é" et les langues comme le russe, le chinois ou l'arabe sont beaucoup plus complexes car tous leurs caractères sont en dehors de la plage ASCII. Ces langues sont définies avec des jeux de caractères totalement différents.
Lorsqu'il s'agit de données internationalisées, il est préférable d'utiliser la norme unicode. L'idée derrière unicode est d'établir un ensemble universel de caractères qui contient tous les caractères de toutes les langues. Le problème avec unicode est qu'un seul caractère peut être représenté avec pas moins de 21 octets. Par conséquent, pour le web, nous utilisons UTF-8, qui fait correspondre les points de code unicode à des séquences de longueur variable d'octets. En UTF-8, les langues les plus utilisés ont leurs caractères codés avec moins de 3 octets.
UTF-8 est le codage par défaut utilisé par symfony, et il est défini dans le
fichier de configuration settings.yml
:
# apps/frontend/config/settings.yml all: .settings: charset: utf-8
Aussi, pour activer la couche d'internationalisation de symfony, vous devez définir
le paramètre i18n
à true
dans settings.yml
:
# apps/frontend/config/settings.yml all: .settings: i18n: true
Templates
Un site web internationalisé signifie que l'interface utilisateur est traduite en plusieurs langues.
Dans un Template, toutes les chaînes qui dépendent de la langue doivent être entourées
du helper __()
(remarquez qu'il y a deux caractères de soulignement).
Le helper __()
fait parti du groupe d'helper I18N
, qui contient des helpers
qui facilitent la gestion i18n dans les Templates. Comme ce groupe de helper n'est pas
chargé par défaut, vous devez soit l'ajouter manuellement dans chaque Template avec
use_helper('I18N')
comme nous l'avons fait pour le groupe d'helper Text
, ou
le charger globallement en l'ajoutant au paramètre standard_helpers
:
# apps/frontend/config/settings.yml all: .settings: standard_helpers: [Partial, Cache, I18N]
Voici comment utiliser le helper __()
pour le pied de page 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>
note
Le helper __()
peut prendre la chaîne de la langue par défaut ou vous pouvez
également utiliser un identificateur unique pour chaque chaîne. C'est juste une question de goût.
Pour Jobeet, nous allons utiliser la première stratégie ainsi les Teplates seront plus lisibles.
Lorsque symfony rend un Template, chaque fois le helper __()
est appelé,
symfony regarde pour une traduction la culture de l'utilisateur actuel. Si une
traduction est trouvée, elle est utilisée, sinon le premier argument est retourné
comme une valeur de repli.
Toutes les traductions sont stockées dans un catalogue. Le framework i18n fournit un grand nombre de stratégies différentes pour stocker les traductions. Nous allons utiliser le format "XLIFF" qui est une norme et qui est la plus souple. C'est également le stockage utilisé pour l'admin generator et la plupart des plugins de symfony.
note
Il existe d'autres catalogues de stockage comme gettext
, MySQL
et SQLite
. Comme toujours,
jetez un oeil à l'API i18n pour plus
de détails.
i18n:extract
Au lieu de créer le fichier du catalogue à la main, utilisez la tâche
intégrée i18n:extract
:
$ php symfony i18n:extract frontend fr --auto-save
La tâche i18n:extract
trouve toutes les chaînes qui doivent être traduits en fr
dans l'application frontend et crée ou met à jour le catalogue correspondant.
L'option --auto-save
enregistre les nouvelles chaînes dans le catalogue.
Vous pouvez également utiliser l'option --auto-delete
pour supprimer automatiquement
les chaînes qui n'existent plus.
Dans notre cas, il remplit le fichier que nous avons créé :
<!-- 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>
Chaque traduction est géré par une balise trans-unit
qui a un attribut id
unique. Vous pouvez maintenant éditer ce fichier et ajouter des traductions pour
la langue française :
<!-- 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
Comme XLIFF est un format standard, de nombreux outils existent pour faciliter le processus de traduction. Open Language Tools est un projet Open-Source en Java avec un éditeur XLIFF intégrée.
tip
Comme XLIFF est un format basé sur un fichier, les mêmes règles de priorité et de fusion qui existent pour les autres fichiers de configuration de symfony sont également applicables. Les fichiers i18n peuvent exister dans un projet, une application ou un module, et les traductions des fichiers les plus spécifiques substituent ceux des principaux.
Traductions avec arguments
Le principe essentiel de l'internationalisation est de traduire des phrases entières. Mais certaines phrases intègrent des valeurs dynamiques. Dans Jobeet, c'est le cas sur la page d'accueil pour le lien du "more..." :
<!-- apps/frontend/modules/job/templates/indexSuccess.php --> <div class="more_jobs"> and <?php echo link_to($count, 'category', $category) ?> more... </div>
Le nombre d'emplois est une variable qui doit être remplacée par un espace réservé pour la traduction :
<!-- 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 chaîne à traduire est maintenant "and %count% more...", et l'espace réservé
%count%
sera remplacé par le nombre réel lors de l'exécution, grâce à la valeur
donnée comme deuxième argument du helper __()
.
Ajoutez la nouvelle chaîne manuellement en insérant une balise trans-unit
dans
le fichier messages.xml
, ou utilisez la tâche i18n:extract
pour mettre à jour le
fichier automatiquement :
$ php symfony i18n:extract frontend fr --auto-save
Après l'exécution de la tâche, ouvrez le fichier XLIFF pour ajouter la traduction française :
<trans-unit id="6"> <source>and %count% more...</source> <target>et %count% autres...</target> </trans-unit>
La seule exigence dans la chaîne traduite est d'utiliser l'espace réservé %count%
quelque part.
Certaines autres chaînes sont encore plus complexes car ils impliquent les pluriels. Selon certains chiffres, la syntaxe change, mais pas nécessairement de la même façon pour toutes les langues. Certaines langues ont des règles de grammaire très complexe pour les pluriels, comme le polonais ou le russe.
Sur la page de catégorie, le nombre d'emplois dans la catégorie actuelle est affichée :
<!-- apps/frontend/modules/category/templates/showSuccess.php --> <strong><?php echo count($pager) ?></strong> jobs in this category
Lorsqu'une phrase a différentes traductions, en fonction du nombre, le
helper format_number_choice()
doit être utilisée :
<?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) ) ?>
Le Helper format_number_choice()
prend trois arguments :
- La chaîne à utiliser en fonction du nombre
- Un tableau d'espace réservé
- Le numéro à utiliser pour déterminer le texte à utiliser
La chaîne qui décrit les différentes traductions en fonction du nombre est formaté comme suit :
- Chaque possibilité est séparée par un caractère pipe (
|
) - Chaque chaîne est composée d'une portée suivie de la traduction
La portée peut décrire n'importe quel éventail de nombres :
[1,2]
: Accepte les valeurs entre 1 et 2 inclus(1,2)
: Accepte des valeurs comprises entre 1 et 2 en excluant 1 et 2{1,2,3,4}
: Seules les valeurs définies sont acceptées[-Inf,0)
: Accepte les valeurs supérieures ou égales à l'infini négatif et strictement inférieur à 0{n: n % 10 > 1 && n % 10 < 5}
: correspond à des nombres comme 2, 3, 4, 22, 23, 24
La traduction de la chaîne est similaire à d'autres chaînes de message :
<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>
Maintenant que vous savez comment internationaliser toutes sortes de chaines, prenez le
temps d'ajouter l'appel __()
à tous les Templates de l'application frontend. Nous n'allons
pas internationaliser l'application backend.
Formulaires
Les classes de formulaires contiennent de nombreuses chaines qui doivent être traduites, comme les labels, les messages d'erreur et les messages d'aide. Toutes ces chaînes sont automatiquement internationalisées par symfony, donc vous avez seulement besoin de fournir des traductions dans les fichiers XLIFF.
note
Malheureusement, la tâche i18n:extract
ne sait pas encore analyser les classes
de formulaires pour les chaînes non traduites.
Les objets de Propel
Pour le site web Jobeet, nous n'allons pas internationaliser tous les tables car il n'y a pas de sens de demander aux annonceurs de traduire leurs annonces d'emploi dans toutes les langues disponibles. Mais la table des catégories doit absolument être traduit.
Le plugin de Propel supporte la sortie des tables i18n. Pour chaque table qui contient des données localisées, deux tables doivent être créés : une pour les colonnes qui sont indépendantes de l'i18n, et l'autre avec les colonnes qui doivent être internationalisé. Les deux tables sont reliées par une relation 1-n.
Mettez à jour le schema.yml
|schema.yml
(I18n) en conséquence :
# 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 }
L'entrée _attributes
définit les options pour la table.
Et actualisez les fixtures pour les catégories :
# 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 }
Reconstruisez le modèle pour créer les classes supplémentaires i18n
:
$ php symfony propel:build --all --no-confirmation $ php symfony cc
Comme les colonnes name
et slug
ont été déplacées dans la table i18n, déplacez
la méthode setName()
de JobeetCategory
vers JobeetCategoryI18n
:
// lib/model/JobeetCategoryI18n.php public function setName($name) { parent::setName($name); $this->setSlug(Jobeet::slugify($name)); }
Nous avons également besoin de corriger la méthode getForSlug()
dans 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
Comme la tâche propel:build --all --and-load
supprime toutes les tables et les données
de la base de données, n'oubliez pas de re-créer un utilisateur pour accéder au backend de
Jobeet avec la tâche guard:create-user
. Autrement, vous pouvez ajouter un fichier fixture
pour l'ajouter automatiquement pour vous.
Lors de la construction du modèle, symfony crée des méthodes proxy dans l'objet
principal JobeetCategory
pour accéder commodément aux colonnes i18n définies dans
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
Pour réduire le nombre de requêtes à la base de données, utilisez la méthode doSelectWithI18n()
à la place de doSelect()
. Cela permettra de récupérer l'objet principal et celui
du i18n dans une seule requête.
$categories = JobeetCategoryPeer::doSelectWithI18n($c, $culture);
Comme la route category
est liée à la classe du modèle JobeetCategory
et
parce que le slug
fait maintenant partie de JobeetCategoryI18n
, la route n'est pas en mesure
de récupérer l'objet Category
automatiquement. Pour aider le système de routage,
nous allons créer une méthode qui se chargera de la récupération de l'objet :
// 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); } }
Ensuite, utilisez l'option method
method
(Routage) pour dire à la route category
d'utiliser la méthode doSelectForSlug()
pour récupérer l'objet :
# 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)
Nous avons besoin de recharger les jeux de test pour régénérer les slugs adéquates pour les catégories :
$ php symfony propel:data-load
Maintenant, la route category
est internationalisé et l'URL pour une catégorie
intègre le slug de la catégorie traduite :
/frontend_dev.php/fr/category/programmation /frontend_dev.php/en/category/programming
Admin Generator
Pour le backend, nous voulons que les traductions françaises et anglaises soient éditées dans le même formulaire :
L'intégration d'un formulaire i18n peut être fait en
utilisant la méthode 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'); } }
L'interface de l'admin generator supporte l'internationalisation. Il est livré
avec des traductions pour plus de 20 langues, et il est très facile d'en ajouter une
nouvelle, ou pour d'en personnaliser une existante. Copiez le fichier pour la langue que
vous souhaitez personnaliser depuis symfony (les traductions de l'admin se trouvent dans
lib/vendor/symfony/lib/plugins/sfPropelPlugin/i18n/
) dans le répertoire
i18n
de l'application. Comme le fichier dans votre application sera fusionné avec celle de
symfony, ne conservez que les chaines modifiées dans le fichier de l'application.
Vous remarquerez que les fichiers de traduction de l'admin generator sont nommés
sf_admin.fr.xml
, au lieu de fr/messages.xml
. En réalité, messages
est le nom
du catalogue par défaut utilisé par symfony et il peut être modifié pour permettre une
meilleure séparation entre les différentes parties de votre application. En utilisant
un autre catalogue que celui par défaut, cela nécessite que vous le spécifier lorsque
vous utilisez le helper __()
:
<?php echo __('About Jobeet', array(), 'jobeet') ?>
Dans l'appel précédent de __()
, symfony va chercher la chaîne "About Jobeet" dans
le catalogue jobeet
.
Tests
La correction des tests est une partie intégrante de la migration de
l'internationalisation. Premièrement, mettez à jour les jeux de test pour les catégories
en copiant les jeux de test que nous avons
défini ci-dessus dans test/fixtures/010_categories.yml
.
Reconstruisez le modèle de l'environnement de test
:
$ php symfony propel:build --all --no-confirmation --env=test
Vous pouvez maintenant lancer tous les tests pour vérifier qu'ils s'exécutent bien :
$ php symfony test:all
note
Quand nous avons développé l'interface backend pour Jobeet, nous n'avons pas écrit les tests fonctionnels. Mais chaque fois que vous créez un module avec la ligne de commande de symfony, symfony produit aussi des bouts de test. Ces bouts sont sûres d'être enlevés.
Régionalisation
Templates
Le support des différentes cultures, c'est aussi le soutien de différents formats pour les dates et les chiffres. Dans un Template, plusieurs helpers sont à votre disposition pour vous aider à prendre en compte toutes ces différences, basée sur la culture actuelle de l'utilisateur :
Dans le groupe d'helper
Date
:
Helper | Description |
---|---|
format_date() |
Formate la date |
format_datetime() |
Formate la date avec l'heure (heures, minutes, secondes) |
time_ago_in_words() |
Affiche le temps écoulé entre une date et maintenant |
distance_of_time_in_words() |
Affiche le temps écoulé entre deux dates |
format_daterange() |
Formate un intervalle de dates |
Dans le groupe d'helper
Number
:
Helper | Description |
---|---|
format_number() |
Formate un nombre |
format_currency() |
Formate une monnaie |
Dans le groupe d'helper
I18N
:
Helper | Description |
---|---|
format_country() |
Affiche le nom d'un pays |
format_language() |
Affiche le nom d'une langue |
Formulaires (I18n)
Le framework de formulaire fournit plusieurs widgets et validateurs pour régionalisé les données :
sfWidgetFormI18nDate
sfWidgetFormI18nDateTime
sfWidgetFormI18nChoiceCurrency
sfWidgetFormI18nChoiceLanguage
sfValidatorI18nChoiceLanguage
sfValidatorI18nChoiceTimezone
À demain
L'internationalisation et la régionalisation sont des citoyens de première classe dans symfony. Fournir un site régionalisé à vos utilisateurs est très facile car symfony fournit tous les outils de base et vous donne même les tâches en ligne de commande pour le faire rapidement.
Soyez prêts pour un tutoriel très spécial demain où nous déplacerons beaucoup de fichiers et explorons une approche différente de l'organisation d'un projet symfony.
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.