[Tutoriel] Un site plurilingue.

Statut
N'est pas ouverte pour d'autres réponses.

ozilrit

Elite
Bonjour,

l'on m'a très souvent posé cette question, alors voici un très petit tutoriel sur une manière assez basique de gèrer ce problème.

Les avantages d'une telle méthode sont :
:arrow: la simplicité,
:arrow: l'évolutivité.​

Explication de l'exemple :

Une base de données de produits, disposant d'un champ banal (Prix), et d'un champ à internationaliser (Nom).
Cette base va se diviser en deux tables : _produits (un underscore) et __produits (deux underscores). La différence ? L'une contient les données structurelles (Prix), l'autre les traductions (Nom).

Elles sont définies et indexées comme suit :
_Produits :
ID (entier non-signé, clef primaire), identifieur d'un produit.
Prix (nombre décimal).​
__Produits :
ID (entier non-signé, clef primaire (avec Locale)), identifieur d'un produit.
Locale (char, clef primaire (avec ID)), identifieur d'une langue.
Nom (varchar).
Uniquement Oracle :
_Locales (Code (char, clef primaire), Nom (varchar)).
__Produits ID & Locale sont contraints avec les champs externes _Produits (ID) et _Locales (Code).

Dans le cas présent, si la base de données ne trouve pas de nom correspondant au produit en espagnol, elle cherchera en anglais puis, en dernier lieu, en français.

Assurez-vous simplement de posséder au moins un nom (quelque soit la langue) pour chaque produit.​

MySQL (MyISAM, sans contraintes donc) :
Code:
#DROP TABLE `tutoriel_produits` CASCADE;
#DROP TABLE `tutoriel__produits` CASCADE;

CREATE TABLE `tutoriel_produits` (
  `ID` int(10) unsigned NOT NULL auto_increment,
  `Prix` decimal(8,2) NOT NULL default '0.00',
  PRIMARY KEY  (`ID`)
) ENGINE=MyISAM AUTO_INCREMENT=0 AUTO_INCREMENT=0;

CREATE TABLE `tutoriel__produits` (
  `ID` int(10) unsigned NOT NULL,
  `Locale` char(5) NOT NULL,
  `Nom` varchar(32) NOT NULL,
  PRIMARY KEY  (`ID`,`Locale`)
) ENGINE=MyISAM;

INSERT INTO `tutoriel_produits` (`ID`, `Prix`) VALUES (1, 1.00), (2, 0.05), (3, 0.50);

INSERT INTO `tutoriel__produits` (`id`, `locale`, `nom`) VALUES (1, 'fr_FR', 'Banane'), (2, 'es_ES', 'Aceituna'), (2, 'fr_FR', 'Olive'), (3, 'en_EN', 'Pepper'), (3, 'fr_FR', 'Poivre');
Requête type (testée sur MySQL 5.1) :
Code:
SELECT `P`.`ID` , `P`.`Prix` , (

SELECT `PL`.`Nom` 
FROM `tutoriel__produits` PL
WHERE `PL`.`ID` = `P`.`ID` 
ORDER BY FIELD( `PL`.`Locale` , 'es_ES', 'en_EN', 'fr_FR' ) 
LIMIT 1 
) `Nom` 
FROM `tutoriel_produits` P
GROUP BY `P`.`ID`

Oracle (attention, deux espaces se sont glissés (LOC ALE), supprimez-les) :
Code:
#DROP TABLE "TUTORIEL_LOCALES" CASCADE CONSTRAINTS;
#DROP TABLE "TUTORIEL_PRODUITS" CASCADE CONSTRAINTS;
#DROP TABLE "TUTORIEL__PRODUITS" CASCADE CONSTRAINTS;
#DROP SEQUENCE "SQ_TUTORIEL_PRODUITS_ID";

CREATE TABLE "TUTORIEL_LOCALES" 
(	"CODE" CHAR(5) NOT NULL ENABLE,
	"NOM" VARCHAR(32),
	CONSTRAINT "IX_TUTORIEL_LOCALES_CODE" PRIMARY KEY ("CODE") ENABLE
);

