Un site multilingue avec CakePHP

Nous allons voir comment préparer une application CakePHP pour qu’elle reçoive plusieurs langues. Nous devrons faire attention :

  • à traiter correctement les caractères spécifiques à chaque langue ;
  • traduire l’interface du site ;
  • trouver une méthode pour déterminer la langue demandée par le visiteur tout en soignant le référencement par les moteurs de recherche ;
  • et enfin traduire les textes enregistrés dans la base de données.

1. Configuration de l’encodage

Nous allons commencer par régler le problème des accents et des caractères non standards comme le signe euro. En effet, beaucoup de langues ont des caractères spécifiques et nous devons travailler de telle manière qu’il soit possible de tous les prendre en compte, de les enregistrer correctement en base et dans nos fichiers, puis de les afficher sans perte. La norme UTF-8 correspond tout à fait à ces besoins.

1.1 Base MySQL en UTF-8

Dans phpMyAdmin, nous créons une nouvelle base de données en prenant soin de choisir :

  • Interclassement pour la connexion MySQL : “utf8_unicode_ci” ;
  • A droite du nom de la nouvelle base : “utf8_unicode_ci”.

1.2 Enregistrement des fichiers en UTF-8

Quel que soit l’éditeur de code avec lequel nous travaillons, nous devons faire attention à toujours enregistrer les fichiers .php et .ctp en UTF-8.
Dans Eclipse PDT par exemple, nous faisons un clic droit sur la racine du projet > “Properties” > dans la section “Text file encoding” nous cochons le bouton radio “Other” et sélectionnons “UTF-8″.

1.3 Connexion à la base en UTF-8

Nous allons indiquer à CakePHP que nous allons nous connecter à une base codée en UTF-8 pour que la connexion se fasse elle aussi en UTF-8. Editions le fichier {app}/config/database.php : pour chaque configuration nous précisons :

1
'encoding' => 'utf8' // (sans tiret, attention)

1.4 Spécification de l’encodage dans l’entête HTML

Nous devons préciser aux navigateurs que les pages HTML de notre site sont encodées en UTF-8. Si nous avons plusieurs layouts, nous pouvons définir une fois pour toute le charset de notre application, en plaçant la ligne suivante dans le fichier {app}/config/bootstrap.php

1
Configure::write('App.encoding', 'utf-8');

Ainsi dans n’importe quel layout, l’appel de la méthode $html->charset() renverra bien :

1
2
3
4
<head>
<meta http-equiv="Content-Type" content="text/html charset=utf-8"/>
...
</head>

Nous sommes fin prêts à programmer notre site multilingue.

2. Traduction de l’interface

Nous allons commencer par mettre en place les traductions de toutes les expressions utilisées dans l’interface du site, c’est à dire tout ce qui n’est pas enregistré dans la base de données : les menus, les messages, les titres de page, etc.
Considérons un exemple extrêmement simple pour la page d’accueil d’un site multilingue anglais / français, le français étant la langue par défaut :

1
2
3
4
5
6
7
8
9
10
11
12
// {app}/views/layouts/default.ctp
<?php e($html->docType('html4-strict')); ?> 
<html>
	<head>
		<?php e($html->charset()); ?> 
		<title><?php e($title_for_layout); ?></title>
	</head>
 
	<body>
		<?php e($content_for_layout); ?> 
	</body>
</html>
1
2
3
4
// {app}/views/pages/home.ctp
<?php $this->pageTitle = __('titre_page_accueil', true); ?>
<h1><?php __('Salut à tous'); ?></h1>
<?php e($html->link(__('Je suis un lien', true), array('controller' => 'pages', 'action' => 'display', 'home'))); ?>

2.1 Les fonctions gettext

CakePHP met à notre disposition des fonctions à la forme curieuse : __(), __n() et d’autres. La fonction __() prend en paramètre une chaîne de caractères, une sorte d’”identifiant d’expression”. Cet identifiant va se retrouver dans un fichier de traduction (un fichier par langue) et c’est dans ce fichier que nous allons écrire les expressions traduites. La fonction __() va donc chercher, dans le fichier de traduction de la langue demandée, l’expression traduite correspondant à l’identifiant passé en argument. La fonction prend un booléen comme deuxième argument (false par défaut), qui permet de spécifier si l’on veut afficher ou simplement retourner la chaîne trouvée.
Le format des identifiants d’expression est libre : soit strict et codifié comme le nom d’une variable PHP (ex : titre_page_accueil), soit directement l’expression elle-même dans la langue par défaut (ex : Salut à tous) ce qui permet de gagner du temps.

2.2 Extraction des expressions à traduire

Nous pouvons maintenant préparer les fichiers de traduction. La console de CakePHP offre un outil d’extraction automatique qui va scanner toute l’application pour retrouver les appels à la fonction __(), et en sortir la liste dans le fichier {app}/locale/default.pot.
Nous lançons la console avec l’argument “i18n”. Nous lisons ceci :

1
2
What is the full path you would like to extract?
Example: F:\Sites\myapp

Nous remplaçons “myapp” par le nom du répertoire racine de notre application, puis validons.

