English spoken conference

Symfony 5: The Fast Track

A new book to learn about developing modern Symfony 5 applications.

Support this project

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

Jour 19 : Internationalisation et régionalisation

1.4 / Propel
Symfony version
1.2
Language ORM

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.

Pied de page internationalisé

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 methodmethod (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 :

Les catégories du backend

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 :

À 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.