CREATE TABLE "TUTORIEL_PRODUITS" 
(	"ID" NUMBER NOT NULL ENABLE,
	"PRIX" NUMBER(8,2),
	CONSTRAINT "IX_TUTORIEL_PRODUITS_ID" PRIMARY KEY ("ID") ENABLE
);
CREATE SEQUENCE "SQ_TUTORIEL_PRODUITS_ID" MINVALUE 1000 INCREMENT BY 1 START WITH 1000;

CREATE TABLE "TUTORIEL__PRODUITS" 
(	"ID" NUMBER NOT NULL ENABLE,
	"LOCALE" CHAR(5),
	"NOM" VARCHAR(32),
	CONSTRAINT "IX_TUTORIEL__PRODUITS_IDLOCALE" PRIMARY KEY ("ID", "LOCALE") ENABLE,
	CONSTRAINT "FK_TUTORIEL__PRODUITS_ID" FOREIGN KEY ("ID") REFERENCES "TUTORIEL_PRODUITS" ("ID") ENABLE,
	CONSTRAINT "FK_TUTORIEL__PRODUITS_LOCALE" FOREIGN KEY ("LOCALE") REFERENCES "TUTORIEL_LOCALES" ("CODE") ENABLE
);

INSERT INTO "TUTORIEL_LOCALES" ("CODE", "NOM") VALUES ('fr_FR', 'Français');
INSERT INTO "TUTORIEL_LOCALES" ("CODE", "NOM") VALUES ('en_EN', 'English');
INSERT INTO "TUTORIEL_LOCALES" ("CODE", "NOM") VALUES ('es_ES', 'Español');

INSERT INTO "TUTORIEL_PRODUITS" ("ID", "PRIX") VALUES (SQ_TUTORIEL_PRODUITS_ID.NEXTVAL, '1,00');
INSERT INTO "TUTORIEL_PRODUITS" ("ID", "PRIX") VALUES (SQ_TUTORIEL_PRODUITS_ID.NEXTVAL, '0,05');
INSERT INTO "TUTORIEL_PRODUITS" ("ID", "PRIX") VALUES (SQ_TUTORIEL_PRODUITS_ID.NEXTVAL, '0,50');

INSERT INTO "TUTORIEL__PRODUITS" ("ID", "LOCALE", "NOM") VALUES (1000, 'fr_FR', 'Banane');
INSERT INTO "TUTORIEL__PRODUITS" ("ID", "LOCALE", "NOM") VALUES (1001, 'es_ES', 'Aceituna');
INSERT INTO "TUTORIEL__PRODUITS" ("ID", "LOCALE", "NOM") VALUES (1001, 'fr_FR', 'Olive');
INSERT INTO "TUTORIEL__PRODUITS" ("ID", "LOCALE", "NOM") VALUES (1002, 'en_EN', 'Pepper');
INSERT INTO "TUTORIEL__PRODUITS" ("ID", "LOCALE", "NOM") VALUES (1002, 'fr_FR', 'Poivre');
Requête type (testée sur Oracle 10g) :
Code:
SELECT P.ID, P.PRIX, (MAX (PL.NOM) KEEP (
					DENSE_RANK FIRST 
					ORDER BY CASE PL.LOCALE
						WHEN 'es_ES' THEN 1
						WHEN 'en_EN' THEN 2
						WHEN 'fr_FR' THEN 3
					END)
			) NOM
FROM "TUTORIEL_PRODUITS" P
INNER JOIN "TUTORIEL__PRODUITS" PL ON P.ID = PL.ID
GROUP BY P.ID, P.PRIX;

Les deux cas devraient renvoyer :

Code:
ID                        PRIX                NOM                              
--------------------- ------------------ --------------------
1000                   1                      Banane                           
1001                   0,05                 Aceituna                         
1002                   0,5                   Pepper

Ce tutoriel est destiné à donner une idée générale au travers d'un exemple basique, il est de votre responsabilité de modifier, d'indexer, de contraindre et de sécuriser votre version en fonction de vos besoins. :oops:

Si une erreur surgissait, ou qu'une meilleure solution vous sautait aux yeux, n'hésitez pas à l'indiquer. Ce tutoriel signe l'arrêt d'une trêve de deux ans entre MySQL et moi, il est donc probable qu'une solution plus économique existe (hors procédure).
 

