Blog Jaune

Internationaliser une application en C


Internationaliser son application consiste à la préparer à être distribuée dans différentes langues. Lorsque l'on développe, c'est toujours utile et pratique de prévoir à long terme. Ce tutorial est destiné à être utilisé dans un programme en C, mais la partie théorique s'applique aux autres langages, et très peux de différences existent entre les différents langages, seul l'initialisation dans le programme changera.

Ce tutorial est à destination des utilisateurs de systèmes de type unix. N'ayant pas de windows à disposition et n'ayant aucune envi de programmer dessus, je vous laisses à vos recherches. Les méthodes ne différent pas complètement.

Pour internationaliser notre application, nous allons utiliser la bibliothèque GNU destinée à cela, gettext.

Cela permet de distribuer facilement une application dans différentes langues, sans la recompiler, et sans mettre de nombreuses conditions dans le code source. De plus, de nombreux outils sont disponibles pour les traducteurs non développeurs. gettext n'est pas limité au C, et peut être utilisé dans la plupart des langages de programmation, du moment qu'un port existe.

Au final, dans votre code source C;

printf("My name is %s.\n", my_name);

se transformera en ceci:

printf(_("My name is %s.\n"), my_name);

Sommaire

Comprendre le fonctionnement de gettext

Dans votre programme, au lieu de mettre directement en mémoire votre chaîne de caractères, vous appelez la fonction gettext avec pour paramètre une chaîne de caractères qui sert de clé.

Si il existe une traduction pour la langue de l'utilisateur et la clé, la traduction est retournée en lieu et place.

Si il n'en existe pas, c'est la clée qui est retournée. Pour cette raison, on préférera plus nommer ses clés en anglais, qu'en hexadécimal. D'ailleurs pour le travail de traduction, c'est bien plus facile à comprendre.

