Introduction au système 'gettext' de GNU pour la production de programmes polyglottes.
Cet article est une adaptation de l'article de Pancrazio de Mauro (pdemauro@datanord.it), et a été publié dans le Linux Journal de Mars 1999.
Linux devient plus populaire de jour en jour. Jusqu'alors, l'utilisateur typique de Linux était un administrateur système, un étudiant ou un bidouilleur UNIX. Des projets comme GNOME, KDE et GNUStep préparent la route pour des utilisateurs différents, moins préparés techniquement.
Exécuter un logiciel s'exprimant en anglais n'est généralement pas un problème pour quelqu'un ayant un niveau moyen en Informatique, mais les utilisateurs de base ont besoin (et veulent) des logiciels s'exprimant dans leur langue afin d'être productifs et à l'aise avec le système. De plus, de nombreux programmes ont besoin de connaître les conventions locales pour des choses comme les dates ou les formats monétaires afin d'être utilisables et complets.
Cet article est une introduction au système 'gettext' de GNU, un ensemble d'outils et de bibliothèques pour les programmeurs et les traducteurs leur permettant de produire des programmes polyglottes avec des messages exprimés dans les langages spécifiés. Nous traiterons des langages qui utilisent l'un des ensembles de caractères ISO-8859-X, pas du japonais ou du Chinois qui nécessitent un traitement particulier.
Définitions
Deux mots apparaissent fréquemment lorsque l'on parle de la gestion des différentes langues dans les programmes : internationalisation et localisation. L'écriture répétée de ces deux mots (sans typos) étant pénible et longue, on préfère les abréger respectivement en I18N et L10N (le 18 et le 10 indiquent le nombre de caractères se trouvant entre la première et la dernière lettre de chaque mot).
Internationaliser un programme, c'est suivre les différentes étapes qui lui permettront de tenir compte des différentes langues et standards nationaux.
La localisation d'un programme a lieu lorsque l'on donne à un programme internationalisé les informations dont il a besoin pour se comporter correctement avec une certaine langue et un ensemble d'habitudes culturelles.
Commençons par le début
La première chose à faire, pour le programmeur comme pour l'utilisateur de base, est de configurer sa machine Linux pour qu'elle utilise les locales. La plupart des utilisateurs n'auront qu'à suivre ce qui est écrit dans le Locales-mini-HOWTO, disponible sur http://www.freenix.fr/linux. Les distributions récentes contiennent tout ce qu'il faut pour gérer les locales.
Lorsque le système peut gérer les locales, vous pouvez spécifier les langues et les standards particuliers que vous souhaitez utiliser. Ceci est réalisé via un ensemble de variables d'environnement, chacune contrôlant un aspect spécifique du système des locales :
- LANG indique la locale au niveau global, elle peut être surchargée par les variables suivantes ;
- LC_COLLATE indique la locale utilisée pour les tris et les comparaisons ;
- LC_CTYPE indique l'ensemble de caractères à utiliser, pour que isupper('À') renvoie vrai dans la locale française, par exemple ;
- LC_MONETARY donne des informations sur la représentation des sommes monétaires dans une locale spécifique ;
- LC_NUMERIC donne des informations sur les nombres : la façon dont les chiffres sont séparés et divisés en groupes, comment est représenté le point décimal, etc. ;
- LC_TIME indique la locale à utiliser pour représenter l'heure : valeurs 12 heures AM/PM ou sur 24 heures, par exemple ;
- LC_MESSAGES indique la langue choisie pour les messages texte des programmes ;
- LC_ALL surcharge toutes les précédentes et configure une locale au niveau global.
Des exemples de valeurs pour une locale globale sont :
- en_US pour l'Anglais des États-Unis ;
- it_IT pour l'Italien en Italie ;
- fr_CA pour le Français au Canada ;
- fr_FR pour le Français en France.
En résumé, pour utiliser les standards de la langue LL dans le pays PP, la valeur de locale est LL_PP.
La locale utilisée par défaut, à moins qu'elle ne soit surchargée par les variables citées plus haut, s'appelle la locale C (ou POSIX). Il est donc très facile d'illustrer le comportement d'un programme tenant compte des locales en utilisant 'date' :
$ echo $LC_ALL
$ date
Sun Feb 28 20:50:02 CET 1999
$ cat /foo/bar
cat: /foo/bar: No such file or directory
$ export LC_ALL="fr_FR"
$ date
dim fév 28 20:51:15 CET 1999
$ cat /foo/bar
cat: /foo/bar: Aucun fichier ou répertoire de ce type.
$ export LC_ALL="it_IT"
$ date
dom feb 28 20:53:14 CET 1999
$ cat /foo/bar
cat: /foo/bar: No such file or directory
Si la variable LC_ALL n'est pas positionnée, la réponse est en anglais. Puis, on configure la locale française pour obtenir des réponses en français. Enfin, on fait de même avec la locale italienne. On notera que le message de 'cat' reste malgré tout en anglais : c'est parce qu'aucune information italienne n'est disponible.
Gestion des messages dans les programmes C
Examinons un peu le paquetage gettext de GNU. Si vous ne l'avez pas encore installé sur votre système, vous pouvez le télécharger à partir de ftp://prep.ai.mit.edu/pub/gnu/ ou l'un de ses miroirs.
Lorsque l'on écrit des programmes polyglottes avec ce paquetage, les chaînes de caractères sont « enveloppées » dans une fonction au lieu d'être directement codées dans le source. La fonction s'appelle 'gettext', ne prend qu'une seule chaîne comme paramètre et renvoie une chaîne.
En dépit de sa simplicité, 'gettext' est très efficace : la chaîne passée en argument est recherchée dans une table afin de trouver une traduction correspondante. Si une traduction est trouvée, gettext la renvoie ; sinon, la chaîne passée est renvoyée et le programme continuera à utiliser la langue par défaut.
Notre premier programme internationalisé 'Hello, world !' pourrait être écrit de la façon suivante :
#include <stdio.h>
#include <libintl.h>
int main() {
textdomain("hello-world");
printf(gettext("Hello, world !\n"));
return 0;
}
N'oubliez pas d'inclure libintl.h dans tout programme C utilisant le paquetage gettext.
La fonction textdomain doit être appelée avant d'utiliser gettext. Son but est de sélectionner la « base de données » de messages adéquate (un terme plus approprié serait « catalogue de messages ») pour que le programme l'utilise.
Puis, chaque chaîne traduisible doit être utilisée comme argument de gettext. Écrire gettext ("foobar") à chaque fois peut s'avérer pénible, c'est pourquoi de nombreux programmeurs utilisent cette macro :
#define _(x) gettext(x)
Ainsi, le surcoût introduit pas l'internationalisation des messages est relativement faible : au lieu d'écrire "foobar", on écrit simplement _("foobar"). Cela ne fait que trois caractères supplémentaires par chaîne traduisible, avec l'avantage que cette macro élimine complètement le code gettext du module.
Traduction des messages
Lorsqu'un programme a été internationalisé, le processus de localisation peut commencer. La première chose à faire consiste à extraire toutes les chaînes à traduire du code source.
Ce processus est automatisé par l'utilisation de xgettext, qui produit un fichier .po (pour 'portable object'). xgettext analyse les fichiers source qui lui sont passés en argument et extrait chaque chaîne traduisible marquée par le programmeur avec gettext ou un autre identificateur.
Dans notre cas, nous pourrions invoquer xgettext de cette façon :
xgettext -a -d hello-world -k_ -s hello.c
Le fichier résultant, hello-world.po, est :
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 1999-02-28 22:34+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Eric Jacoboni <jaco@linux-france.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: ENCODING\n"
#: hello.c:6
msgid "Hello, world !\n"
msgstr ""
Je vous conseille de jeter un coup d'oeil à la documentation (au format info) de gettext pour en connaître les autres options. Celles que j'ai utilisées sont :
-a extrait toutes les chaînes ;
-d produit le résultat dans hello-world.po (la valeur par défaut est messages.po) ;
-k informe xgettext de rechercher _ lorsqu'il cherche des chaînes à traduire (gettext et gettext_noop sont quand même recherchés) ;
-s génère une sortie triée et supprime les doublons.
Le traducteur peut alors se contenter de remplir hello-world.po avec les messages sans aucune connaissance du code source. En fait, un programme peut être internationalisé et compilé avant l'ajout de nouvelles langues.
Pour être utilisable, un objet portable doit être compilé pour donner un objet machine (un fichier .mo). Ceci est réalisé par la commande :
msgfmt -o hello-world.mo -v hello-word.po
La figure 1 représente toutes les étapes nécessaires à l'obtention d'un fichier .mo à partir d'un source C. La partie la plus critique est le lancement de tupdate (voir ci-dessous) pour ajouter les nouvelles chaînes, non traduites, à ce qui a déjà été fait, sans perdre ce travail.
L'étape finale consiste à copier hello-world.mo dans un emplacement adéquat, où il sera trouvé par le système gettext. Sur ma machine Linux, l'emplacement par défaut est /usr/local/share/locale/LL/LC_MESSAGES/ ou /usr/local/sha re /lo cale / LL_ PP/ LC_MESSAGES/ où LL est le langage et PP est le pays. Par exemple, la traduction française devrait être placée dans /usr/local/share/locale/fr/LC_MESSAGES/hello-world.mo.
textdomain doit être appelé au début du programme pour que le système puisse choisir le .mo adéquat en fonction des variables de la locale courante. Dans l'ordre d'importance (la plus importante d'abord), ce sont LC_ALL, LC_MESSAGES et LANG.
Un fichier .mo peut être partagé par plusieurs programmes si les programmeurs décident de pratiquer ainsi. C'est ce qui est fait avec le paquetage fileutils de GNU, par exemple.
Gestion du fichier des messages
Si le code source est modifié, le fichier .po correspondant doit être mis à jour sans perdre les traductions déjà effectuées. Malheureusement, un nouvel appel à xgettext ne convient pas car il écraserait le .po existant. Le programme tupdate vient à la rescousse : il fusionne deux fichiers .po en conservant les traductions déjà effectuées tant que les nouvelles chaînes correspondent aux anciennes. Sa syntaxe est simple :
tupdate nouveau.po ancien.po > courant.po
Évidemment, les nouvelles chaînes seront vides dans courant.po, mais celles déjà traduites y seront sans qu'il n'y ait besoin de les retravailler.
Exceptions
Il n'est pas toujours possible d'utiliser la fonction gettext « telle quelle ». Regardons l'extrait de code source suivant :
char *nom_items[] = {
"files",
"messages",
"tapes",
"penguins",
"floppies"
};
...
if ((index >= 0) && (index <= 4))
strcpy(items,nom_items[index]);
else
strcpy(items,"unknown items");
printf("You have %d %s\n", quantity, items);
L'internationalisation de ce code doit atteindre deux buts : toute chaîne à traduire doit apparaître dans le fichier .po ; avant d'afficher une chaîne lors de l'exécution, on doit la passer par gettext.
La chaîne "You have %d %s\n" pose problème. Nous ne pouvons pas simplement transformer tous les chaînes déclarées dans nom_items par des appels à gettext car les tableaux doivent être initialisés avec des constantes. Une solution est donnée ci-dessous. gettext_noop est un marqueur utilisé pour rendre la chaîne reconnaissable par xgettext (c'est pour cela qu'il est recherché par défaut). La traduction se fait au moment de l'exécution avec un appel normal à gettext.
#define gettext_noop(x) (x)
char *nom_items[] = {
gettext_noop("files"),
gettext_noop("messages"),
gettext_noop("tapes"),
gettext_noop("penguins"),
gettext_noop("floppies")
};
...
if ((index >= 0) && (index <= 4))
strcpy(items, gettext(nom_items[index]));
else
strcpy(items, gettext("unknown items"));
printf(gettext("You have %d %s\n"), quantity, items);
Format du fichier des messages
Les fichiers .po ont une structure très simple et peuvent être modifiés à partir de n'importe quel éditeur de texte. Emacs, entre autres, dispose d'un mode po pour faciliter leur édition.
Tout fichier de messages consiste en une séquence d'enregistrements. Chacun d'eux a cette structure :
(lignes blanches)
# commentaires personnels éventuels
#. commentaires automatiques éventuels
#: références éventuelles au code source
msgid chaine-originale
msgstr chaine-traduite
Les commentaires introduits par le traducteur doivent avoir une espace placée immédiatement après le # (dièse). Les commentaires automatiques sont produits par xgettext et tupdate pour améliorer la lisibilité du fichier et pour permettre au traducteur de parcourir rapidement le code source afin de trouver une ligne utilisant une chaîne précise. Ils sont parfois nécessaires pour effectuer une traduction correcte.
Les chaînes sont formatées comme en C. Il est par exemple tout à fait possible d'écrire :
msgid ""
"Hello\t"
"World!\n"
msgstr ""
"Salut\t"
"tout le monde !\n"
Comme vous pouvez le constater, les chaînes peuvent être réparties sur plusieurs lignes et l'antislash est utilisé pour introduire des caractères spéciaux comme la tabulation et le retour à la ligne.
Autres systèmes de catalogues de messages
Il n'existe aucun standard POSIX pour les catalogues de messages -- le comité ne peut arriver à trouver un accord.
gettext de GNU n'est pas le seul système de catalogues de messages utilisable pour l'internationalisation des programmes. Une autre bibliothèque, basée sur un appel à la fonction catgets, existe aussi. L'interface catgets est supportée par le consortium X/Open, tandis que l'interface gettext a d'abord été utilisée par Sun.
Le principal désavantage de catgets est qu'il faut choisir un identificateur unique pour chaque message et que cet identificateur doit être passé à chaque fois à catgets. Ceci rend la gestion d'un grand ensemble de message, où des entrées sont ajoutées et ôtées régulièrement, plus difficile. GNU gettext peut utiliser catgets comme interface sous-jacente sur les systèmes qui utilisent cette dernière.
Linux supporte les interfaces gettext et catgets. Mon opinion personnelle est que le système gettext est bien plus facile à utiliser par les programmeurs comme par les traducteurs.
Eric Jacoboni