internationalisation sans révolution avec symfony (pareil mais mieux!)

imaginons qu’on ait un mld de ce genre
mcd
Le propos de ce tuto est le suivant

optimiser la navigation multilingue et le référencement avec symfony en ne touchant pas aux urls des link_to existants, sachant que le site n’est que partiellement traduit, que l’internaute doit être notifié dans sa langue en cas de contenu non traduit, et que tous les objets du site ne sont pas forcément multilingues

référencement et internationalisation

Plusieurs paramètres permettent à Google de présager de la langue par défaut d’un site
1 – L’extension pays (.fr -> français, .it italien, etc …)
2 – Et en cas d’extension générique (.com, .org, .net, etc …) la géolocalisation de l’IP
Ce dernier point est à surveiller avec vigilance: un hébergeur comme http://1and1.fr qui possède une IP allemande est susceptible de vous attribuer une IP allemande pour un site a contenu exclusivement français … ce qui n’est pas conseillé par les tueurs du référencement qui m’ont fait la formation référencement naturelle ;-)
Le meilleur moyen de veiller au grain est encore d’installer FlagFox, une extension firefox indispensable qui affiche le drapeau du pays où est hébergé une page dans la barre d’url de votre navigateur (ainsi que beaucoup plus de détails via un simple clic sur le sus dit drapeau).

Bref une fois que la langue par défaut, le nom de domaine et éventuellement la géolocalisation sont en accord, il s’agit de gérer les autres langues disponibles de manière claire.
En gros j’ai retenu qu’il y avait deux écoles:
1 – Un sous domaine par langue comme dans ce post – symfonians en VF
2 – Un répertoire dans l’url par langue comme je vais l’exposer dans ce billet
D’après ce que j’ai compris on pourrait aussi faire figurer la langue dans le stripped_title des url réécrites par exemple

Pour toute la suite on travaille sur un site en .fr (donc google s’attend par défaut à du français), une application nommée back, et les trois langues gérées sont le français l’anglais et le chinois

N.B le stripped_title est généré via la fonction stripText suivante /lib/myTools.php

public static function stripText($text)
{
   $text = preg_replace('/\ +/', '-', $text);
   $text = preg_replace('/\-$/', '', $text);
   $text = preg_replace('/^\-/', '', $text);
   return $text;
}

faire apparaitre la culture dans l’url

il s’agit ici de ne pas utiliser la variable d’url sf_culture qui courcircuiterait la gestion de la culture et compliquerait pas mal les chose …
Je propose plutot une routing rule par culture /apps/back/config/routing.yml

sf_cms_content_fr:
  url:   /:stripped_title.html
  param: { module: sfCmsContent, action: show }
sf_cms_content_en:
  url:   /en/:stripped_title.html
  param: { module: sfCmsContent, action: show }
sf_cms_content_zh:
  url:   /zh/:stripped_title.html
param: { module: sfCmsContent, action: show }

les link_to s’écrivent alors comme suit

<?php link_to('link to content', '@sf_cms_content_'.$sfCmsContent->getCulture().'?stripped_title='.$sfCmsContent->getStrippedTitle());?>

L’idée est donc que l’on a ou non une langue par défaut qui ne fait pas l’objet d’un « préfixe culture » dans l’ur
On peut stocker la cultutre éventuelle par défaut dans /config/app.yml

lang:
  default: fr

navigation multilingue

le changement de culture se fait via une action associé à l’utilisateur /apps/back/modules/sfCmsUser/actions/actions.class.php

class sfCmsUserActions extends autosfCmsUserActions

{
   public function executeLanguage() {
      $this->getUser()->setCulture($this->getRequestParameter('culture'));
      $url = $this->getRequest()->getReferer() != '' ? $this->getRequest()->getReferer() : '@homepage';
      $this->redirect($url);
   }
}

L’affichage des drapeaux se fait comme suit /apps/back/modules/sfCmsLang/tmeplates/_default.php

<?php if( count( sfConfig::get('app_lang_back') ) > 1):?>
   <?php foreach(sfConfig::get('app_lang_back') AS $lg => $lang):?>
      <?php if($lg==$sf_user->getCulture()):?>
         <?php echo image_tag('lang/'.$lg.'_shadow',array('alt'=> $lang, 'title'=>$lang))?>
      <?php else:?>
         <?php echo link_to(image_tag('lang/'.$lg,array('alt'=> $lang, 'title'=>$lang)), 'sfCmsUser/language?culture='.$lg)?>
      <?php endif?>
   <?php endforeach?>
<?php endif?>

Notez la soumission par méthode get qui permet aux moteur de recherche d’indexer le contenu multilingue
Enfin j’ai retouché la classe SfCmsContent comme suit dans /lib/model/SfCmsContent.php

class SfCmsContent extends BaseSfCmsContent
{
  public function hydrate(ResultSet $rs, $startcol = 1)
  {
    parent::hydrate($rs, $startcol);
    $this->setCulture(sfContext::getInstance()->getUser()->getCulture());
    $lang = array_keys(sfConfig::get('app_lang_'.sfConfig::get('sf_app')));
    $lg = array_pop($lang);
    while($this->getTitle()=='' && $lg )
    {
      $this->setCulture($lg);
      $lg = array_pop($lang);
    }
  }

  public function isTranslated($lg)
  {
    $c = new Criteria();
    $c->add(SfCmsContentI18nPeer::CULTURE, $lg);
    $c->addAnd(SfCmsContentI18nPeer::SF_CMS_CONTENT_ID, $this->getId());
    $sfCmsContentI18n = SfCmsContentI18nPeer::doSelectOne($c);
    if($sfCmsContentI18n && $sfCmsContentI18n->getTitle() && $sfCmsContentI18n->getStrippedTitle())
    {
      return $sfCmsContentI18n;
    }
    else
    {
      return false;
    }
  }
}

