I18n : traduction des modèles liés

La solution d’internationalisation de CakePHP passe par le comportement Translate qui renvoie de façon transparente les champs textuels d’un enregistrement dans la langue demandée. Cependant le comportement ne renvoie pas les champs traduits des enregistrements liés au Modèle concerné. Voyons quelles solutions apporter à cela.

1. Traduire les modèles liés dans le callback afterFind()

La première solution a été proposée il y a longtemps par jitka sur le Google Group anglophone de CakePHP. Elle consiste à retrouver les traductions des modèles associés dans le callback afterFind du modèle concerné. Bien que cette solution fonctionne, elle est très lourde à mettre en place, et enlève toute souplesse dans la gestion des modèles. Il faut en effet coder explicitement la façon dont Cake doit retrouver les traductions, en prévoyant à l’avance quels modèles traiter. Si l’on utilise le comportement Containable ou si l’on ajoute un modèle aux relations existantes, il faut modifier l’afterFind pour prendre en compte les changements.

2. Utiliser une méthode statique par modèle

La solution que nous proposons passe par la création d’une méthode statique dans chaque modèle. Cette méthode, que nous appellerons translate(), va prendre en paramètre un nom de champ à renvoyer traduit, un id d’enregistrement, et éventuellement la langue attendue. Elle va renvoyer le champ traduit s’il est trouvé dans la table des traductions.

Imaginons une boutique de produits classés par catégorie. Dans ce cas les relations entre les modèles sont Product belongsTo Category et Category hasMany Product. Voyons comment afficher la liste paginée des produits d’une catégorie dans le Contrôleur Products :

class ProductsController extends AppController {
	var $paginate = array(
		'limit' => 10,
		'order' => 'Product.id DESC'
	);
 
	function index($category_id = null) {
		$this->Product->Category->id = $category_id;
 
		if (!$category_id || !$this->Product->Category->exists()) {
			$this->cakeError('error404');
		}
 
		$this->paginate['conditions'] = array('Product.category_id' => $category_id);
		$data = $this->paginate();
 
		$this->set(compact('data', 'category_id'));
	}
}

Si l’on affiche un debug($data), on voit que le tableau contient bien les produits avec leurs champs traduits, mais pas les champs traduits de la catégorie. Ajoutons notre méthode statique translate() dans le Modèle Category :

class Category extends AppModel {
/**
 * Renvoie la traduction d'un champ de la base
 * 
 * @param string $field Nom du champ
 * @param int $id Id de l'enregistrement
 * @param string $locale Nom de la langue de la traduction attendue
 */
	static function translate($field = '', $id = null, $locale = null) {
		$model =& ClassRegistry::init('Category');
 
		if (!$locale) {
			$locale = Configure::read('Config.language');
		}
 
		$model->locale = $locale;
 
		$data = $model->find('first', array(
			'conditions' => array($model->alias.'.'.$model->primaryKey => $id),
			'recursive' => -1
		));
 
		if (!empty($data[$model->alias][$field])) {
			return $data[$model->alias][$field];
		} else {
			// On recherche dans la langue par défaut :
			if ($locale != Configure::read('Config.language')) {
				return self::translate($field, $id, Configure::read('Config.language'));
			}
		}
 
		return false;
	}
}

Cette méthode est maintenant disponible dans les Contrôleurs ou les Vues en relation avec ce Modèle. Voyons la vue index.ctp correspondant à la liste des produits, dans laquelle nous souhaitons afficher la catégorie choisie :

<h1><?php echo Category::translate('name', $category_id); ?></h1>
 
<?php foreach ($data as $product): ... ?>

Dans un exemple aussi simple, l’intérêt n’est pas flagrant, nous aurions très bien pu faire un afterFind dans le modèle Product ou faire un find de la catégorie choisie avant de la passer à la Vue directement dans l’action index(), et dans ce cas la traduction aurait été présente. Mais imaginons maintenant que le Modèle Product est lié à des déclinaisons, des tags, etc., et que la catégorie est elle-même liée à d’autres Modéles : dans ce cas l’afterFind devient extrêment complexe, ou l’action du contrôleur devient vite illisible et suppose que l’on ait bien prévu toutes les données à afficher dans la Vue. Notre méthode, bien que contraire au pattern MVC puisqu’il est possible de l’appeler directement dans les Vues, procure une souplesse inégalable.

Pierre-Emmanuel Fringant

Articles connexes

Commentaires

[...] [...]

Ce n’est pas un peu lourd pour la bd ?

Si je comprends bien une page index de catégorie contenant 20 produits avec 2 champs traduits donnera 40 connections (requêtes) de plus sur la bd.

C’est tout à fait vrai, mais la solution que je propose doit être couplée avec une stratégie de caching efficace pour ne pas être un fardeau pour la base.

Sympa cette petite fonction, je l’utilise dans plusieurs projets, mais j’ai mis la fonction dans AppModel, en lui ajoutant juste un paramètre « model_name », ainsi, pas besoin de la réécrire dans chaque modèle.

On l’appelle ensuite comme cà : AppModel::translate(‘MonModele’,1,’fre’);

Oups, j’ai oublié un paramètre :)

AppModel::translate(‘MonModele’,’name’,1,’fre’);

Participez

Pour insérer une portion de code, utilisez <pre lang="php">...</pre>