La langue de l'utilisateur est déterminée à partir des paramètres régionaux de son ordinateur. Pour les langues, il s'agit de variables globales de la forme [language[_territory][.codeset].

Pour un français habitant en france, on aura donc une variable locale de type fr_FR.UTF-8, pour un anglais, on aura en_GB.UTF-8.

Si on se contente du choix automatique possible avec gettext, on en retiendra juste qu'il vaut mieux écrire en unicode. En effet, sur les systèmes modernes, l'unicode UTF-8 est désormais l'encodage par défaut.

De plus, pour un programme qui vise à être traduit dans de nombreuses langues, il vaut mieux avoir un encodage permettant d'utiliser tout les caractères disponibles. Après, rien ne vous empêche d'utiliser un autre encodage, mais ce ne sera pas détaillé dans ce tutorial.

Installer gettext

Si gettext n'est pas déjà installé, vous pouvez l'installer avec votre gestionnaire de paquets préféré, ou télécharger les sources et compiler manuellement (./configure && make && sudo make install). C'est ce que j'ai fait pour mon mac, et bien que la compilation soit étrangement longue, tout fonctionne à merveille.

Les fichiers de localisation

Il existe trois types de fichiers de localisation. Avec deux grandes familles, les fichiers compilés à destination du programme (d'extension .mo), et les fichiers textes lisibles par un être humain (.po et .pot).

Les fichiers pour humains sont couramment dans un dossier po à la base du programme. Le fichier .pot est nommé par le nom du programme, nom_programme.pot encore une fois par soucis de faire comme tout le monde. Les autres fichiers .po sont nommés par leur locale, pour faire simple. Ainsi pour la traduction française, on aura fr_FR.po, pour l'américaine, en_US.po.

Inutile d'indiquer l'encodage dans le nom du fichier. De même rien n'empêche de ne pas utiliser d'indication de région, on peut donc mettre un fichier fr.po qui sera à destinations de tout les utilisateurs francophones.

Vous générez le fichier .pot à partir d'une commande et du code C. C'est un des énormes avantages de gettext, une commande suffit à extraire toutes les clés de vos fichiers de code et à générer le fichier .pot. Le gain de temps est assuré.

Le fichier .pot sert de fichier de base pour les fichiers .po de traduction. Vous ne devez donc pas le modifier, ça risque de créer quelques problèmes, et ça ne sert à rien. C'est ce fichier que vous devez fournir aux traducteurs, à moins qu'ils ne préfèrent partir d'une traduction déjà réalisée dans une autre langue. Mais pour commencer, vous copier le fichier .po.

Pour les fichiers compilés, c'est un peu plus compliqué, mais gettext est construit comme ça.
Tout est contenu dans locale/language/LC_MESSAGES/nom_programme.mo .
Et il ira chercher les fichiers dans ces répertoires, ne changez pas les noms. Par exemple, pour le fichier de traduction en français, on aura un chemin du type locale/fr/LC_MESSAGES/nom_programme.mo .

gettext propose des outils pour compiler les fichiers .po en .mo (ou décompiler les fichiers .mo en .po, mais c'est moins utile).

Dans le code C

Nous allons aborder l'utilisation de gettext dans un langage en C à l'aide d'un programme très simple, à peine éloigné du Hello World.

Nous avons besoin de deux nouvelles bibliothèques:

#include <libintl.h>
#include <locale.h>

Un raccourcis très répandu consiste à appeler la fonction gettext par un underscore, permettant de ne pas surcharger visuellement le code. On ajoute donc cette ligne:

#define _(string) gettext(string)

Ensuite, nous initialisons gettext (en premier dans le programme):

setlocale(LC_ALL, "");
textdomain(NOM_PROGRAMME);
bindtextdomain(NOM_PROGRAMME, "locale");

Ici, le nom du programme est le même que celui que vous avez utilisé pour nommer vos fichiers, sinon vous risquez d'avoir des surprises.

Comme nous l'avons vu dans l'introduction, pour traduire une chaîne de caractères, nous appelons donc la fonction _(), ce qui donne: _("Hello\n") par exemple.

Voici le code d'un programme d'exemple, très simple:

#include <stdio.h>
#include <stdlib.h>
#include <libintl.h>
#include <locale.h>

#define _(string) gettext(string)
#define NOM_PROGRAMME "hello"

void init_gettext()
{
    setlocale(LC_ALL, "");
    textdomain(NOM_PROGRAMME);
    bindtextdomain(NOM_PROGRAMME, "locale");
}


int main (int argc, char const* argv[])
{
    init_gettext();
    
    /* Liste des messages à traduire */
    
    char *tab[] = {_("Hello"),_("Great"),_("Done"),_("Error"),_("Quit")};
    
    /* Il faut les afficher maintenant */
    int i;
    for(i = 0; i < (sizeof tab / sizeof *tab); i += 1 )
    {
        printf("%s\n",tab[i]);
    }
    
    return 0;
}

Attention, une chose à ne pas faire est de remplacer:

char *tab[] = {_("Hello"),_("Great"),_("Done"),_("Error"),_("Quit")};
// par:
char *tab[] = {"Hello","Great","Done","Error","Quit"};

et,

printf("%s\n",tab[i]);
// par:
printf("%s\n",_(tab[i]));

En effet, deux problèmes se posent, le premier est qu'il est stocké deux fois plus de texte dans la ram (la clé, et la traduction). Le deuxième est que le parseur de gettext ne verra pas vos clés (il repère les appels à la fonction gettext et _), et ne les ajouteras pas automatiquement au fichier .pot .

Pour compiler votre programme, utilisez la classique commande cc main.c -Wall -lintl

Utiliser gettext

Le parseur de gettext qui crée le fichier .pot à partir des sources est xgettext.

Dans le dossier de votre application, il s'utilise comme cela:
xgettext main.c autre_fichier.c un_autre.c -o po/nom_programme.pot --keyword=_ --from-code=utf-8

Vous donnez chaque fichier à analyser, vous indiquez où enregistrer le fichier .pot, que vous travaillez en unicode, et que vous avez rajouté le raccourcis underscore pour les appels de fonctions.

Pour le programme d'exemple, on utilise cette commande: xgettext main.c -o po/hello.pot --keyword=_ --from-code=utf-8

Vous copiez maintenant le fichier .pot en .po, par exemple: cp po/hello.pot po/fr.po

Vous pouvez modifier le fichier .po désormais. msgid est la clé et msgstr est la traduction.
Il est également indiqué où est rencontré chaque clée, pour mieux se repérer par rapport au code lorsque le nom de la clé est ambigu.
Pour traduire, un éditeur de texte fonctionne très bien, mais il existe aussi des outils destinés à cela.

Voici par exemple poEdit:
poEdit

Une fois votre traduction finie, vous devez la compiler dans le bon dossier. Si celui si n'existe pas encore, il faut le créer:
mkdir -p locale/fr/LC_MESSAGES Puis exécuter la commande msgfmt:
msgfmt po/fr.po -o locale/fr/LC_MESSAGES/hello.mo
Vous pouvez maintenant lancer votre application, si vous avez les bonnes locales, tout devrait fonctionner.

Pour changer vos locales, vous devez changer la définition de la variable LC_ALL:
export LC_ALL="en_GB.UTF-8" par exemple.
Pour revenir comme avant, LC_ALL="C" est à utiliser.
La commande locale affiche les différentes variables locales, pour ne pas se perdre.

Lorsqu'il y a des changements dans votre code source, vous devez ensuite régénérer les fichiers de traductions pour prendre en compte les modifications. Pour cela, régénérez le fichier .pot avec xgettext comme plus haut.
Vous devez ensuite répercuter les modifications sur les différents fichiers .po.

Vous pouvez utiliser un logiciel comme poEdit qui propose cette fonctionnalité, ou utiliser la commande gettext msgmerge.
Par exemple:
msgmerge fr.po hello.pot --verbose -o fr.po Vous devez ensuite à nouveau compiler la locale avec msgfmt.

Conclusion

Tout ceci est un peu pénible au début, mais avec l'habitude, le gain de temps est évident. Je vous encourage à internationaliser vos programme, vous y gagnerez beaucoup au final. Et puis vos traductions ne sont pas dépendantes du langage utilisé, ou même du projet. C'est plus souple d'utilisation, rien ne vous empêche de réécrire votre application dans un autre langage ou même de combiner plusieurs langages de programmation en même temps.

Sources

Commentaires

Le 30/09/2010 à 20:32

Très intéressant  

Vos explications sur le sujet sont éclairantes. Merci !

Le 03/10/2010 à 17:04

utilisation gettext à partir des sources en français 

Bonjour J'ai bien compris le fonctionnement de gettext mais le problème qui se pose est que toutes mes chaines de caractères sont en français. Mon projet est hébergé par Launchpad. En lisant la documentation, il serait nécessaire d'avoir les sources en anglais. (Le fichier .pot généré ne contient pas d'indication de la langue) Ne peut-on pas garder les sources en français et commencer la traduction vers les autres langues ? Si oui, quelle est la méthode ? Merci

Ajouter un commentaire