<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Cloud Connected &#187; i18n</title>
	<atom:link href="http://www.cloudconnected.fr/tag/i18n/feed/" rel="self" type="application/rss+xml" />
	<link>http://www.cloudconnected.fr</link>
	<description>Thoughts of a french web developer</description>
	<lastBuildDate>Wed, 01 Feb 2012 08:53:57 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.3.1</generator>
		<item>
		<title>Internationalisation d&#8217;une base de données</title>
		<link>http://www.cloudconnected.fr/2009/03/06/internationalisation-dune-base-de-donnees/</link>
		<comments>http://www.cloudconnected.fr/2009/03/06/internationalisation-dune-base-de-donnees/#comments</comments>
		<pubDate>Fri, 06 Mar 2009 15:19:16 +0000</pubDate>
		<dc:creator>Rémi</dc:creator>
				<category><![CDATA[Non classé]]></category>
		<category><![CDATA[i18n]]></category>
		<category><![CDATA[mysql]]></category>
		<category><![CDATA[pdo]]></category>
		<category><![CDATA[php]]></category>

		<guid isPermaLink="false">http://www.the-asw.com/?p=407</guid>
		<description><![CDATA[Disons que vous soyez en charge d&#8217;une application web écrite en PHP/MySQL, par exemple l&#8217;intranet de votre entreprise, et que vous ayez soudainement besoin de l&#8217;internationaliser parce que votre entreprise installe des bureaux à l&#8217;étranger et que, malheureusement, notre merveilleuse langue française n&#8217;est pas parlée dans tous les pays du monde. Pour les templates, pas [...]]]></description>
			<content:encoded><![CDATA[<p>Disons que vous soyez en charge d&#8217;une application web écrite en PHP/MySQL, par exemple l&#8217;intranet de votre entreprise, et que vous ayez soudainement besoin de l&#8217;internationaliser parce que votre entreprise installe des bureaux à l&#8217;étranger et que, malheureusement, notre merveilleuse langue française n&#8217;est pas parlée dans tous les pays du monde.</p>
<p>Pour les templates, pas de problème, <code>gettext</code> est là pour ça (je reviendrais peut-être dessus dans un futur article, si je suis motivé). Si vous n&#8217;avez pas envie de mettre les mains dans le système, votre framework propose sûrement une émulation plus ou moins performante en pur PHP, ou, au pire, une solution &#8220;maison&#8221;. Bref, ça c&#8217;est facile.</p>
<p>Ce qui pose plus de problèmes, ce sont les données présentes en base. Par exemple : la liste de catégories pour les tickets d&#8217;incidents des clients. Elle est stockée en base dans une table qui contient notamment l&#8217;intitulé de cette catégorie. Oui, mais cet intitulé doit être traduit. Et comme les catégories sont gérées dynamiquement, ce n&#8217;est pas envisage d&#8217;utiliser la méthode <code>gettext</code> qui repose sur des fichiers statiques.</p>
<h3>Transformer le modèle</h3>
<p>Nous allons commencer par ajouter une table pour stocker les traductions. Si on envisage la catégorie comme un élément identifié par un id uniquement, les traductions sont liées à une catégorie par une relation &#8220;1-N&#8221; : 1 catégorie possède N traductions. Chaque traduction est identifiée par un code de langue. L&#8217;idée est de retirer les champs contenant du texte à traduire de la catégorie, et de les stocker dans la table contenant les traductions</p>
<div id="attachment_408" class="wp-caption aligncenter" style="width: 395px"><a href="http://www.cloudconnected.fr/wp-content/uploads/2009/03/i18n.png"  rel="lightbox"><img src="http://www.cloudconnected.fr/wp-content/uploads/2009/03/i18n.png" alt="Schéma de la base" title="i18n" width="385" height="142" class="size-full wp-image-408" /></a><p class="wp-caption-text">Schéma de la base</p></div>
<p>On crée donc les tables suivantes :</p>
<pre>
CREATE TABLE `category` (
	`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
	`created_at` DATETIME NOT NULL,
	`deleted_at` DATETIME NULL,
	PRIMARY KEY (`id`)
) ENGINE = InnoDB;

CREATE TABLE `category_i18n` (
	`category_id` INT UNSIGNED NOT NULL,
	`lang` VARCHAR(6) NOT NULL,
	`name` VARCHAR(45) NOT NULL,
	PRIMARY KEY (`lang`, `category_id`),
	INDEX `category_i18n_category_fk` (`category_id` ASC),
	CONSTRAINT `category_i18n_category_fk`
		FOREIGN KEY (`category_id` )
		REFERENCES `category` (`id` )
		ON DELETE CASCADE
) ENGINE = InnoDB;
</pre>
<p><span id="more-407"></span></p>
<h3>Insertion</h3>
<p>Lors de l&#8217;insertion, il faut d&#8217;abord insérer dans <code>category</code>, récupérer l&#8217;id puis insérer dans <code>category_i18n</code>. Puisqu&#8217;on travaille avec plusieurs tables en même temps, l&#8217;utilisation d&#8217;une transaction (avec le moteur de stockage InnoDB) est fortement recommandée.</p>
<pre>
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO category (created_at) VALUES (SYSDATE());
Query OK, 1 row affected (0.00 sec)

mysql> SET @category_id = LAST_INSERT_ID();
Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO category_i18n (category_id, lang, name) VALUES
    -> (@category_id, 'fr', 'Incident réseau'),
    -> (@category_id, 'en', 'The network is fucked up');
Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)
</pre>
<p>Et ça donne :</p>
<pre>
mysql> SELECT * FROM category;
+----+---------------------+------------+
| id | created_at          | deleted_at |
+----+---------------------+------------+
|  3 | 2009-03-06 15:58:44 | NULL       |
+----+---------------------+------------+
1 row in set (0.00 sec)

mysql> SELECT * FROM category_i18n;
+-------------+------+--------------------------+
| category_id | lang | name                     |
+-------------+------+--------------------------+
|           3 | en   | The network is fucked up |
|           3 | fr   | Incident réseau          |
+-------------+------+--------------------------+
2 rows in set (0.00 sec)
</pre>
<h4>Exemple d&#8217;implémentation en utilisant PDO</h4>
<pre>
// $names contient un tableau associatif "langue => valeur"
function newCategory($dbh, $names = array())
{
	if ( empty($names) )
		return false;

	try {
		$dbh->beginTransaction();

		$sql =
			'INSERT INTO category
			(created_at)
			VALUES (SYSDATE())';

		$dbh->exec($sql);
		$id = $dbh->lastInsertId();

		$sql = array();
		foreach ( $names as $lang => $name ) {
			$sql[] = sprintf(
				'(%d, %s, %s)',
				$id,
				$dbh->quote($lang),
				$dbh->quote($name)
			);
		}
		$sql =
			'INSERT INTO category_i18n
			(category_id, lang, name)
			VALUES '.implode(', ',$sql);
		$dbh->exec($sql);

		$dbh->commit();
	} catch (Exception $e) {
		$dbh->rollback();
		throw $e;
	}
}

// appel de la fonction
$dbh = new PDO('mysql:host=localhost;dbname=test', 'test', 'test');
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

newCategory($dbh, array(
	'fr' => 'Incident réseau',
	'en' => 'The network is fucked up'
));
newCategory($dbh, array(
	'fr' => 'Incident système',
	'en' => 'The system screwed up'
));
newCategory($dbh, array(
	'fr' => 'Commande de matériel',
	'en' => 'I need some stuffs'
));
newCategory($dbh, array(
	'fr' => 'Problème typiquement français'
));
</pre>
<h3>Sélection</h3>
<p>Pour récupérer la liste des catégories, il faut savoir dans quelle langue on les veut. Pour cela, on peut se baser sur les préférences indiquée par le navigateur de l&#8217;utilisateur, ou sur ses options s&#8217;il est inscrit, etc. La requête de sélection se fait ensuite par une simple jointure.</p>
<p>Dans l&#8217;exemple suivant, on sélectionne d&#8217;abord les catégories en français et ensuite les catégories en anglais. La catégorie non-traduite n&#8217;apparaît pas. On pourrait utiliser un <code>LEFT JOIN</code> pour avoir un libellé <code>NULL</code> (ça peut servir dans certains cas).</p>
<pre>
mysql> SELECT c.id, ci.name
    -> FROM category c
    -> JOIN category_i18n ci ON (ci.category_id = c.id AND ci.lang = "fr")
    -> WHERE c.deleted_at IS NULL;
+----+-------------------------------+
| id | name                          |
+----+-------------------------------+
|  1 | Incident réseau               |
|  2 | Incident système              |
|  3 | Commande de matériel          |
|  4 | Problème typiquement français |
+----+-------------------------------+
4 rows in set (0.00 sec)

mysql> SELECT c.id, ci.name
    -> FROM category c
    -> JOIN category_i18n ci ON (ci.category_id = c.id AND ci.lang = "en")
    -> WHERE c.deleted_at IS NULL;
+----+--------------------------+
| id | name                     |
+----+--------------------------+
|  1 | The network is fucked up |
|  2 | The system screwed up    |
|  3 | I need some stuffs       |
+----+--------------------------+
3 rows in set (0.00 sec)

mysql> SELECT c.id, ci.name
    -> FROM category c
    -> LEFT JOIN category_i18n ci ON (ci.category_id = c.id AND ci.lang = "en")
    -> WHERE c.deleted_at IS NULL;
+----+--------------------------+
| id | name                     |
+----+--------------------------+
|  1 | The network is fucked up |
|  2 | The system screwed up    |
|  3 | I need some stuffs       |
|  4 | NULL                     |
+----+--------------------------+
4 rows in set (0.00 sec)
</pre>
<h3>Modification</h3>
<p>La modification peut être triviale, ou un petit peu complexe, selon le type d&#8217;interface utilisateur mise en place. Quelques astuces : si un simple <code>UPDATE</code> ne suffit pas, vous devriez regarder du côté de <code>INSERT ... ON DUPLICATE KEY UPDATE ...</code>, ou implémenter une suppression complète immédiatement suivi de nouvelles insertions (toujours avec une transaction).</p>
<h3>Suppression</h3>
<p>Grâce à la clause <code>ON DELETE CASCADE</code> il suffit de supprimer l&#8217;entrée dans catégorie pour que les traductions correspondantes soient également supprimées. Personnellement j&#8217;utilise le champ <code>deleted_at</code> pour effectuer une suppression logique et ne pas casser toutes les clés étrangères des tables qui font référence à la catégorie supprimée.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cloudconnected.fr/2009/03/06/internationalisation-dune-base-de-donnees/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
	</channel>
</rss>