Tifox

ou pas
Un bon petit tutoriel. Personnellement, je n'utilise cette méthode que quand le nombre de langues de mon application devra (souvent) évoluer, car ça a le désavantage de doubler toute les tables (voir même de les tripler ou quadrupler parfois), et ça peut très vite devenir le bordel et compliqué a maintenir. Dans le cas ou le nombre de langues est fixé a l'avance et non-évolutif, je préfère coder la langue directement dans l'objet. C'est moins propre, mais plus facile a maintenir.
 

Ahava

Revenant
Je pensais qu'un fichier xml par langue, et importation du bon fichier selon le choix de l'utilisateur est la meilleure manière de faire, non ?
 
1er
OP
O

ozilrit

Elite
Merci Tifox.

Il s'agit ici d'une solution, non de LA solution, tout dépends des besoins, des possibilités et des préférences de chacun. :)

J'utilise la version Oracle (quelque peu plus complexe et optimisée) sur plus d'un million de lignes sans le moindre problème et avec une gestion beaucoup plus aisée qu'un fichier pour gettext ou un fichier xml.
Lors d'un essai de conception du schéma, j'ai tenté de placer les données de la bdd dans des fichiers XML, mais le gain était infime.

Sinon Ahava, comment fais-tu si, par hasard ou par volonté, une traduction manquait ?
 

Tifox

ou pas
Personnellement, j'utilise les traduction dans des fichiers pour tout ce qui est texte "statique" dans la page (genre les titres, ...) et dans la DB (soit d'une manière proche de celle de ozilrit soit comme j'ai expliqué en fonction des besoins) pour tout ce qui est objet dynamique chargé dans la DB.
 

Ahava

Revenant
ozilrit a dit:
Sinon Ahava, comment fais-tu si, par hasard ou par volonté, une traduction manquait ?
Bah s'il manque un fichier c'est que qqun a eu acces au serveur ce qui ne devrait jamais arriver... Puis, y a une langue de base, l'anglais, quoiqu'il arrive, mais aussi dans un fichier...
 
1er
OP
O

ozilrit

Elite
Je ne parle pas de l'absence d'un fichier mais de l'absence d'un simple et unique élément de traduction. Imaginons que ta langue de référence soit l'anglais, tu disposes d'un fichier contenant : "Apple"; "Pear"; complet. Mais également d'un fichier en français : "Pomme"; sans traduction pour "Pear", qu'advient-il alors ?


Tifox, j'imagine donc que tu codes un champ par caractéristique par langue, puis que tu sélectionnes les bons champs en fonction de la langue ?
Code:
_Produits : Id, Prix, nom_fr, nom_en, description_fr, description_en
Pourquoi pas, tant qu'il n'est pas nécessaire d'ajouter ou supprimer des langues. :)
 

Tifox

ou pas
ozilrit a dit:
Tifox, j'imagine donc que tu codes un champ par caractéristique par langue, puis que tu sélectionnes les bons champs en fonction de la langue ?
Code:
_Produits : Id, Prix, nom_fr, nom_en, description_fr, description_en
Pourquoi pas, tant qu'il n'est pas nécessaire d'ajouter ou supprimer des langues. :)
C'est ça, à condition effectivement que le nombre de langue soit fixé une fois pour toute au début du développement.
 
P

Pum

ex membre
Pas mal le petit tuto... Par contre, dans le cas de la programmation de JSP, j'utilise une toute autre méthode : les bundles. Pour plus d'infos, c'est ici . C'est vraiment très propre comme programmation et très rapide (on accède à un fichier properties et plus de connexion vers une BD);) L'idée est fort proche de l'utilisation du fichier XML mais en plus simple (accès aux données moins complexes que l'accès aux données d'un fichier XML) Maintenant l'utilisation d'un fichier XML par langue est souvent utilisée et peut montrer ses avantages aussi. Enfin, ce n'est que mon avis... Voilà, @+
 
1er
OP
O

ozilrit

Elite
L'équivalent Java du Gettext (de PHP) que nous évoquions ci-dessus. :)

Merci de ta contribution. ;)
 
Statut
N'est pas ouverte pour d'autres réponses.
Haut