Author: forresst Date: 2010-02-12 16:09:07 +0100 (Fri, 12 Feb 2010) New Revision: 27943
Added: doc/branches/1.2/jobeet/fr/19.txt Log: [doc-fr][1.2] Add doc in french, jobeet/19 rev:en/22448 Added: doc/branches/1.2/jobeet/fr/19.txt =================================================================== --- doc/branches/1.2/jobeet/fr/19.txt (rev 0) +++ doc/branches/1.2/jobeet/fr/19.txt 2010-02-12 15:09:07 UTC (rev 27943) @@ -0,0 +1,1207 @@ +Jour 19 : Internationalisation et régionalisation +================================================= + +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|Internationalisation~** +(ou ~i18n|I18N~) de Jobeet et la **~régionalisation|Régionalisation~** (ou ~l10n|L10n~). + +Extrait de [Wikipedia](http://fr.wikipedia.org/wiki/Internationalisation_de_logiciel) : + +>L'**internationalisation** est le processus de conception d'un logiciel afin qu'il +>puisse être adapté aux différentes ~langues|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|Local~ et la +>~traduction du texte|Traducions~. + +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~](http://www.icu-project.org/). + +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|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 : + + [php] + // in an action + $this->getUser()->setCulture('fr_BE'); + echo $this->getUser()->getCulture(); + +>**TIP** +>La ~langue|Langue de l'utilisateur~ est codée par deux caractères minuscules, selon la +>[norme ISO 639-1](http://en.wikipedia.org/wiki/ISO_639-1) et le pays est +>codé par deux caractères en majuscules, selon la +>[norme ISO 3166-1](http://en.wikipedia.org/wiki/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`~ : + + [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|Session~. Au cours du développement, si vous changez la +>~culture par défaut|Culture par défaut~, vous devrez effacer le ~cookie|Cookies~ +>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|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 : + + [php] + // 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 : + + [php] + // 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`|Préfixe~ qui commence avec `/:sf_culture`. + + [yml] + # 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 +<propel> + options: + model: JobeetJob + type: object + method_for_criteria: doSelectActive +</propel> +<doctrine> + options: + model: JobeetJob + type: object + method_for_query: retrieveActiveJob +</doctrine> + 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 : + + [php] + // 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); + } + } + +Ajoutez la route `localized_homepage` : + + [yml] + # 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: + + [php] + // 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'); + } + +<propel> + $this->categories = JobeetCategoryPeer::getWithJobs(); +</propel> +<doctrine> + $this->categories = Doctrine::getTable('JobeetCategory')->getWithJobs(); +</doctrine> + } + +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|Erreur 404~ car nous avons limité la variable `sf_culture` `en` ou `fr`. Ajouter +cette exigence à toutes les routes qui intègrent la culture : + + [yml] + 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 : + + [php] + // 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') + ; + +Changer de langue +----------------- + +Pour l'utilisateur qui veut changer la culture, un ~formulaire|Formulaires~ 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`~](http://www.symfony-project.org/plugins/sfFormExtraPlugin?tab=plugin_readme), +qui contient les ~validateurs|Validateurs~, les ~widgets|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 + +Et videz le cache car le plugin définit de nouvelles classes : + + $ php symfony cc + +>**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. + + [php] + // 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|Action~, ce qui est loin d'être pratique. +Dans de tels cas, vous devez utiliser un **component**. Un ~component|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()`~ : + + [php] + // 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 : + + [php] + // 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 : + + [php] + // 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` : + + [yml] + # apps/frontend/config/routing.yml + change_language: + url: /change_language + param: { module: language, action: changeLanguage } + +Et créez l'action correspondante : + + [php] + // 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|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` : + + [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` à `on` dans `settings.yml` : + + [yml] + # apps/frontend/config/settings.yml + all: + .settings: + i18n: on + +### 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`~ : + + [yml] + # apps/frontend/config/settings.yml + all: + .settings: + standard_helpers: [Partial, Cache, I18N] + +Voici comment utiliser le helper `__()` pour le pied de page de Jobeet : + + [php] + // apps/frontend/templates/layout.php + <div id="footer"> + <div class="content"> + <span class="symfony"> + <img src="/images/jobeet-mini.png" /> + powered by <a href="http://www.symfony-project.org/"> + <img src="/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** +>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|Catalogue de traduction~. +Le framework i18n fournit un grand nombre de stratégies différentes pour stocker les traductions. +Nous allons utiliser le format ["~XLIFF~"](http://en.wikipedia.org/wiki/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](http://www.symfony-project.org/api/1_2/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`|Tâche d'extraction I18n~ : + + $ 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éé : + + [xml] + <!-- 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 : + + [xml] + <!-- 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](https://open-language-tools.dev.java.net/) 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..." : + + [php] + <!-- 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 : + + [php] + <!-- 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 : + + [xml] + <trans-unit id="5"> + <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|Pluriels (I18n)~. 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 : + + [php] + <!-- apps/frontend/modules/category/templates/showSuccess.php --> + <strong><?php echo $pager->getNbResults() ?></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] + <?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() + ) + ?> + +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|Portée (I18n)~ 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 : + + [xml] + <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> + +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|Formulaires (I18n)~ + +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 ##ORM## + +Pour le site web Jobeet, nous n'allons pas ~internationaliser tous les tables|Internationalisation du modèle~ +car il n'y a pas de sens de demander aux annonceurs de ~traduire|I18N (Modèle)~ leurs annonces +d'emploi dans toutes les langues disponibles. Mais la table des catégories doit absolument +être traduit. + +Le plugin de ##ORM## 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 : + +<propel> + [yml] + # 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|Fixtures (I18n)~ pour les catégories : + + [yml] + # 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` : + + [php] + // 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` : + + [php] + // 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); + } +</propel> +<doctrine> + [yml] + # config/doctrine/schema.yml + JobeetCategory: + actAs: + Timestampable: ~ + I18n: + fields: [name] + actAs: + Sluggable: { fields: [name], uniqueBy: [lang, name] } + columns: + name: { type: string(255), notnull: true } + +En se tournant sur le comportement `I18n`, un modèle nommé `JobeetCategoryTranslation` +sera automatiquement créé et les `fields` spécifiés sont déplacés vers ce +modèle. + +Remarquez que nous mettons simplement le comportement `I18n` et déplacons le comportement +`Sluggable` pour être attaché au modèle `JobeetCategoryTranslation` automatiquement créé. +L'option `uniqueBy` décrit pour le comportement `Sluggable` quels sont les champs qui déterminent +si un slug est unique ou non. Dans notre cas, chaque slug doit être unique pour chaque paire +`lang` et `name`. + +Et actualisez les ~fixtures|Fixtures (I18n)~ pour les catégories: + + [yml] + # 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 + +Nous avons besoin aussi de surcharger la méthode `findOneBySlug()` dans `JobeetCategoryTable`. +Car Doctrine fournit quelques chercheurs magiques pour toutes les colonnes dans un modèle. Nous +avons besoin de créer simplement la méthode `findOneBySlug()` afin de surcharger la fonctionnalité +magique par défaut qu'offre Doctrine. + +Nous devons faire quelques modifications afin que la catégorie soit récupérée sur la +base du slug anglais dans la table `JobeetCategoryTranslation`. + + [php] + // 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(); + } + +Reconstruisez le modèle : + + $ php symfony doctrine:build-all-reload --no-confirmation + $ php symfony cc + +</doctrine> + +>**TIP** +<propel> +>Comme la tâche `propel:build-all` supprime toutes les tables et les données de la base de données, +</propel> +<doctrine> +>Comme la tâche `doctrine:build-all-reload` supprime toutes les tables et les données de la base de données, +</doctrine> +>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. + +<propel> +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` : + + [php] + $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 +</propel> +<doctrine> +Lorsque vous utilisez le comportement `I18n`, les proxies sont créés entre l'objet +`JobeetCategory` et l'objet `JobeetCategoryTranslation`, donc toutes les anciennes fonctions +pour récupérer le nom de la catégorie fonctionneront encore et récupéront la valeur pour la +culture actuelle. + + [php] + $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 +</doctrine> + +<propel> +>**TIP** +>Pour réduire le nombre de ~requêtes à la base de données|Performances~, 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. +> +> [php] +> $categories = JobeetCategoryPeer::doSelectWithI18n($c, $culture); +</propel> +<doctrine> +>**TIP** +>Pour réduire le nombre de ~requêtes à la base de données|Performances~, joignez `JobeetCategoryTranslation` +>dans vos requêtes. Cela permettra de récupérer l'objet principal et celui du i18n dans une seule +>requête. +> +> [php] +> $categories = Doctrine_Query::create() +> ->from('JobeetCategory c') +> ->leftJoin('c.Translation t WITH t.lang = ?', $culture) +> ->execute(); +> +>Le mot clé `WITH` ci-dessus va ajouter une condition à la condition `ON` ajoutée +>automatiquement sur de la requête. Ainsi, la condition `ON` de la jointure sera à la +>fin. +> +> [sql] +> LEFT JOIN c.Translation t ON c.id = t.id AND t.lang = ? +</doctrine> + +Comme la route `category` est liée à la classe du modèle `JobeetCategory` et +<propel> +parce que le `slug` fait maintenant partie de `JobeetCategoryI18n`, la route n'est pas en mesure +</propel> +<doctrine> +parce que le `slug` fait maintenant partie de `JobeetCategoryTranslation`, la route +n'est pas en mesure +</doctrine> +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 : + +<propel> + [php] + // 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); + } + } +</propel> +<doctrine> +Comme nous avions déjà surchargé `findOneBySlug()`, refactorisons un peu plus afin +que ces méthodes puissent être partagées. Nous allons créer des nouvelles méthodes +`findOneBySlugAndCulture()` et `doSelectForSlug()` et changer la méthode `findOneBySlug()` +pour utiliser simplement la méthode `findOneBySlugAndCulture()`. + + [php] + // 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'); + } +</doctrine> + +Ensuite, utilisez l'~option `method`|option `method` (Routage)~ pour dire à la route `category` +d'utiliser la méthode `doSelectForSlug()` pour récupérer l'objet : + + [yml] + # 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 + +A cause d'un bogue dans symfony 1.2.1, vous devez commenter le `title` dans la +section `edit` : + + [yml] + # apps/backend/modules/category/config/generator.yml + edit: + #title: Editing Category "%%name%%" (#%%id%%) + +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|Formulaire (Traduction)~ peut être fait en +utilisant la méthode `embedI18N()` : + + [php] + // lib/form/JobeetCategoryForm.class.php + class JobeetCategoryForm extends BaseJobeetCategoryForm + { + public function configure() + { +<propel> + unset($this['jobeet_category_affiliate_list']); +</propel> +<doctrine> + unset( + $this['jobeet_affiliates_list'], + $this['created_at'], $this['updated_at'] + ); +</doctrine> + + $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 +<propel> +`lib/vendor/symfony/lib/plugins/sfPropelPlugin/i18n/`) dans le répertoire +</propel> +<doctrine> +`lib/vendor/symfony/lib/plugins/sfDoctrinePlugin/i18n/`) dans le répertoire +</doctrine> +`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] + <?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|I18n (Test)~ 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 +<propel> +défini ci-dessus dans `test/fixtures/010_categories.yml`. +</propel> +<doctrine> +défini ci-dessus dans `test/fixtures/categories.yml`. +</doctrine> + +Reconstruisez le modèle de l'environnement de `test` : + +<propel> + $ php symfony propel:build-all-load --no-confirmation --env=test +</propel> +<doctrine> + $ php symfony doctrine:build-all-reload --no-confirmation --env=test +</doctrine> + +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`](http://www.symfony-project.org/api/1_2/DateHelper) : + + | 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`](http://www.symfony-project.org/api/1_2/NumberHelper) : + + | Helper | Description | + | ------------------- | -------------------- | + | `format_number()` | Formate un nombre | + | `format_currency()` | Formate une monnaie | + +Dans le groupe d'helper +[`I18N`](http://www.symfony-project.org/api/1_2/I18NHelper) : + + | 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|Widgets (I18n)~ et ~validateurs|Validateurs (I18n)~ pour régionalisé les données : + + * [`sfWidgetFormI18nDate`](http://www.symfony-project.org/api/1_2/sfWidgetFormI18nDate) + * [`sfWidgetFormI18nDateTime`](http://www.symfony-project.org/api/1_2/sfWidgetFormI18nDateTime) + * [`sfWidgetFormI18nTime`](http://www.symfony-project.org/api/1_2/sfWidgetFormI18nTime) + + * [`sfWidgetFormI18nSelectCountry`](http://www.symfony-project.org/api/1_2/sfWidgetFormI18nSelectCountry) + * [`sfWidgetFormI18nSelectCurrency`](http://www.symfony-project.org/api/1_2/sfWidgetFormI18nSelectCurrency) + * [`sfWidgetFormI18nSelectLanguage`](http://www.symfony-project.org/api/1_2/sfWidgetFormI18nSelectLanguage) + + * [`sfValidatorI18nChoiceCountry`](http://www.symfony-project.org/api/1_2/sfValidatorI18nChoiceCountry) + * [`sfValidatorI18nChoiceLanguage`](http://www.symfony-project.org/api/1_2/sfValidatorI18nChoiceLanguage) + +À 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. + +__ORM__ -- You received this message because you are subscribed to the Google Groups "symfony SVN" group. To post to this group, send email to [email protected]. To unsubscribe from this group, send email to [email protected]. For more options, visit this group at http://groups.google.com/group/symfony-svn?hl=en.
