Bonjour à tous,

Cherchant une solution simple d'internationalisation de textes, j'ai fait le tour du web sans vraiment être emballé.

Il faut dire aussi que j'étais exigeant :
- pas de fichiers texte à rallonge
- système évolutif et très modulaire
- traductions statiques et paramétrables
- pas de parseur autre que celui du PHP
- support de l'UTF-8
- intégration dans une IDE afin de profiter de l'autocomplétion

Bref un truc qui ressemble au nirvana !

Finalement, étant partisan du yaka se servir soi-même (vu que l'on est jamais mieux servi que par soi-même...) je me suis attelé à la tâche et j'ai créé un système qui répond à tous les besoins cités ci-dessus.

Allez c'est parti :-)


MINIMUM REQUIS
La souplesse du système de traduction repose sur les dernières fonctionnalités du PHP 5.3+ : Late Static Binding et __callStatic.
Donc si vous ne développez pas au minimum sous la version 5.3 de PHP, ce qui suit ne fonctionnera pas.


IMPORTANT
Afin d'illustrer la logique, les explications seront accompagnées d'un exemple réel provenant de mon framework d'entreprise.
J'ai choisi le module fournissant les messages d'erreurs au moteur d'exécution des requêtes de la base de données.
La classe d'exemple s'appelle : i18nData (dans le fichier i18nData.php)


PRINCIPE
Le concept : avoir des classes qui se chargent de la traduction à partir d'une classe pilote qui dérive de la classe abstraite i18n (classe en charge du mécanisme de traduction à la volée).
Le développeur ne manipule que la classe pilote.

Voici comment s'organisent les classes (et accessoirement les fichiers) :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
abstract class i18n                 # Mécanisme de traduction à la volée
 
class i18nData extends i18n         # Classe pilote contenant une liste de messages d'erreur
class i18nData_fr                   # Classe de traduction contenant la version française des messages d'erreur
class i18nData_en                   # Classe de traduction contenant la version anglaise des messages d'erreur
class i18nData_de                   # Classe de traduction contenant la version allemande des messages d'erreur
 
class i18nValidating extends i18n   # Classe pilote contenant une liste de messages d'erreur (relatifs à la validation des données de l'utilisateur)
class i18nValidating_fr             # Classe de traduction contenant la version française des messages d'erreur
class i18nValidating_en             # Classe de traduction contenant la version anglaise des messages d'erreur
class i18nValidating_de             # Classe de traduction contenant la version allemande des messages d'erreur
La système de traduction repose sur 3 constantes globales :
- GCT_LANG__USER : langue du client (lecture du header du browser) ou langue explicitement demandée par le client
- GCT_LANG__DEFAULT : langue par défaut du système d'affichage
- GCT_LANG__TRANSLATION_MISSING : si pour n'importe quelle raison, la traduction n'a pas pu être faite, la routine renverra au final le contenu de cette constante


REGLES DE CODAGE
- Les messages simple utilisent des constantes de classe
- Les messages paramétrés utilisent des fonctions statiques
- Toutes les classes (pilote et traduction) doivent obligatoirement définir cette constante :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
const __SELF__ = __CLASS__;
- Les classes pilotes doivent toutes dériver de la classe i18n
- Les classes pilotes peuvent être dérivées les unes des autres
- Les classes pilotes et leurs classes de traduction doivent partager le même namespace
- Une constante ou une fonction statique dans une classe pilote NE DOIT SURTOUT PAS ETRE REDEFINIE dans une classe dérivée
-> En clair : une fois une traduction faite, on ne doit pas y revenir dessus
- Le nommage des classes de traduction est strict : ClassePilote_Langue (voir l'exemple d'organisation des fichiers)
- Afin d'éviter tout impair, les classes de traduction doivent être finales (non dérivables)
- Les classes de traduction peuvent être dérivées de la classe pilote mais cela n'est pas nécessaire
Ex :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
class i18nData_fr extends i18nData { }
class i18nData_fr { } # fonctionne aussi
- Pour les messages paramétrés le traitement de la logique linguistique est déporté dans les classes de traduction :
-> En conséquence, le code dans la classe pilote doit se résumer à ceci :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
static function myParamMsg($paramA, $paramB, $lang = GCT_LANG__USER) {
   return parent::translate(__FUNCTION__, array($paramA, $paramB), $lang);
}
Dans les classes de traduction le paramètre $lang n'a pas lieu d'être
Ex: dans un fichier de traduction quelconque ce que l'on pourrait trouver :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
static function myParamMsg($paramA, $paramB) {
   return "$paramA est invalide car le maximum $paramB est dépassé";
}
- Les traductions multi-lignes doivent être ramenée sur une seule ligne


FONCTIONNEMENT
- Tout d'abord, il faut que le traitement normal d'une requête définisse les constantes globales :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
- GCT_LANG__USER
- GCT_LANG__DEFAULT
- GCT_LANG__TRANSLATION_MISSING
- Ne pas oublier que l'on appelle les message à partir des classes pilotes et non des classes de traduction.

- Pour traduire un message simple (défini par une constante de classe)
- il suffit de transformer la constante en fonction par l'adjonction de parenthèses ()
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
Ex: constante  => i18nData::DATA_NOT_FOUND
Ex: traduction => i18nData::DATA_NOT_FOUND()
- Il est tout à fait possible de forcer la traduction d'un message simple dans une langue donnée en passant l'identifiant de la langue désirée en paramètre :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
Ex: i18nData::DATA_NOT_FOUND('fr')
    i18nData::INTEGRITY_VIOLATION('en')
- Si aucun paramètre n'est passé, le système se réfère dans l'ordre aux constantes GCT_LANG__USER et GCT_LANG__DEFAULT

- Pour traduire un message paramétré, il suffit d'appeler la fonction statique correspondante en n'oubliant pas de lui passer les paramètres requis (9 maximum)


INTERCEPTION DES ERREURS :
- Si pour n'importe quelle raison (fichier de traduction manquant, fonction ou constante manquante...), la traduction repart sur le langage par défaut (GCT_LANG__DEFAULT)
- Et si rien de tout cela ne fonctionne, la traduction renverra le contenu de la constante globale GCT_LANG__TRANSLATION_MISSING


CODE SOURCE DE LA CLASSE i18n
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
 
/**
 * Copyright (C) 2011+ Martin Lacroix
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * @license GNU General Public License Version 3 (GPLv3)
 * @link http://www.gnu.org/licenses/
 */
 
/**
 * PHP_VER   : 5.3+ 
 * LIBRARIES : 
 * KEYWORDS  : INTERNATIONALIZATION I18N TRANSLATION
 *             INTERNATIONALISATION I18N TRADUCTION
 * 
 * Class helping for internationalization (i18n) of texts
 * 
 * Classe fournissant un mécanisme d'internationalisation des textes
 * 
 * @package tools
 * @version 1.0.0
 * @author Martin Lacroix
 */
abstract class i18n {
 
   const __SELF__ = __CLASS__;
 
   /**
    * Renvoie la valeur de la constante traduite
    * Il est possible de forcer la traduction dans une langue en passant son identifiant un argument
    *    Ex: 'fr' 'en' 'de'
    * @param string $name
    * @param array $args
    * @return string|NULL
    */
   static function __callStatic($name, $args = array()) {
      # on récupère la classe propriétaire de la constante (pilotClass)
      $caller  = static::__SELF__;
      $parents = self::treeClassParents($caller);
      $owner   = $caller; # par défaut
 
      if (count($parents)) {
         # on parcourt les parents à partir de la racine des dérivées et on évalue la constante
         foreach($parents as $parent) {
            $msg = @constant($parent . '::' . $name);
            if (NULL !== $msg) {
               $owner = $parent;
               break;
            }
         }
      }
 
      if (NULL === $owner) {
         return GCT_LANG__TRANSLATION_MISSING;
      }
 
      # langue de traduction
      $lang = (empty($args)) ? GCT_LANG__USER : $args[0];
 
      if (NULL === $lang) {
         if (NULL === GCT_LANG__DEFAULT) {
            return GCT_LANG__TRANSLATION_MISSING;
         }
         $lang = GCT_LANG__DEFAULT;
      }
 
      $msg = @constant($owner . '_' . $lang . '::' . $name);
 
      # si pas de traduction trouvée, récupère la traduction avec le langage par défaut
      if ((NULL === $msg) && ($lang !== GCT_LANG__DEFAULT)) {
         $msg = @constant($owner . '_' . GCT_LANG__DEFAULT . '::' . $name);
      }
 
      return (NULL === $msg) ? GCT_LANG__TRANSLATION_MISSING : $msg;
   }
 
   /**
    * Renvoie l'arborescence des classes parents
    * @param object $p
    * @param  bool $reflectionInsteadOfNames renvoyer les instances ReflectionClass à la place du nom de la classe
    * @return array Array([] => parentClassName)
    */
   static private function treeClassParents($p, $reflectionInsteadOfNames = FALSE) {
      # vu que les redéfinitions des constantes ou des fonctions statiques sont interdites
      # on renverse le tableau des parents de manière à le parcourir dans le sens racine->enfant->enfant...
      # afin d'isoler la classe qui définit physiquement la constante ou la fonction statique 
      $parents = array();
      $rc = new \ReflectionClass($p);
      while($rc = $rc->getParentClass()) {
         $parents[] = ($reflectionInsteadOfNames) ? $rc : $rc->getName();
      }
      # on écrête le tableau de la classe racine i18n
      array_pop($parents);
      return array_reverse($parents);
   }
 
   /**
    * Détermine la classe qui doit exécuter la fonction en fonction de la langue
    * Langue définie par GCT_LANG__USER ou GCT_LANG__DEFAULT
    * @param string $funcName
    * @param array $params Liste ordonnée de paramètres (9 max) Array([] => param)
    * @param string $lang
    * @return string|NULL
    */
   static protected function translate($funcName, array $params = array(), $lang = GCT_LANG__USER) {      
      # on récupère la classe propriétaire de la fonction statique
      $caller  = static::__SELF__;
      $parents = self::treeClassParents($caller, TRUE);
      $owner   = $caller; # par défaut
 
      if (count($parents)) {
         # on parcourt les parents à partir de la racine des dérivées 
         # et on détermine si la fonction appelée est présente
         foreach($parents as $parent) {
            if ($parent->hasMethod($funcName)) {
               $owner = $parent->getName();
               break;
            }
         }
      }
 
      if (NULL === $owner) {
         return GCT_LANG__TRANSLATION_MISSING;
      }
 
      # langue de traduction      
      if (NULL === $lang) {
         if (NULL === GCT_LANG__DEFAULT) {
            return GCT_LANG__TRANSLATION_MISSING;
         }
         $lang = GCT_LANG__DEFAULT;
      }
 
      $msg = self::callFunc($owner . '_' . $lang . '::' . $funcName, $params);
 
      # si pas de message en retour avec la langue courante, on réessaye avec la langue par défaut
      if ((NULL === $msg) && ($lang !== GCT_LANG__DEFAULT)) {
         $msg  = self::callFunc($owner . '_' . GCT_LANG__DEFAULT . '::' . $funcName, $params);
      }
 
      return (NULL === $msg) ? GCT_LANG__TRANSLATION_MISSING : $msg;
   }
 
   /**
    * Lance l'éxecution d'une fonction de traduction paramétrable (9 paramètres maximum)
    * @param string $func
    * @param array $params
    * @return string|NULL
    */
   static private function callFunc($func, array $params = array()) {
      $nb = count($params);
 
      if ($nb === 0) { 
         return @call_user_func($func); 
      }
 
      if ($nb === 1) { 
         return @call_user_func(
            $func, 
            $params[0]
         );
      }
 
      if ($nb === 2) { 
         return @call_user_func(
            $func, 
            $params[0], $params[1]
         );
      }
 
      if ($nb === 3) { 
         return @call_user_func(
            $func, 
            $params[0], $params[1], $params[2]
         );
      }
 
      if ($nb === 4) { 
         return @call_user_func(
            $func, 
            $params[0], $params[1], $params[2], $params[3]
         );
      }
 
      if ($nb === 5) { 
         return @call_user_func(
            $func, $params[0], $params[1], $params[2], $params[3], $params[4]
         );
      }
 
      if ($nb === 6) { 
         return @call_user_func(
            $func, $params[0], $params[1], $params[2], $params[3], $params[4], $params[5]
         );
      }
 
      if ($nb === 7) { 
         return @call_user_func(
            $func, 
            $params[0], $params[1], $params[2], $params[3], $params[4], $params[5], $params[6]
         ); 
      }
 
      if ($nb === 8) { 
         return @call_user_func(
            $func, 
            $params[0], $params[1], $params[2], $params[3], $params[4], $params[5], $params[6], $params[7]
         );
      }
 
      if ($nb === 9) { 
         return @call_user_func(
            $func, 
            $params[0], $params[1], $params[2], $params[3], $params[4], $params[5], $params[6], $params[7], $params[8]
         );
      }
   }
}

EXEMPLE SIMPLE
Classe pilote : i18nData
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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
<?php
class i18nData extends i18n {
 
   const __SELF__ = __CLASS__;
 
   const UNAVAILABLE_CONNEXION               = 0;
   const UNABLE_TO_GET_A_STATEMENT           = 1;
   const UNABLE_TO_LINK_A_VALUE_TO_A_TAG     = 2;
   const EXECUTION_ERROR                     = 3;
   const INTEGRITY_VIOLATION                 = 4;
   const UNABLE_TO_EXTRACT_THE_DATA          = 5;
   const DATA_NOT_FOUND                      = 6;
   const EMPTY_RECORD                        = 7;
   const UNABLE_TO_RETRIEVE_THE_NEW_ID       = 8;
   const UNABLE_TO_BUILD_THE_SELECT_CLAUSE   = 9;
   const UNABLE_TO_BUILD_THE_FROM_CLAUSE     = 10;
   const UNABLE_TO_BUILD_THE_ORDER_BY_CLAUSE = 11;
   const UNABLE_TO_BUILD_THE_GROUP_BY_CLAUSE = 12;
   const UNABLE_TO_BUILD_THE_WHERE_CLAUSE    = 13;
   const UNABLE_TO_BUILD_THE_HAVING_CLAUSE   = 14;
 
   static function unableToLinkAValueToTheTag($tag, $lang = GCT_LANG__USER) {
      return parent::translate(__FUNCTION__, array($tag), $lang);
   }
}
?>
Classe de traduction française :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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
<?php
final class i18nData_fr {
 
   const __SELF__ = __CLASS__;
 
   const UNAVAILABLE_CONNEXION               = 'connexion indisponible';
   const UNABLE_TO_GET_A_STATEMENT           = 'impossible de récupérer un statement';
   const UNABLE_TO_LINK_A_VALUE_TO_A_TAG     = 'impossible de rattacher une valeur à son tag';
   const EXECUTION_ERROR                     = "erreur à l'exécution";
   const INTEGRITY_VIOLATION                 = "erreur d'intégrité référentielle";
   const UNABLE_TO_EXTRACT_THE_DATA          = "impossible d'extraire les données";
   const DATA_NOT_FOUND                      = 'données non trouvées';
   const EMPTY_RECORD                        = 'enregistrement vide';
   const UNABLE_TO_RETRIEVE_THE_NEW_ID       = 'impossible de récupérer le nouvel id';
   const UNABLE_TO_BUILD_THE_SELECT_CLAUSE   = 'impossible de générer la clause select';
   const UNABLE_TO_BUILD_THE_FROM_CLAUSE     = 'impossible de générer la clause from';
   const UNABLE_TO_BUILD_THE_ORDER_BY_CLAUSE = 'impossible de générer la clause order by';
   const UNABLE_TO_BUILD_THE_GROUP_BY_CLAUSE = 'impossible de générer la clause group by';
   const UNABLE_TO_BUILD_THE_WHERE_CLAUSE    = 'impossible de générer la clause where';
   const UNABLE_TO_BUILD_THE_HAVING_CLAUSE   = 'impossible de générer la clause having';
 
   static function unableToLinkAValueToTheTag($tag) {
      return 'impossible de rattacher une valeur au tag : ' . $tag;
   }
}
?>
Classe de traduction anglaise :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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
<?php
final class i18nData_en {
 
   const __SELF__ = __CLASS__;
 
   const UNAVAILABLE_CONNEXION               = 'unavailable connexion';
   const UNABLE_TO_GET_A_STATEMENT           = 'unable to get a statement';
   const UNABLE_TO_LINK_A_VALUE_TO_A_TAG     = 'unable to link a value to a tag';
   const EXECUTION_ERROR                     = 'execution error';
   const INTEGRITY_VIOLATION                 = 'integrity violation';
   const UNABLE_TO_EXTRACT_THE_DATA          = 'unable to extract the data';
   const DATA_NOT_FOUND                      = 'data not found';
   const EMPTY_RECORD                        = 'empty record';
   const UNABLE_TO_RETRIEVE_THE_NEW_ID       = 'unable to retrieve the new id';
   const UNABLE_TO_BUILD_THE_SELECT_CLAUSE   = 'unable to build the select clause';
   const UNABLE_TO_BUILD_THE_FROM_CLAUSE     = 'unable to build the from clause';
   const UNABLE_TO_BUILD_THE_ORDER_BY_CLAUSE = 'unable to build the order by clause';
   const UNABLE_TO_BUILD_THE_GROUP_BY_CLAUSE = 'unable to build the group clause';
   const UNABLE_TO_BUILD_THE_WHERE_CLAUSE    = 'unable to build the where clause';
   const UNABLE_TO_BUILD_THE_HAVING_CLAUSE   = 'unable to build the having clause';
 
   static function unableToLinkAValueToTheTag($tag) {
      return 'unable to link a value to the tag: ' . $tag;
   }
}
?>
Utilisation :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
 
define('GCT_LANG__USER', 'fr');
define('GCT_LANG__DEFAULT', 'en');
define('GCT_LANG__TRANSLATION_MISSING', 'notTranslated');
 
echo i18nData::UNABLE_TO_BUILD_THE_FROM_CLAUSE(), '<br />';
echo i18nData::UNABLE_TO_BUILD_THE_FROM_CLAUSE('en'), '<br />';
echo i18nData::UNABLE_TO_BUILD_THE_FROM_CLAUSE('de'), '<br />';
echo i18nData::unableToLinkAValueToTheTag('monTag'), '<br />';
echo i18nData::unableToLinkAValueToTheTag('monTag', 'en'), '<br />';
echo i18nData::unableToLinkAValueToTheTag('monTag', 'de'), '<br />';
?>

EXEMPLE AVANCE MONTRANT UNE ARBORESCENCE DE CLASSES PILOTES
On suppose qu'on a défini une classe de base à tous les formulaires regroupant quelques contrôles très communs et que l'on doive maintenant développer un formulaire plus complexe bâti sur les fondations du formulaire de base.
Voici comment internationaliser tout ça :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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
54
55
56
57
58
59
60
61
62
<?php
# Classe pilote du formulaire de base
class i18nFormBase extends i18n {
 
   const __SELF__ = __CLASS__;
 
   const CREATE = 1;
   const UPDATE = 2;
   const DELETE = 3;
   const SELECT = 4;
}
 
# Une classe de traduction du formulaire de base
final class i18nFormBase_fr {
 
   const __SELF__ = __CLASS__;
 
   const CREATE = 'créer';
   const UPDATE = 'modifier';
   const DELETE = 'supprimer';
   const SELECT = 'sélectionner';
}
 
# Une classe de traduction du formulaire de base
final class i18nFormBase_en {
 
   const __SELF__ = __CLASS__;
 
   const CREATE = 'create';
   const UPDATE = 'update';
   const DELETE = 'delete';
   const SELECT = 'select';
}
 
 
# Classe pilote du formulaire complexe bâti sur le formulaire de base
class i18nFormMail extends i18nFormBase {
 
   const __SELF__ = __CLASS__;
   # comme vous pouvez le constater, on  n'ajoute que ce qui est nécessaire
   const SEND = 1;
   const SPAM = 2;
}
 
# Une classe de traduction du formulaire complexe
class i18nFormMail_fr {
 
   const __SELF__ = __CLASS__;
   # comme vous pouvez le constater, on  ne traduit que ce qui est nécessaire
   const SEND = 'expédier';
   const SPAM = 'courrier indésirable';
}
 
# Une classe de traduction du formulaire complexe
class i18nFormMail_en {
 
   const __SELF__ = __CLASS__;
   # comme vous pouvez le constater, on  ne traduit que ce qui est nécessaire
   const SEND = 'send';
   const SPAM = 'spam';
}
?>
Et maintenant le code de test :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
<?php
define('GCT_LANG__USER', 'fr');
define('GCT_LANG__DEFAULT', 'en');
define('GCT_LANG__TRANSLATION_MISSING', 'notTranslated');
 
echo i18nFormBase::CREATE(), '<br />';
echo i18nFormMail::CREATE(), '<br />';
echo i18nFormMail::SEND(), '<br />';
?>
Si vous utilisez un EDI moderne, vous pourrez constater que l'autocomplétion fonctionne sans problèmes particuliers et vous récupérez les bonnes données, ouf

Dans tous les cas, si vous avez des questions pour l'implémentation, n'hésitez pas. Et si malgré tout mon attention, une banane s'était glissée dans le code, je suis preneur de toute remarque et commentaire qui va bien. Merci et j'espère que cela vous servira un jour.