[Tutoriel] Un site plurilingue.

Discussion dans 'Web, design' créé par ozilrit, 7 Octobre 2007.

Statut de la discussion:
Fermée.
  1. Offline
    ozilrit Touriste
    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).
    ozilrit, 7 Octobre 2007
    #1
  2. Online
    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.
    Tifox, 7 Octobre 2007
    #2
  3. Offline
    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 ?
    Ahava, 7 Octobre 2007
    #3
  4. Offline
    ozilrit Touriste
    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 ?
    ozilrit, 7 Octobre 2007
    #4
  5. Online
    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.
    Tifox, 7 Octobre 2007
    #5
  6. Offline
    Ahava Revenant
    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...
    Ahava, 7 Octobre 2007
    #6
  7. Offline
    ozilrit Touriste
    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. :)
    ozilrit, 7 Octobre 2007
    #7
  8. Online
    Tifox ou pas
    C'est ça, à condition effectivement que le nombre de langue soit fixé une fois pour toute au début du développement.
    Tifox, 8 Octobre 2007
    #8
  9. Offline
    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à, @+
    Pum, 13 Octobre 2007
    #9
  10. Offline
    ozilrit Touriste
    L'équivalent Java du Gettext (de PHP) que nous évoquions ci-dessus. :)

    Merci de ta contribution. ;)
    ozilrit, 13 Octobre 2007
    #10
Statut de la discussion:
Fermée.