1
2
What is the full path you would like to output?
Example: f:\Sites\{racine de l'application}\locale

Nous validons à nouveau pour arriver à “I18n Shell”. Parmi les choix proposés, nous prenons le premier, “Extract POT file from sources”.
A la question “Would you like to merge all translations into one file?” nous répondons oui (un seul fichier à gérer est beaucoup simple, nous le verrons par la suite). Nous laissons le nom “default”.
Si le fichier existait déjà, nous le remplaçons.
Si nous rafraîchissons l’explorateur d’Eclipse, nous voyons le fichier default.pot dans le répertoire {app}/locale. Il contient tous les identifiants d’expressions de notre projet.

2.3 Traduction des expressions avec Poedit

CakePHP ne va pas se servir de ce fichier default.pot, il attend que soient créés des fichiers default.po, à raison d’un par langue et contenant la même chose que le .pot mais avec les traductions. Chaque fichier default.po devra se trouver dans {app}/locale/{code de la langue}/LC_MESSAGES.
L’édition des fichiers .pot ou .po est assez fastidieuse avec un éditeur de texte classique. Nous allons installer un logiciel libre extrêmement pratique, Poedit.
Nous ouvrons le programme et allons dans “Fichier” > “Nouveau catalogue depuis un fichier POT” et recherchons le fichier {app}/locale/default.pot. La boîte de dialogue “Enregistrer sous…” s’ouvre, nous le mettons dans {app}/locale/eng/LC_MESSAGES/default.po.
Nous pouvons maintenant traduire nos expressions :

  • titre_page_accueil => Welcome
  • Salut à tous => Hi all
  • Je suis un lien => I am a link

Nous répétons la même opération en enregistrant cette fois sous {app}/locale/fre/LC_MESSAGES/default.po :

  • titre_page_accueil => Bienvenue
  • Salut à tous =>
  • Je suis un lien =>

Nous constatons ici l’intérêt d’utiliser l’expression elle-même dans la langue par défaut comme identifiant d’expression, plutôt qu’une chaîne de caractère arbitraire comme “titre_page_accueil” : le fichier de traduction de la langue par défaut devient inutile.
L’utilité de Poedit ne s’arrête pas là : nous venons de faire le fichier de traduction de chaque langue, mais il est certain que ce fichier va grossir jusqu’à ce que le site soit terminé, et nous ne voulons pas ressaisir les traductions à chaque fois, ni attendre la fin du projet pour les faire d’un seul coup.
Il suffit de refaire l’extraction du fichier .pot avec la console, puis d’ouvrir chaque fichier .po dans Poedit et de faire la commande “Catalogue” > “Mettre à jour depuis fichier POT”. Poedit importera uniquement les nouvelles expressions à traduire !

3. Identifier la langue demandée

Nous voulons maintenant détecter la langue demandée pour renvoyer le contenu attendu par le visiteur. Nous pourrions identifier automatiquement la langue du navigateur, ou bien laisser choisir le visiteur et conserver son choix en session ou dans un cookie (ou les deux). Cette méthode n’est pas satisfaisante concernant l’indexation des pages par les moteurs de recherche ; nous préférons identifier la langue demandée en faisant transiter un code langue de 2 lettres dans toutes les URL du site. Par exemple, /fr/article/5 renverra sur une page en français, /en/article/5 sur la même page en anglais.
Pour parvenir à nos fins, nous devons d’une part analyser l’URL demandée pour identifier ce code langue, et d’autre part veiller à ce que tous les liens hypertextes comportent le code langue.

3.1 Identifier le code langue dans l’URL

Nous ne souhaitons pas encombrer le Router avec la gestion de la langue, nous allons donc simplement analyser l’URL, identifier le code langue et l’enlever de l’URL avant de la transmettre au Router.
Nous effectuons ce traitement dans le fichier {app}/config/bootstrap.php :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php
// Encodage
// --------
Configure::write('App.encoding', 'utf-8');
 
// Langues
// -------
 
// Langues acceptées
$languages = array(
	'fr' => 'fre',
	'en' => 'eng'
);
 
// Français par défaut
$langCode = 'fr';
$language = 'fre';
 
// Analyse de l'URL
if(!empty($_GET['url']))
{
	if(strpos($_GET['url'], '/') !== false)
	{
		$langFromUrl = substr($_GET['url'], 0, strpos($_GET['url'], '/'));
	}
	else
	{
		$langFromUrl = $_GET['url'];
	}
 
	// Code langue accepté ?
	if(isset($languages[$langFromUrl]))
	{
		$langCode = $langFromUrl;
		$language = $languages[$langCode];
 
		// On enlève le code langue et le slash au début de l'URL
		// avant qu'elle ne soit transmise au Router
		if(strlen($_GET['url']) > strlen($langFromUrl))
		{
			$_GET['url'] = substr($_GET['url'], strlen($langFromUrl));
		}
		else
		{
			$_GET['url'] = '/';
		}
	}
}
 
Configure::write('Config.languages', $languages);
Configure::write('Config.language',  $language);
Configure::write('Config.langCode',  $langCode);
?>

CakePHP va se servir de la clé 'Config.language' pour aller chercher la traduction dans le fichier .po de la langue identifiée.

3.2 Intégrer le code langue dans les liens hypertextes

Les liens hypertextes de notre site sont presque toujours créés à partir des méthodes link() ou url() du HtmlHelper. Nous avons besoin ici d’ajouter le code langue au début de chaque lien, nous allons donc créer notre propre Helper et créer deux méthodes link() et url() qui le feront automatiquement. En procédant de la sorte, nous ne touchons pas au HtmlHelper et pouvons continuer à l’utiliser dans un contexte ou la langue est inutile (dans l’administration par exemple).
Nous créons donc un nouveau fichier : {app}/views/helpers/i18n.php. Il est extrêmement simple : nous créons une méthode url() qui retourne le résultat de la méthode url() du HtmlHelper en le précédant du code langue, puis la méthode link() qui retourne le résultat de la méthode link() du HtmlHelper.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class I18nHelper extends HtmlHelper
{
	function url($url = null, $full = false)
	{
		return '/' .
		       Configure::read('Config.langCode') .
		       parent::url($url, $full);
	}
 
	function link($title, $url = null, $htmlAttributes = array(), $confirmMessage = false, $escapeTitle = true)
	{
		return parent::link($title, $url, $htmlAttributes, $confirmMessage, $escapeTitle);
	}
}
?>

Nous ajoutons le nouveau Helper à notre application :

1
2
// {app}/app_controller.php
var $helpers = array ('Html', 'Text', 'Form', ..., 'I18n');

Nous mettons à jour la Vue de la page d’accueil :

1
2
// {app}/views/pages/home.ctp
$html->link(__('Je suis un lien', true), array('controller' => 'pages', 'action' => 'display', 'home'));

Devient :

1
$i18n->link(__('Je suis un lien', true), array('controller' => 'pages', 'action' => 'display', 'home'));

3.3 Passer d’une langue à l’autre

Poursuivons en créant les liens de passage d’une langue à l’autre :

1
2
3
4
5
// {app}/views/layouts/default.ctp
<div id="flags">
	<a href="/fr">Français</a> |
	<a href="/en">English</a>
</div>

3.4 Traduire les URL elles-mêmes

Là encore dans un souci d’optimiser le référencement, nous pouvons adapter les mots clés des URL à chaque langue. Imaginons un lien vers une page de contact :

1
2
3
4
5
6
// {app}/views/layouts/default.ctp
<div id="footer">
<?php
e($i18n->link(__('Page des contacts', true), array('controller' => 'pages', 'action' => 'display', 'contact')));
?>
</div>

Dans le Router :

1
2
// {app}/config/routes.php
Router::connect('/nous-contacter', array('controller' => 'pages', 'action' => 'display', 'contact'));

En l’état, le lien pointe sur /fr/nous-contacter si nous sommes sur une page en français. L’URL est parfaite. Mais si nous sommes sur une page en anglais, le lien pointe sur /en/nous-contacter… Eduquons la vache espagnole :

1
Router::connect('/'.__('nous-contacter', true), array('controller' => 'pages', 'action' => 'display', 'contact'));

Cette fois, les mots clés de l’URL sont traduisibles dans le fichier .po de chaque langue !

1
2
3
4
// {app}/locale/eng/LC_MESSAGES/default.po
#: \config\routes.php:2
msgid "nous-contacter"
msgstr "contact-us"

Le lien vers la page contact depuis une page en anglais pointe maintenant sur /en/contact-us. La souplesse est totale pour l’optimisation du référencement.
Et voilà notre interface traduite, nous pouvons passer d’une langue à l’autre et tous nos liens sont optimisés pour une indexation efficace dans chaque langue.

4. Traductions dans la base de données

Si notre site était unilingue, une table articles aurait un id, un titre et une description. Sur un site multilingue, nous devons externaliser tous les champs traduisibles d’une table et les stocker dans une table des traductions.

4.1 Création des tables

CakePHP fournit la définition de la table des traductions. Nous allons l’installer via la console : nous la lançons avec l’argument “i18n” et choisissons le menu “I”. Nous pouvons constater dans phpMyAdmin que Cake a créé une table nommée “i18n”.
Créons la table articles, sans les champs susceptibles d’être traduits (le titre, le corps, le résumé, etc.) :

1
2
3
4
5
CREATE TABLE `articles` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
`created` DATETIME NULL ,
PRIMARY KEY ( `id` )
);