La réécriture de la méthode hydrate permet d’avoir l’objet sfCmsContent dans la culture la plus adaptée à celle de l’internaute (notez que l’ordre des cultures à un sens)
La méthode isTranslated permet de connaître la disponibilité d’un objet sfCmsContent pour une culture …

gérer la cohérence de la langue dans l’url / éviter le duplicate content

Maintenant le problème est de gérer la cohérence des urls, en utilisant correctement les routing rules énoncées plus haut, il y a une bonne partie du travail de fait … le seul problème qui se pose est celui de la culture par défaut qui n’a pas de préfixe. La conséquence est que les urls des cultures autres que par défaut répondent sur toute leur routing rule … et même sion a bien soigné la génération des liens en amont, mieux vaut se préserver du duplicate content et éviter les blagues. Voici donc un validateur pour l’action show qui va vérifier

  • que l’url contient bien un stripped_title et non pas un simple identifiant
  • la disponibilité du contenu dans la culture de l’utilisateur (en le notifiant si le contenu n’est pas disponible)
  • la cohérence entre la culture du stripped_title du contenu et celle de l’utilisateur
  • la cohérence entre la culture du préfixe et celle du stripped_title
public function validateShow()
{

  // l'url contient bien un stripped_title et non pas un simple identifiant
  if($this->getRequestParameter('id'))
  {
    $sfCmsContent = SfCmsContentPeer::retrieveByPk($this->getRequestParameter('id'));
    $this->redirect('@sf_cms_content_'.$this->getUser()->getCulture().'?stripped_title='.$sfCmsContent->getStrippedTitle());
  }
  elseif($this->getRequestParameter('stripped_title'))
  {
    $sfCmsContent = SfCmsContentPeer::retrieveByStrippedTitle($this->getRequestParameter('stripped_title'));
    if($this->getUser()->can($sfCmsContent, 'show'))
    {
      $c = new Criteria();
      $c->add(SfCmsContentI18nPeer::STRIPPED_TITLE, $this->getRequestParameter('stripped_title'));
      $sfCmsContentI18nFromStrippedTitle = SfCmsContentI18nPeer::doSelectOne($c);

      // disponibilité du contenu dans la culture de l'utilisateur (en le notifiant si le contenu n'est pas disponible)
      if($sfCmsContent->isTranslated($this->getUser()->getCulture()))
      {
        $this->getUser()->setAttribute('translation_missing', false);
      }
      else
      {
        $this->getUser()->setAttribute('translation_missing', true);
      }

      // cohérence entre la culture du stripped_title du contenu et celle de l'utilisateur
      if($sfCmsContentI18nFromStrippedTitle->getCulture()!=$this->getUser()->getCulture())
      {
        if($sfCmsContent->isTranslated($this->getUser()->getCulture()))
      	 {
      	   $c = new Criteria();
      	   $c->add(SfCmsContentI18nPeer::SF_CMS_CONTENT_ID, $sfCmsContentI18nFromStrippedTitle->getSfCmsContentId());
      	   $c->addAnd(SfCmsContentI18nPeer::CULTURE, $this->getUser()->getCulture());
      	   $sfCmsContentUserCulture = SfCmsContentI18nPeer::doSelectOne($c);
      	   $this->redirect('@sf_cms_content_'.$this->getUser()->getCulture().'?stripped_title='.$sfCmsContentUserCulture->getStrippedTitle());
      	  }
       }
       // cohérence entre la culture du préfixe et celle du stripped_title
      	$uri = split('/',$this->getRequest()->getPathinfo());
    	if(strlen($uri[1])==2)
    	{
	  $uri_language = $uri[1];
    	}
    	elseif(sfConfig::get('app_lang_default'))
    	{
	  $uri_language = sfConfig::get('app_lang_default');
    	}
    	if(sfConfig::has('app_lang_default') && $uri_language != $sfCmsContentI18nFromStrippedTitle->getCulture())
    	{
	  $this->redirect('@sf_cms_content_'.$sfCmsContentI18nFromStrippedTitle->getCulture().'?stripped_title='.$sfCmsContentI18nFromStrippedTitle->getStrippedTitle());
    	}
     }
     return true;
   }
   else
   {
     $this->forward404();
   }
 }
}

Comme on a pris soin de mettre une notification si le contenu n’est pas disponible dans la culture souhaité, il ne reste plus qu’à l’afficher, avec un petit effet scriptaculous histoire d’attirer l’attention de l’internaute
/apps/back/modules/sfCmsContent/templates/showSuccess.php

<?php if($sf_user->getAttribute('translation_missing')):?>
   <p id="missing_translation">
      <?php echo __('The content you are looking for is missing for your language')?>
   </p>
   <?php echo javascript_tag(
      visual_effect('highlight', 'missing_translation',
         array(
            'duration' => 2
         )
      )
   )?>
<?php endif?>
<?php echo $sfCmsContent->getHtmlBody();?>

N.B pour chaque nouvel objet multilingue il faudra

  • ajouter une routing rules par culture
  • réécrire les méthode hydrate et isTranslated
  • adapter le validateur validateShow
  • modififer le template pour faire apparaître la notification
  • del.icio.us
  • Twitter
  • Facebook
  • Tumblr
  • FriendFeed
  • LinkedIn
  • MySpace
  • StumbleUpon
  • Digg
  • Google Bookmarks
  • MSN Reporter
  • Netvibes
  • Ping.fm
  • Wikio FR
  • Reddit
  • Scoopeo
  • Slashdot
  • email
  • PDF
  • Print

poster un commentaire

votre email ne sera jamais publié ou communiqué. les champs obligatoires sont marqués par une *

*
*