Rentrons un article de test :

1
2
3
4
5
6
7
INSERT INTO `articles` (`id`, `created`) VALUES
(1, NOW());
INSERT INTO `i18n` (`id`, `locale`, `model`, `foreign_key`, `field`, `content`) VALUES
(1, 'fre', 'Article', 1, 'titre', 'Mon premier article'),
(2, 'fre', 'Article', 1, 'description', 'Ceci est la version française'),
(4, 'eng', 'Article', 1, 'titre', 'My first article'),
(5, 'eng', 'Article', 1, 'description', 'Here is the english version');

4.2 Préparation du Modèle

Voyons la définition du Modèle Article, auquel nous allons lier le comportement Translate en précisant quels sont les champs à traduire, et Cake va automatiquement gérer les différentes traductions pour toutes les opérations comme save, find, findAll, etc.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Article extends AppModel
{
	var $name = 'Article';
 
	var $actsAs = array(
		'Translate' => array(
			'titre'       => 'Titres',
			'description' => 'Descriptions'
		)
	);
 
	var $validate = array(
		'titre' => array(
			'rule' => '/\S+/',
			'required' => true,
			'allowEmpty' => false,
			'message' => 'titre_non_vide'
		),
	);
}

Avertissement : nous avons ouvert un ticket concernant le TranslateBehavior de la version 1.2.0.7125-rc1, qui fait échouer les règles de validation pour les champs traduits.
Mise à jour du 27/06/08 : la dernière version, 1.2.0.7296-rc2, corrige le problème.

4.3 Les actions de consultation

Commençons par deux actions simples dans le Contrôleur ArticlesController :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ArticlesController extends AppController
{
	var $name = 'Articles';
 
	var $paginate = array(
		'order'     => 'Article.created DESC',
		'limit'     => 10,
		'recursive' => -1
	);
 
	function index()
	{
		$this->set('data', $this->paginate());
	}
 
	function view($id = null)
	{
		$this->set('data', $this->Article->read(null, $id));
	}
}

Si nous nous rendons sur la page /fr/articles, CakePHP va renvoyer :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Array
(
    [0] => Array
        (
            [Article] => Array
                (
                    [id] => 2
                    [created] => 2008-06-07 12:41:31
                    [locale] => fre
                    [titre] => Mon premier article
                    [description] => Ceci est la version française
                )
        )
)

Allons sur la page /en/articles :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Array
(
    [0] => Array
        (
            [Article] => Array
                (
                    [id] => 2
                    [created] => 2008-06-07 12:41:31
                    [locale] => eng
                    [titre] => My first article
                    [description] => Here is the english version
                )
        )
)

Nous voyons avec bonheur que Cake renvoie les données dans la bonne langue, et ce sans que nous ayons à manipuler de tableaux complexes dans les Vues, tout se passe comme si nous travaillions avec une simple table articles !

4.4 Les actions d’ajout et d’édition

Nos actions d’ajout (edit/null) et d’édition (edit/$id) doivent au contraire accueillir l’anglais et le français simultanément : nous le faisons en passant la liste des langues à gérer au Modèle Article :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ArticlesController extends AppController
{
	...
	function edit($id = null)
	{
		// Langues à éditer
		$locales = array_values(Configure::read('Config.languages'));
		$this->Article->locale = $locales;
 
		// Sauvegarde
		if(isset($this->data))
		{
			$this->Article->save($this->data);
			$this->redirect('index');
		}
 
		$this->data = $this->Article->read(null, $id);
	}
}

Prenons soin de correctement nommer les champs de la Vue :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php e($form->create('Article', array('action' => 'edit'))); ?> 
<?php e($form->input('Article.id')); ?> 
 
<fieldset>
	<legend>Titres</legend>
	<?php
	// On récupère les éventuelles traductions existantes :
	$titres = Set::combine($this->data['Titres'], '{n}.locale', '{n}.content');
 
	foreach(Configure::read('Config.languages') as $codeLang => $locale):
		e($form->input(
			'Article.titre.'.$locale,
			array(
				'label' => "Titre ($codeLang) :",
				'value' => isset($titres[$locale]) ? $titres[$locale] : ''
			)
		));
	endforeach;
	?> 
</fieldset>
 
<fieldset>
	<legend>Descriptions</legend>
	<?php
	$descriptions = Set::combine($this->data['Descriptions'], '{n}.locale', '{n}.content');
 
	foreach(Configure::read('Config.languages') as $codeLang => $locale):
		e("<div>Description ($codeLang) :<br>");
		e($form->textarea(
			'Article.description.'.$locale,
			array(
				'value' => isset($descriptions[$locale]) ? $descriptions[$locale] : ''
			)
		));
		e('</div>');
	endforeach;
	?> 
</fieldset>
 
<?php e($form->end('Valider')); ?>

Là encore si nous respectons ce format, CakePHP s’occupe du reste et enregistre les informations de l’article et les traductions automatiquement.

Et voilà notre application multilingue terminée !

Pierre-Emmanuel Fringant

Commentaires

Bonjour Pierre-Emmanuel,

Excellent article comme d’habitude. Je consulte ton blog avec beaucoup d’intérêt depuis des mois. Il a le mérite compiler brillamment sous forme de cas clairs, les centaines de lectures que j’ingurgite sur cakePhp au fil du net.

Ma contribution. A moins que quelque chose m’ait échappé, je pense qu’il n’est pas utile de recopier le contenu de la méthode ‘link’ dans ton helper I18n.
Le helper peut se résumer à mon sens à ceci :

class I18nHelper extends AppHelper {
   	var $helpers = array('Html');
 
	function url($url = null, $full = false) {
		return '/' .
			   Configure::read('Config.langCode') .
			   parent::url($url, $full);
	}
 
	function link($title, $url = null, $htmlAttributes = array(), $confirmMessage = false, $escapeTitle = true) {
		 $url = $this->url($url);
		 return $this->Html->link($title, $url, $htmlAttributes, $confirmMessage, $escapeTitle);
	}
}

Qu’en penses-tu ?

Aussi, pour éviter d’utiliser un nouvel helper i18n->link qui obligerait à rééditer un site déjà construit avec le helper d’origine $html->link, on peut éditer ‘app_helper.php’ à la racine et y ajouter :

class AppHelper extends Helper {
	function url($url = null, $full = false) {
		return '/' .
		   Configure::read('Config.langCode') .
		   parent::url($url, $full);
	}
}

Aussi, et c’est peut être de ça dont tu parles dans ta note à propos du bug, la fonction __() n’était pas reconnue dans les $validate lors de mes derniers essais (avec beta antérieure à la récente RC1).

'message' => __("Donnez un titre à l'article.", true)

Je n’ai pas essayé depuis et j’espère que c’est corrigé car très ennuyeux pour un site multilangue… Je suis inquiet.

Bonjour Jay,

Excellente idée pour la méthode link() de mon I18nHelper, j’ai mis l’article à jour en la corrigeant légèrement (parent::link() au lieu de $this->Html->link()), merci !

Tu as raison aussi pour l’utilisation de la fonction __() dans l’array $validate du Model, ça ne fonctionne pas (je n’avais pas encore testé mes règles de validation ici…).
On peut contourner en renvoyant un identifiant d’erreur et de gérer la chaine traduite dans la vue du formulaire :
$form->input('titre', array('error' => array('titre_non_vide' => __('Le titre doit être renseigné.', true))));

Mais le bug que j’ai découvert entraîne l’ignorance des règles de validation pour les champs qui ne sont pas dans la table du modèle à traduire (dans notre exemple, les règles de validation sont ignorées pour les champs titre et description).

Content d’avoir participé :)
Encore mieux le ‘return parent::link(…’ !

J’ai lu que le non fonctionnement du __() dans $validate est un sujet qui leur a été reporté, ils l’ont changé dans une précédente revision et sont revenus en arrière un dans la revision d’après.
Je suppose qu’ils partent du principe que c’est pas très MVC de donner accès à cette fonction dans le Modèle… Dans ce cas pourquoi donner la possibilité de définir un message d’erreur dans celui-ci ?

La team cakePhp a la réputation d’être bornée et parfois déconcertante.

Bravo Pierre-Emmanuel ! Encore un super tuto, clair, simple, précis… comme on les aime !

Bon, petites remarques :

- réécrire les variables superglobales n’est jamais une très bonne idée, donc modifier le contenu de $_GET['url'] pour déterminer la langue devrait être fait autrement… je cherche une méthode !

- j’ai lu à plusieurs endroits qu’il était préférable de travailler avec les fichiers “.mo” plutôt que les “.po”, or Poedit nous permet de générer automatiquement les “.mo” à la sauvegarde d’un catalogue (option à cocher dans les préférences). Que pensez-vous de cette différence “.mo” / “.po” ?

- tu ne reparles pas des autres fonctions du gettext, apportent-elles quelque chose ? ne permettent-elles pas de “contourner/corriger” certains problèmes comme ceux que vous soulevez avec Jay ?

- on peut avoir besoin de traduire des chaînes qui sont dans le coeur de Cake et non dans le /app, comme par exemple les noms des mois utilisés par le helper form pour les champs dates. Dans ce cas, on peut très bien extraire des chaînes sur un fichier précis du coeur, mais je ne sais pas comment merger “automatiquement” ces extractions complémentaires avec “default.pot”, actuellement je le fais à la main, avez-vous des idées sur cette problématique ?

Jay> j’ai corrigé la méthode link() du I18nHelper qui provoquait l’ajout du code langue en double au début de l’URL… Il suffit donc de retourner ce que renvoie la méthode parente (HtmlHelper::link()), puisqu’elle fait appel à $this->url(), donc si nous nous trouvons dans la classe I18nHelper, $this->url() renvoie déjà l’URL avec le code langue.

Avairet>
1. “réécrire les variables superglobales n’est jamais une très bonne idée”
C’est une question de sécurité selon toi ? Si tu pouvais nous en dire un peu plus sur le fait que ce soit pas conseillé, j’avoue ne pas être calé sur cette question.
2. J’ai cru comprendre que les .mo sont les .po compilés, j’éviterai donc de les éditer directement… Quand le debug est à 2, Cake se sert des .po, puis quand on passe en prod avec le debug à 0, il calcule les .mo pour aller plus vite.
3. Je découvre moi-même les fonctions gettext, quand j’en aurai fait le tour, ça sera le sujet d’un nouvel article !
4. Je ne pense pas qu’on puisse le faire autrement qu’à la main, mais comme cette opération ne doit se faire qu’une seule fois, au début du dév, ça n’est pas très gênant.

1. C’est une question de principe et un peu de sécurité, par commodité d’usage il est tentant de modifier $_GET et $_POST, mais par nature, elles représentent des données transmises par le client et ne devraient pas être reconstruites/modifiées/complétées, même si rien ne l’interdit ! C’est un risque d’erreur (si on utilise à nouveau $_GET ou $_POST plus loin dans le code).

2. Je ne parle pas d’éditer les “.mo”, Poedit le fait tout seul à partir du “.pot” ou du “.po”. Tu m’as donné un argument : en utilisant directement un “.mo” on ne demande plus à Cake de compiler ! On gagne donc en performance. Il y avait aussi un argument stabilité je crois, lu à travers le GoogleGroups ou sur un blog d’un Master of Cake…

3. OK, désolé, nous aprendrons donc ensemble !

4. Ben non, cette opération peut-être exécutée plusieurs fois par projet, dès lors que l’on utilise les Nightly Builds ou que l’on souhaite utiliser un nouveau Component/Behavior/Helper du coeur…

Juste un commentaire hors de propos pour vous féliciter sur la(es) qualité(s) de vos articles, et sur la nouvelle mouture de votre site.

Bonjour, et merci pour ce tuto.

Cependant j’avoue être très étonné par la “compléxité” de mise en oeuvre.

Il est assez étrange que CakePhp ne gère pas cela d’origine…

Devoir créer un helper pour gérer l’identifiant de langue dans l’URL, je trouve ça anormal pour un framework de cette envergure.

Idem pour toute la partie “Analyse de l’URL”, comment ont-ils pu laisser passer quelque chose comme ça ?

J’utilise actuellement un framework que je me suis développé tout seul, et bien que n’était pas un super expert de Php, il gère sans problème le code de langue dans l’URL, c’est la moindre des choses quand on prévoie un framework capable de gérer l’i18n.

En tout cas, encore merci pour le tuto, mais du coup je suis pas sûr de me séparer de mon framework perso.

Bonne continuation :)

Bravo pour ce tuto.
Il manque une chose que je cherche désespéremment : comment faire pour scinder le fichier default.po en plusieurs fichiers. Je souhaiterais avoir un fichier .po par vue, histoire de ne pas avoir un seul et unique fichier default.po de 3 kilomètres.

Les cript de cakephp permettent de générer un fichier .po par vue mais par défaut, la moteur de cake ne prend pas en compte les fichiers générés. Exemple : je créé une vue dans /views/pages/display.po est je souhaiterais que le fichiers app/locale/fre/LC_MESSAGES/-views-pages-display.po soit utilisé pour la traduction.

@pizaninja
Tu peux creer autant de po que tu veux. Pour afficher il suffit de remplacer __(), par __d(’filename’, ‘msgid’).
Supposons un fichier error.po, tu peux ainsi faire __d(’error’, ‘msgerrorid’).

Excellent article, très complet et précis.
Je n’ai pas vu d’équivalent ailleurs…

Petite remarque sur la partie “Les actions d’ajout et d’édition”.
J’ai rencontré un problème relatif au rechargement de la vue en cas d’erreur de validation du modèle.
Dans ce cas (en tout cas avec CakePHP 1.2.0.7296 RC2), les tableaux contenant les localisations ne sont pas rechargés et d’autre part, il faut recharger les champs traduits avec ce qui a été saisi par l’utilisateur.
Afin de gérer correctement ce cas de figure, il convient de rajouter une petite condition à l’initialisation des tableaux dans la vue, comme suit (exemple) :

// Récupération des traductions
if(isset($this->data['titles'])) {
	$titles = Set::combine($this->data['titles'], '{n}.locale', '{n}.content');
} else {
	$titles = $this->data['Cms']['title'];
}

Salut,

Très bon article globalement. J’ai découvert poedit, qui va m’être extrêmement utile.

Une petite remarque néanmoins concernant le gros bloc de code de boostrap utilisé pour le routing quand on a le paramètre de langue : il existe une façon plus propre de le gérer, en utilisant la récupération de paramètres du Router.

Dans routes.php, on peut créer la route suivante :
Router::connect(’/:langue/:controller/:action/*’, array(), array(’langue’=>’fr|en’));

En indiquant ceci au Router, dans le cas ou le premier paramètre est “fr” ou “en”, il l’extraiera de l’url et le transmettra à l’application via params.

Il ne reste plus qu’à initialiser la langue courante depuis le app_controller, en checkant $this->params['langue'] .

Concernant le transport du paramètre de langue, on peut le gérer uniquement en surclassant la fonction url() dans app_helper. La méthode que j’utilise actuellement n’est pas suffisament propre (pour être présentée ici), mais à priori on doit pouvoir le gérer en rajoutant uniquement le paramètre “langue” tel qu’on l’a défini dans les routes, à l’url passée ($url['langue'] = Configure::read(’langue’)). Grace au reverse routing, cake se débrouille pour le placer là ou il faut dans l’url.

Bon multilingue à tous !
Sébastien

Très bon article, le plus complet que j’ai vu sur le multilingue, que je pratique pourtant depuis plusieurs mois.

Je viens de découvrir ce blog au contenu excellent en général.
En dehors de l’auteur, il y aussi des intervenants de qualité. Hein francky06l, je ne savais pas que tu parlais français ;) Merci pour tes tutos Auth/ACL en passant!

Voici un petit comment je procède pour l’interface. J’espère que cette façon defaire vous sera utile.

// app/app_controller.php
class AppController extends Controller {
...
   var $domain = 'default';
...
   function beforeFilter() {
...
       $this->domain = 'default';
   }
 
   function beforeRender(){
...
       $this->set('domain', $this->domain);
   }
}
 
// app/controllers/xxx_controller.php :
class XxxController extends AppController {
     function yyy($index = 0) {
...
       $this->set('domain', '-views-xxxs-yyy');
...
   }
}
 
// app/views/xxxs/yyy.ctp :
...
__d($domain, "SENTENCE_TO_TRANSLATE");
...
 
// app/locale/eng/LC_MESSAGES/-views-xxxs-yyy.po :
...
msgid  "SENTENCE_TO_TRANSLATE"
msgstr "sentence translated"
...
 
// app/locale/fre/LC_MESSAGES/-views-xxxs-yyy.po :
...
msgid  "SENTENCE_TO_TRANSLATE"
msgstr "phrase traduite"
...

Optimisation de la méthode pour faire du multilangue:
grâce au commentaire de Sébastien Charrier, j’ai pu simplifier le code (surtout le bootstrap).

// vider bootstrap.php et ajouter :
Configure::write('Config.languages', array(
  'fr' => 'fre',
  'en' => 'eng',
);
 
// ajouter dans router.php :
Router::connect('/:lang/:controller/:action/*', // format de l'url
  array('lang' => 'fr'), // langue par défaut
  array('lang' => 'fr|en')); // langues gérées
 
// créer app_helper.php et y saisir :
class AppHelper extends Helper {
  // surcharger la fonction appelée par $html-&gt;link()
  function url($url = null, $full = false) {
    // si l'url est sous form de array et que le paramètre lang n'y est pas déjà déclaré
    if (is_array($url) && array_key_exists('lang', $url)) {
      $url['lang'] = Configure::read('Config.language');
    }
    return parent::url($url, $full);
  }
}
 
// app_controller.php, ajouter dans beforeFilter :
// récupérer la langue détectée par le Router
$lang = $this->params['lang'];
// I18n ne récupère plus la langue du 'Config.language' (bizarre, je vais contacter les core dev pour leur demander si c'est normal), donc on lui force la langue :
App::import('Core', 'i18n');
$I18n =& I18n::getInstance();
$I18n->l10n->get($lang);
 
// Configure inutile car I18n le fait déjà
//Configure::write('Config.language',  $lang);

Et avec ces modifs:
- les urls /fr/articles et /en/articles sont gérées
- les liens contiennent le code de langue, sans changer de helper
- on se repose plus sur l’automatisme de Cake

@Sebastien Charrier, @Djiize : Merci messieurs pour votre participation, je testerai la méthode de Djiize dès que possible, elle m’a l’air effectivement plus propre que celle que j’ai proposée.

Quelques commentaires et questions à propos de la dernière solution proposée par Djiize.

Tout d’abord deux détails :
- il manque une parenthèse fermante pour le bout de code à ajouter dans le bootstrap.
- et si je ne me trompe pas dans l’helper, il faudrait plutôt écrire !array_key_exists(’lang’, $url).

Pour les questions :
- Ne faudrait-t-il pas vérifier dans le app_controller que $this->params['lang'] existe bien ? Que se passe-t-il si tu appelles la route / par exemple ?
- Et que deviennent les liens du tutoriel pour changer de langue ?

@Julien : merci pour tes corrections, la parenthèse manquait et le array_key_exists() est plus approprié comme test
Pour répondre à tes questions :
- le 2e paramètres du Router::connect définie ‘fr’ comme langue par défaut, ce qui fait que le paramètre ‘lang’ existera même s’il n’est pas présent dans l’URL. Il y a aussi possibilité de rajouter le paramètre ‘lang’ sur la route /
- voici les liens que j’utilise, ils permettent de changer de langue en restant sur la même page:
link(’Français’, am($this->params['pass'], array(’lang’ => ‘fr’))) ?>
link(’English’, am($this->params['pass'], array(’lang’ => ‘en’))) ?>

@Djiize : merci pour les réponses.

A propos du array_key_exists(), je ne suis pas sur de bien comprendre ce que tu veux dire par “le array_key_exists() est plus approprié comme test”. Simplement je voulais souligner le fait que ton commentaire dans le code et le code ne semblent pas se correspondre. Il manque le point d’exclamation devant array_key_exists pour que le test fonctionne. Si j’ai bien compris, cela nous permet d’ajouter le paramètre ‘lang’ dans l’URL quand il n’y est pas.

D’autre part, j’ai rajouté comme tu le disais dans ton commentaire le paramètre ‘lang’ sur la route ‘/’ parce que sinon on se retrouve avec une erreur de ce type “Undefined index: lang [APP/app_controller.php, line 20]“.

@Julien : oui et merci, j’ai pataugé au niveau du code. En fait, j’avais d’abord utilisé !isset($url['lang']) avant de mettre array_key_exists() dans mon commentaire.

Voici le app_helper.php corrigé:

class AppHelper extends Helper {
  function url($url = null, $full = false) {
    if (is_array($url) &amp;&amp; !array_key_exists('lang', $url)) {
      $url['lang'] = Configure::read('Config.language');
    }
    return parent::url($url, $full);
  }
}

un truc sur lequel je viens de tomber, le coeur de cake n’utilise pas encore les fonctions mb_* en interne, il faut pour le moment utiliser la classe Multibyte qui récupère le App.encoding et simplifie l’utilisation des fonctions mb_*. Par contre je sais qu’il y a des problèmes sur les validations notamment sur la longueur des chaînes de caractères.

// inclure Multibyte
dans bootstrap.php :
App::import('Core', 'Multibyte');
 
// utiliser à la place des fonctions mb_*
Multibyte::strtolower('ÀÉÔÏ');
au lieu de
mb_strtolower('ÀÉÔÏ', 'UTF-8');
 
remplacer ucfirst :
$string = Multibyte::strtolower($string);
$string = sprintf(
    '%s%s',
    Multibyte::strtoupper(Multibyte::substr($string,0,1)),
    Multibyte::substr($string,1));

à mettre dans une class custom type MyMultibyte::ucfirst() peut-être.. si quelqu’un a plus propre je suis preneur !

// utiliser dans la validation
dans app_model.php
function between($check, $min, $max) { 
    $length = Multibyte::strlen($check); 
    if ($length &gt;= $min &amp;&amp; $length &lt;= $max) { 
        return true; 
    } else { 
        return false; 
    } 
}

j’ai oublié, le ticket correspondant à l’intégration du multibyte dans le coeur : https://trac.cakephp.org/ticket/2218

Salut !

Heureux de voir que ma contribution a pu faire avancer le shmilblick.

Je suis confronté actuellement à une partie dont on ne parle pas dans cet article, mais qui est bel et bien liée à l’internationalisation d’un site : la gestion des champs spéciaux de type date ou monnaie.

En effet, peu importe la langue dans laquelle est configurée le site, quand on affiche un input de type datetime ou date ($form->input(’name’,array(’type’=>’datetime’)), les inputs affichés sont au format anglais.

J’ai vu que l’on pouvait gérer le format via les paramètres dateformat/timeformat, mais rien d’automatique …

Nos formateurs préférés auraient ils une esquisse de solution ? (je n’ai rien trouvé de probant sur le net jusqu’à présent :s)

Pour la gestion des “locales” (format des dates, monnaie, etc), regarde ce précédent sujet : Afficher les dates en français.

Pour traduire les noms de mois en français (qui s’affichent dans les select quand on doit saisir une date), regarde ce précédent sujet : Multilingue : quelques astuces, le point 3.

Hello. D’abord merci pour cet excellent article qui tombe à pic.

J’ai mis en place mon application multilingue en suivant la procédure proposée par Pierre-Emmanuel Fringant puis l’ai modifiée selon la proposition de Djiize. Globalement, ça marche bien sous CakePHP 1.2.0.7296 RC2.

Toutefois, je rencontre le problème suivant: lors de la création/modification d’un enregistrement dans la base de données, le champ field n’est pas renseigné. Aussi, un champ non traduit (user_created) n’est pas non plus renseigné dans la table articles.

Je me casse la tête là-dessus depuis un moment sans trouver la solution. Quelqu’un a-t-il rencontré le même problème et trouvé la parade?

Mon code est repri

Bonjour,

Je reviens sur la fin de mes premiers messages quant à l’impossibilité d’utiliser __(”) dans les messages de validation.

Je n’avais pas regardé le code avant d’intervenir et un article que j’avais lu m’avait induit en erreur.
Cette impossibilité n’est pas du à CakePhp mais bel et bien à la structure de Php qui interdit l’utilisation de fonctions hors méthodes dans une classe.

Voici la marche à suivre pour contourner le problème et enfin bénéficier de l’internationalisation des messages dans vos validations de Modèles :

http://blog.jaysalvat.com/articles/comment-utiliser-la-fonction-dinternationalisation-dans-les-validations-de-cakephp.php

En espérant avoir aidé.

Bonjour,

Trois problèmes pour lesquels je n’ai pas trouvé de solution malgré mes recherches:

- lorsque je supprime un enregistrement, seul l’enregistrement de la table “principale” est supprimé. Les champs traduits dans la table i18n ne sont pas supprimés. A terme, ça risque de devenir gênant d’avoir une table i18n surchargée d’infos obsolètes.
Voici ma fonction de suppression:

function delete($id = null)
	{
    $this-&gt;Article-&gt;del($id);
 
		if(!$this-&gt;Article-&gt;exists())
		{
			$this-&gt;Session-&gt;setFlash(__('Enregistrement introuvable',true),'message_error');
		}
		else
		{
			$this-&gt;Article-&gt;del();
			$this-&gt;Session-&gt;setFlash(__('Données supprimées',true),'message_ok');
		}
		$this-&gt;redirect($this-&gt;referer());
	}

- En utilisant la méthode de Djiize, le routage admin standard de Cakephp ne fonctionne plus. Y-a-t-il un moyen de contourner ce problème? J’ai essayé plusieurs variantes de routes dans routes.php sans succès.
- Les messages d’erreur lors de la validation ne sont pas affichés dans la vue du formulaire.
Je traite l’enregistrement comme suit dans le contrôleur:

  function edit($id = null)
  	{
 
	// Save
	if(isset($this-&gt;data))
	{
if($this-&gt;Article-&gt;save($this-&gt;data)){
$this-&gt;redirect('gestion');
}
}
$this-&gt;data = $this-&gt;Article-&gt;read(null, $id);
	}

et dans la vue je place la fonction d’affichage de l’erreur:

[...]
echo $form-&gt;error('Article.titre.'.$locale);
[...]

Bonjour,

Encore un complément d’informations et une méthode simplifiée pour colporter le choix de la langue dans l’URL.

http://blog.jaysalvat.com/articles/choix-des-langues-par-url-dans-cakephp.php

Suite du message précédent :

Je viens de voir que Djiize propose une solution proche de la mienne dans ces mêmes commentaires. :)

La méthode proposée pour réécrire l’url dans Apphelper n’est vraiment pas terrible puisque ne fonctionne que si cake est à la racine de l’url….

La méthode de Djiize et Jay Salvat est bien mieux pensée, mais pas encore parfaite, puisque ne fonctionnant pas si on utilise des paramêtres nommés, en effet le paramêtre de langue perd son caractère préfixé et se place avec les autres paramêtres nommés à la fin de l’url sous la forme lang:fr. La seule solution que j’ai pu trouver pour l’instant est d’utiliser Router::connectNamed(). Je suis preneur si quelqu’un a plus d’infos.

J’attaque enfin ma première appli avec i18n complète, et ce tutorial est vraiment une très bonne base : BRAVO !

Un petit point qui me tracasse, au niveau de la vue d’édition : pourquoi manipuler les tableaux au sein de la vue en elle-même ? Ne serait-ce pas plutôt au controller de faire ça ?

En tout cas, voici un bout de code que j’ai écrit afin d’éviter d’avoir une variable par champ et de reformater tout d’un coup … si cela peut aider quelqu’un !

$dirtyContent = $this->Content->read(null, $id);
foreach($this->Content->actsAs['Translate'] as $realFieldName => $storedFieldName){
            $this->data['Content'][$realFieldName] = Set::combine($dirtyContent[$storedFieldName], '{n}.locale', '{n}.content');
}

J’ai aussi été confronté au problème cité par jamesg à propos du routing.admin qui saute.

Je pense avoir contourné le problème (un peu dirty) en plaçant le Routing de façon conditionnelle.

Donc dans routes.php, j’ai mis:

$TestAdmin = (strpos($this-&gt;here, "/".Configure::read('Routing.admin')."/") === false) ? true:false;
if($TestAdmin){
	Router::connect('/:lang/:controller/:action/*', // format de l'url
	  array('lang' =&gt; 'fr'), // langue par défaut
	  array('lang' =&gt; 'fr|en')
	);
}

Du coup, la variable de langue est passée en paramètre nommé quand on est en mode admin. J’ai donc groupé la gestion du choix de langue dans une fonction que j’ai placé dans app_controller.php et que j’appelle depuis beforeFilter()

	function __setLang(){
		if(isset($this-&gt;params["lang"]))
			$lang = $this-&gt;params["lang"];
		elseif(isset($this-&gt;params["named"]["lang"]))
			$lang = $this-&gt;params["named"]["lang"];
		else
			$lang = DEFAULT_LANG;
		$this-&gt;L10n = new L10n();
		$this-&gt;L10n-&gt;get($lang);
		Configure::write('Config.language',  $lang);
	}

Une remarque finale d’ordre plus général. Le behavior “Translate” de Cake ne permet pas de traduire les modèles associés, et si j’en crois les messages du groupe Google, ce n’est pas pour demain.

J’ai donc opté pour l’implantation de la contribution que j’ai trouvé à:
http://www.palivoda.eu/2008/04/i18n-in-cakephp-12-database-content-translation-part-2/

et qui utilise des champs supplémentaires dans les tables plutôt qu’une table unique de traduction.

Par contre, l’auteur n’avait pas besoin de se préoccuper d’enregistrer (save) de nouvelles entrées, donc en l’état ça ne fonctionne qu’en lecture, mais dans mon cas ça suffit…

FWIW, et bonne année à tous,
Patrice

Bonjour,
Concernant la redirection vers l’admin j’ai pour ma part rajouté une route avant la gestion des langues (en partant du principe que la zone d’admin n’est accessible qu’en fr).

Router::connect('/admin/:controller/:action/*', array('lang'=&gt;'fr','admin'=&gt;true));
Router::connect('/:lang/:controller/:action/*', // format de l'url
	array('lang' =&gt; 'fr', 'action'=&gt;'index'), // langue par défaut
    array('lang' =&gt; 'fr|en|de', 'controller'=&gt;'pages|candidatures|offres') // langues gérées
);

On devrait pouvoir l’adapter pour une administration multilingue.

Bonsoir Pierre-Emmanuel,

A propos de la traduction avec la table i18n, il m’est impossible de la faire fonctionner avec votre méthode.
Si je fais un insert SQL comme tu le décris, je récupère bien les données en édition.
Mais impossible d’en ajouter ou de les modifier.
Est-ce que le composant Security pourrait en être la cause ?

A première vue je n’en ai aucune idée. Comment procèdes-tu ?

Comment je procède à quel propos ? Pour le composant Security ?
Je l’active dans la méthode beforeFilter() du controller :

if (!empty($this-&gt;data)) {	 $this-&gt;Security-&gt;requireAuth('add','edit','delete');	$this-&gt;Security-&gt;requirePost('add','edit','delete');
}

Sinon, au chapitre 4.2 de ton article, tu écris “Cake va automatiquement gérer les différentes traductions pour toutes les opérations comme save, find, findAll, etc.”, mais le save ne fonctionne pas tel quel pour moi. J’ai essayé avec plusieurs versions de Cake sans succès.

Donc pour l’instant je me vois contraint d’utiliser des méthodes customisées dans le modèle pour l’insert et l’update dans la table i18n.

Ca n’est pas normal, tout doit se faire automatiquement. As-tu essayé de désactiver le composant Security, histoire de voir si c’est lui qui coince ?
Sinon je ne vois d’autre solution que d’essayer de suivre point par point le tutoriel sur une application vierge…

Oui bien sûr, j’ai essayé en désactivant le composant Security, comme sur une version vierge de CakePHP (avec la RC2, RC3 et 1.2 Final).
Je vais encore essayer en partant d’un modèle vierge, car le mien à des relations HABTM.

N’hésite pas à nous faire part des résultats de tes investigations, et désolé de ne pas pouvoir t’aiguiller plus que ça…

“N’hésite pas à nous faire part des résultats de tes investigations”
Compte sur moi.

Effectivement, si je pars d’une version vierge (ici cake_1.2.1.8004) avec juste les deux modèles que sont i18n et articles, et que j’applique ton tutoriel, l’ajout et l’édition fonctionnent très bien.
Il faut que je regarde ce qui peut bloquer sur mon appli.

Je n’en suis pas absolument certain, mais il semblerait que le problème que je rencontrais précédemment soit dû à une mauvaise utilisation (nommage) que je faisais de la clé ‘with’ pour une association HABTM.
C’est en enlevant (uniquement) cette clé (avec la version 1.2.1.8004 en plus) que le problème s’est résolu.
Plus d’infos sur la clé with pour ceux qui souhaitent ici :
http://book.cakephp.org/view/83/hasAndBelongsToMany-HABTM

Bonjour a tous,

Meci déja pour ce super tuto, mais au moment de créer la base de donner avec la console “cake i18n ->I”

il me dit

Error: schema.php could not be loaded

j’ai beau chercher sur le net mais je trouve pas la solution à se problème. Est ce que vous aurez une solution à se petit problème à fin que je puisse continuer se super tuto

Merci d’avance pour vos réponse

Bonjour a tous,
Je suis un mega debutant avec CakePHP. J’ai un petit soucis concerant la redirection:

J’ai suivi tout le tuto, ca marche nikel, le probleme se pose quand je fais une redirection apres avoir ajouter un article par ex.

Dans mon controller j’ai:

// Langues à éditer
		$locales = array_values(Configure::read('Config.languages'));
		$this-&gt;Article-&gt;locale = $locales;
 
		if (!empty($this-&gt;data)) {
			$this-&gt;Article-&gt;create();
			if ($this-&gt;Article-&gt;save($this-&gt;data)) {
				$this-&gt;Session-&gt;setFlash(__('The Article has been saved', true));
				$this-&gt;redirect(array('action'=&gt;'index'));
			} else {
				$this-&gt;Session-&gt;setFlash(__('The Article could not be saved. Please, try again.', true));
			}
		}

Alors a la redirection, mon url se remet à:
http://monsite/
Et ca me met la langue par defaut!
J’aimerai avoir:
http://monsite/fr ou /en d’apres la langue courante.
J’ai pas trouvé de solution. Une idee ?

Rebonjour,

J’explique mon probleme sur les redirections sur le forum francophone de cakephp:

http://forum.cakephp-fr.org/viewtopic.php?id=265

J’ai toujours pas de solution donc si vous avez une idee, n’hesitez pas.

Merci.

Bonjour,
J’ai essayé de finté avec ton astuce pour revenir à la page precedente apres validation d’un formulaire. Ca me renvoi bien à la page precedente mais le message de confirmation reste dans la mauvaise langue. C’est que si je suis dans la langue EN, et que j’ajoute un article, bin apres validation du formulaire je me redirige vers la page precdente (en EN) mais malheureusement le message de confirmation reste en FR (la langue par defaut).

Je ne sais pas si je m’explique bien !! En tout cas si vous avez besoin de precisions, je suis tout oui.

Bonjour,

J’ai utilisé votre tutoriel pour un site anglais/français sur lequel je travaille présentement.

Seulement, j’ai un petit problème, je m’explique :

- Tous les textes provenant de la BD s’affichent correctement. Autant en français qu’en anglais. C’est merveilleux.

- Seulement, si j’utilise la fonction __() pour afficher une string provenant du fichier .po, les accents sont remplacés par des points d’interrogations. Exemple : “Notre ?quipe”

- Pire encore, si j’utilise la fonction $html->link comme suit :

echo $html-&gt;link(__('Notre équipe',true),'http://www.unsite.com')

Le lien ne s’affiche pas, comme si la fonction __() renvoyait une chaine vide.

J’ai pourtant vérifié, html->charset() retourne bien utf-8, mon App.enconding dans mon fichier core.php est bien utf-8. Bref, je suis dans le flou le plus total.

Merci d’avance.

A première vue je dirais qu’il faut bien vérifier que tous les fichiers de l’application sont bien en utf-8 (étape 1.2 du tutoriel).

En effet,

mon fichier default.ctp est encodé en Latin….

Ainsi, même si mes views étaient en utf-8, rien ne pouvait fonctionner.

Merci!

Bonjour à tous,
Merci pour ce site très bien fourni et très intéressant…

J’ai une question par rapport aux foreign key… Cette question n’est pas propre à cet article, elle est bien plus générale.

Je voudrais savoir s’il faut oui ou non ajouter les relations de type FOREIGN KEY lorsque l’on crée les tables dans la DB ou si on laisse ca uniquement à CakePHP…

ex. dois-je executer cette query au moment de la création des tables…
ALTER TABLE ‘objets’ ADD FOREIGN KEY (’id’) REFERENCES ‘objets_tags’('objet_id’);

Quelle est la meilleure pratique pour ces relations ?
SQL + CakePHP
ou
CakePHP only…

Merci beaucoup!

Bonjour François,
Si ta question n’est pas relative à l’article, tu devrais la poster sur le forum des utilisateurs de CakePHP : http://forum.cakephp-fr.org/
Fred

Bonjour François,

tu devrais effectivement ajouter les index dans la BD, cela facilite les recherches et, en théorie, augmente la rapidité d’exécution.

Les relations que tu spécifies dans Cake ne sont là que pour liés les Modèles entre eux et n’ont donc rien à voir avec la BD en soi.

Selon mon humble avis!

Bonjour, ma question concerne le point 4.3 Les actions de consultation.

Dans l’exemple présenté, on peut consulter les enregistrements dans une langue ou un autre selon l’URL. Cela me semble très bien pour le frontend d’une application.

Maintenant dans un backend, imaginons que nous traduisions en “fr” et “en”, il peut être intéressant pour la personne chargée de créer et traduire les enregistrements de voir les traductions “fr” et “en” du dit enregistrement sur la même page.

Comment récupérer donc la traduction pour mes deux langages et les afficher ainsi dans la même vue?

Merci

Bonjour Julien,
Je me permet de te répondre : il suffit de lire la section 4.4 pour trouver ton bonheur.

// Langues à éditer
		$locales = array_values(Configure::read('Config.languages'));
		$this-&gt;Article-&gt;locale = $locales;

Bonjour,

Bien que ces lignes figures dans mon code, il ne me récupère que l’espagnol (je gère deux langues : anglais et espagnol, l’espagnol étant la langue par défaut).

De fait, si j’utilise l’index basic fourni dans l’article:

function index() {
  $this-&gt;set('data', $this-&gt;paginate());
}

$data est vide et je dois ajouter les deux lignes que tu m’as donné précédemment pour obtenir des résultats. Peut être ai-je raté quelquechose.

Bonjour,

J’ai suivit ce tutoriel à la lettre et la fin du tutorial de donne pas le résultat escompter chez moi.

Voici ma vue edit:

data['News']['titles']);
	// On récupère les éventuelles traductions existantes :
	$titles = Set::combine($this-&gt;data['Titles'], '{n}.locale', '{n}.content');
 
	foreach(Configure::read('Config.languages') as $codeLang =&gt; $locale):
		e($form-&gt;input(
			'News.title.'.$locale,
			array(
				'label' =&gt; "Titre ($codeLang) :",
				'value' =&gt; isset($news_titles[$locale]) ? $news_titles[$locale] : ''
			)
		));
	endforeach;
	?&gt;
 
 
 
	Descriptions
	data['Contents'], '{n}.locale', '{n}.content');
 
	foreach(Configure::read('Config.languages') as $codeLang =&gt; $locale):
		e("Description ($codeLang) :");
		e($form-&gt;textarea(
			'News.content.'.$locale,
			array(
				'value' =&gt; isset($contents[$locale]) ? $contents[$locale] : ''
			)
		));
		e('');
	endforeach;
	?&gt;
 
 
end('Valider')); ?&gt;

et je lorsque j’appelle l’action edit avec un id (/news/edit/1), je ne vois pas le contenu de ma news mais uniquement une erreur s’afficher dans le haut de chaque fieldset:
Notice (8): Undefined index: Titles [APP\views\news\edit.ctp, line 9]

J’ai respecté les conventions et la casse et ne sais pas où chercher mon erreur. Un petit coup de pouce ne serait pas de refus.

Participez

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