Traduction d'application Python/PyGTK
Par yarod le lundi, octobre 1 2007, 19:35 - Développement - Lien permanent
Ce tutoriel va nous apprendre à rendre nos application Python/PyGTK polyglotte. Il s'agît de la traduction de l'article Translating your Python/PyGTK application.
Nous ferons nos premiers pas pour traduire (localiser - l10n et internationnaliser - i18n) notre application PyGTK. Nous utiliserons l'application PyWine sur laquelle nous avons travaillé dans les tutoriaux précédents :
Les sources du projet et tous les fichiers nécessaires pour ce tutoriel peuvent être téléchargé sur le site de l'auteur.

Création des Fichiers Portable Object
Afin de pouvoir traduire les textes, nous utiliserons le module gettext module. Ce module est une encapsulation de l'outil GNU gettext dont on peut trouver la documentation ici.
La première étape va consister à éditer notre fichier python (pywine.py) et marquer toutes les chaînes que nous voulons traduire. Pour cela, nous utiliserons la fonction _("xxx") (NdT: Il s'agît bien d'une fonction dont le nom se résume à un blanc souligné) standard que la plupart des personnes utilisent pour marquer les chaînes à traduire. Dans la situation suivante :
#Get the treeView from the widget Tree
self.wineView = self.wTree.get_widget("wineView")
Si on ne modifie rien au code, la chaîne de caractère "wineView" peut être une chaîne qu'il faut traduire, ou au contraire conserver en l'état. Nous devons trancher et entourer chaque chaîne à traduire par la fonction _(). Voici toutes les chaînes de caractères qui ont besoin d'un traduction :
self.sWine = _("Wine")
self.sWinery = _("Winery")
self.sGrape = _("Grape")
self.sYear = _("Year")
self.show_error_dlg(_("Error opening file"))
self.show_error_dlg(_("Error opening file"))
file_dialog = gtk.FileChooserDialog(title=_("Select Project")
filter.set_name(_("pyWine database"))
filter.set_name(_("All files"))
L'étape suivante est la génération du fichier Portable Object Template en utilisant l'outil gettext en ligne de commandes. Sous Linux ou OS X, gettext est probablement déjà installé. Si vous utilisez Windows, l'outil est disponible sous forme d'exécutable depuis le FTP GNU gettext ou depuis le projet sourceforge gettext for Win32.
Pour créer le fichier PO, il faut donner quelques informations à gettext. La première chose est que le fichier à lire est du code source Python :
--language=Python
La seconde est que nous ne voulons traduire que les chaînes marquée par _() :
--keyword=_
La troisième est le fichier à créer :
--output=pywine.pot
Et enfin, le dernier argument est le nom du fichier à lire :
pywine.py
Si l'on réunit tous ces arguments, on obtient la ligne de commande :
xgettext --language=Python --keyword=_ --output=pywine.pot pywine.py
C'est tout ce que nous devrions normalement faire, mais comme nous utilison Glade pour l'interface graphique, il faut extraire et traduire les chaînes de caractères de notre projet Glade. Pour ce faire, nous procédons comme suit (extrait de la l'excellente FAQ PyGTK : How do I internationalize a PyGTK and libglade program?) :
intltool-extract --type=gettext/glade pywine.glade
Ceci va permettra de créer le fichier pywine.glade.h, dont le contenu est le suivant :
char *s = N_("Add Wine");
char *s = N_("Edit Wine");
char *s = N_("Grape Variety: ");
char *s = N_("PyWine");
char *s = N_("Wine");
char *s = N_("Wine:");
char *s = N_("Winery: ");
char *s = N_("Year Produced: ");
char *s = N_("_About");
char *s = N_("_Add");
char *s = N_("_Edit");
char *s = N_("_File");
char *s = N_("_Help");
char *s = N_("_View");
Remarquez que les chaînes de caractères sont entourées de l'appel à N_(). Il faudra prendre en compte cette nouvelle méthode pour la tradution des fichiers pywine.py, pywine.glad.h. Ceci est fait par la ligne de commande suivante :
xgettext --language=Python --keyword=_ --keyword=N_ --output=pywine.pot pywine.py pywine.glade.h
Cette commande va créer un fichier nommé pywine.pot dont le contenu est le suivant :
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR , YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2006-12-01 23:59-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#: pywine.py:77 pywine.glade.h:5
msgid "Wine"
msgstr ""
#: pywine.py:78
msgid "Winery"
msgstr ""
#: pywine.py:79
msgid "Grape"
msgstr ""
#: pywine.py:80
msgid "Year"
msgstr ""
#: pywine.py:176 pywine.py:178
msgid "Error opening file"
msgstr ""
#: pywine.py:229
msgid "Select Project"
msgstr ""
#: pywine.py:237
msgid "pyWine database"
msgstr ""
#: pywine.py:242
msgid "All files"
msgstr ""
#: pywine.glade.h:1
msgid "Add Wine"
msgstr ""
#: pywine.glade.h:2
msgid "Edit Wine"
msgstr ""
#: pywine.glade.h:3
msgid "Grape Variety: "
msgstr ""
#: pywine.glade.h:4
msgid "PyWine"
msgstr ""
#: pywine.glade.h:6
msgid "Wine:"
msgstr ""
#: pywine.glade.h:7
msgid "Winery: "
msgstr ""
#: pywine.glade.h:8
msgid "Year Produced: "
msgstr ""
#: pywine.glade.h:9
msgid "_About"
msgstr ""
#: pywine.glade.h:10
msgid "_Add"
msgstr ""
#: pywine.glade.h:11
msgid "_Edit"
msgstr ""
#: pywine.glade.h:12
msgid "_File"
msgstr ""
#: pywine.glade.h:13
msgid "_Help"
msgstr ""
#: pywine.glade.h:14
msgid "_View"
msgstr ""
Comme vous pouvez le constater, il contient les chaînes à traduire de pywine.py et de pywine.glade.h. La prochaine chose à faire est de créer un fichier .PO (Portable Object). Chaque fichier .PO contient une traduction dans une langue. La création du fichier PO se fait à partir du fichier .POT que nous venons de créer. Nous utiliserons l'outil en ligne de commandes nommé msginit.
Il faut donner le nom du fichier en entrée à msginit :
--input=pywine.pot
Il faut préciser l'option de localisation (NdT: la langue dans laquelle seront traduits les messages). Comme je suis Canadien anglophone, ce sera en_CA, qui est en fait la valeur de language_COUNTRY (NdT: Sur mon système Ubuntu, la commande ///env | grep -i lang/// retourne ///LANG=fr_FR.UTF-8///) :
--locale=en_CA
Pour savoir dans quelle localisation vous êtes, utilisez la commande locale -a sur un système Unix ou apparenté. (NdT: Sur mon système Ubuntu, la commande env | grep -i lang, qui retourne LANG=fr_FR.UTF-8, c'est un peu plus court
)
La commande à saisir pour créer un fichier Anglais Canadien est la suivante :
msginit --input=pywine.pot --locale=en_CA
Ajoutons la création d'un fichier Anglais Etats-Unis :
msginit --input=pywine.pot --locale=en_US
NdT : Mes sources sont en langue Anglaise. J'effectue donc une traduction française :
msginit --input=pywine.pot --locale=fr_FR
Il vous sera peut-être demandé votre adresse mail afin de permettre à quelqu'un de vous contacter à propose de la traduction. Vous devriez maintenant avoir les fichiers en_CA.po et en_US.po créées dans le dossier de pywine. Modifions les fichiers afin de les rendre différents :
en_CA.po :
#: pywine.py:77 pywine.glade.h:5
msgid "Wine"
msgstr "Wine Canadian Style"
en_US.po :
#: pywine.py:77 pywine.glade.h:5
msgid "Wine"
msgstr "Wine US Style"
Il faut ensuite transformer les fichiers .po en fichier .mo, utilisable par le module gettext. Les fichiers .mo sont de simples versions binaires des fichiers .po au format texte.
Nous devons créer une arborescence de dossiers correcte pour contenir les fichier .mo. C'est expliqué dans la fonction bindtextdomain du module gettext de Python :
bindtextdomain( domain, localedir) Associe le domaine au dossier localedir. Plus concrétement, gettext va chercher les fichiers binaires .mo pour le domaine choisi en utilisant le chemin (sur Unix) : localedir/language/LC_MESSAGES/domain.mo, avec les languages récupérées depuis les variables d'environnement LANGUAGE, LC_ALL, LC_MESSAGES, and LANG respectivement.
Il nous faut donc créer les dossiers suivant :
en_CA/LC_MESSAGES
en_US/LC_MESSAGES
Grâce aux commandes :
mkdir -p en_CA/LC_MESSAGES
mkdir -p en_US/LC_MESSAGES
Now we use the msgfmt command line program to create our .mo files in the correct directory :
msgfmt --output-file=en_CA/LC_MESSAGES/pywine.mo en_CA.po
msgfmt --output-file=en_US/LC_MESSAGES/pywine.mo en_US.po
Le Code Python
Maintenant que nous possédons des fichiers de traductions prêt à l'emploi, il faut implémenter leur utilisation. Heureusement il n'y a pas grand chose à écrire, mais j'ai trouvé peu d'exemples ou d'informations sur la manière correcte de le faire, et parfois avec du code confus.
Première étape, l'importation des modules gettext et locale.
import locale
import gettext
Deuxième étape, définir le domaine de nos traductions, en général c'est juste le nom de l'application. Le module gettext utilisera ce domaine pour sa recherche de fichiers .mo. Ainsi, si votre domaine vaut "12345", gettext recherchera les fichiers 12345.mo. Vous remarquerez que nous avons créé des fichiers pywine.mo et donc il faut utiliser "pywine" comme domaine :
APP_NAME = "pywine"
Maintenant, de manière générale pour un système Unix-like, vous installez vous fichiers .mo à l'emplacement /usr/share/locale. Mais comme nous n'installerons pas notre application et que nous voulons qu'elle soit facilement portable sur d'autres systèmes d'exploitation, nous installerons les traductions dans un dossier local.
Voici tout le code qu'il faut ajouter à la méthode init() de la class pywine. C'est un peu compliqué mais les commentaires et mes explications qui suivent devrais rendre la chose plus facile à comprendre :
(NdT : J'ai traduit les commentaires à des fins de meilleure copréhension)
# Mise en oeuvre des traductions
# Récupérer le dossier local car rien ne sera installé
# dans les chemins standards
self.local_path = os.path.realpath(os.path.dirname(sys.argv[0]))
# Initialiser la liste des langages supportés
langs = []
# Tester la localisation en vigueur sur ce système
lc, encoding = locale.getdefaultlocale()
if (lc):
# Si une localisation par défaut existe,
# la mettre en premier dans la liste
langs = [lc]
# Maintenant, récupérer la liste des langages du système
language = os.environ.get('LANGUAGE', None)
if (language):
# language contient une chaîne du style en_CA:en_US:en_GB:en
# pour un system Linux, sur Windows c'est vide. Il faut découper
# la chaîne en une liste
langs += language.split(":")
# Ajouter les traductions connues à la fin de la liste
langs += ["en_CA", "en_US"]
# langs contient maintenant la liste de toutes les langues
# que nous pouvons utiliser. Premièrement, nous testons la valeur
# par défaut, ensuite ce que le système nous a donné et enfin
# les langages connus de cette applications.
gettext.bindtextdomain(APP_NAME, self.local_path)
gettext.textdomain(APP_NAME)
# Obtenir le langage à utiliser
self.lang = gettext.translation(APP_NAME, self.local_path,
languages=langs, fallback = True)
# Installer le langage, associer le marqueur _() à la fonction
# self.lang.gettext() qui sera utilisé pour traduire
_ = self.lang.gettext
Je vais maintenant expliquer le code par petit fragment de sorte que cela soit plus facile à comprendre. La première chose qui est faite est de récupérer le chemin du fichier python qui est en cours d'exécution, c'est simple et pas besoin d'explication superflue.
La première grande action est la constitution de la liste des langages, ou localisation que l'on veut supporter. Cette liste sera utilisé pour déterminer à la fois l'ordre dans lequel seront cherchés les fichiers .mo et lesquels seront cherchés.
La première localisation est celle par défaut, nous utilisons le module locale pour l'obtenir :
# Init the list of languages to support
langs = []
#Check the default locale
lc, encoding = locale.getdefaultlocale()
if (lc):
#If we have a default, it's the first in the list
langs = [lc]
Ensuite, il faut récupérer la liste des langages supportés par le système d'exploitation. La chaîne retournée doit être analysée afin de constituer une liste. Sur Windows ou OS X, cette chaîne n'existe pas, il faut donc s'assurer qu'elle ne soit pas None.
# Now lets get all of the supported languages on the system
language = os.environ.get('LANGUAGE', None)
if (language):
"""langage comes back something like en_CA:en_US:en_GB:en
on linuxy systems, on Win32 it's nothing, so we need to
split it up into a list"""
langs += language.split(":")
L'étape finale pour constituer notre liste de langage est d'y ajouter les localisations que notre application supporte. Il s'agît des fichiers .mo que nous avons fabriqués. nous pouvons les ajouter directement dans la liste, ou alors, si l'on est certain qu'ils sont tous installés, nous pouvons ajouter ce que l'on considère comme localisation par défaut puisque l'on est certain qu'elle sera trouvée.
"""Now add on to the back of the list the translations that we
know that we have, our defaults"""
langs += ["en_CA", "en_US"]
La raison qui nous les fait ajouter à la fin de la liste est que l'utilisateur pourra éventuellement cibler précisément la localisation qu'il souhaite. Nous ajoutons les notres au cas ou rien ne serait trouvé.
L'étape qui suit concerne l'association du domaine (pywine) à un dossier (le dossier local) et alors nous pourrons appliquer une valeur au domaine courant, qui est celui utilisé pour la recherche des fichiers .mo :
gettext.bindtextdomain(APP_NAME, self.local_path)
gettext.textdomain(APP_NAME)
Voici les explications concernant les fonctions de la documentation python:
bindtextdomain( domain, localedir) Associe le domaine au dossier localedir. Plus concrétement, gettext va chercher les fichiers binaires .mo pour le domaine choisi en utilisant le chemin (sur Unix) : localedir/language/LC_MESSAGES/domain.mo, avec les languages récupérées depuis les variables d'environnement LANGUAGE, LC_ALL, LC_MESSAGES, and LANG respectivement. Si localedir is omis ou vaut None, alors l'association en cours pour le domaine est retournée.
textdomain( domain) Modifie ou retourne le domaine global en cours. Si domain vaut None, alors le retourne le domaine global en cours, sinon il prend la valeur de domain, qui est ensuite retournée.
L'étape qui suit va consister à récupérer la traduction que nous voulons utiliser et à connecter gettext sur le fichier .mo :
# Get the language to use
self.lang = gettext.translation(APP_NAME, self.local_path
, languages=langs, fallback = True)
La fonction gettext.translation() est expliquée de la manière suivante :
translation( domain, localedir[, languages[, class_[, ...]]]]) Retourne une instance de Translations basée sur domain, localedir, et languages, qui sont transmis à find() pour obtenir la liste des chemins des fichiers .mo. Les instances avec des fichiers .mo identiques sont mise en cache. La classe instancié est soit class_ si fournie, sinon c'est GNUTranslations. Le constructeur de la classe doit prendre un objet fichier comme unique argument. S'il est transmis, codeset transformera le jeu de caractères utilisé pour l'encodage des chaînes traduites. Si plusieurs fichiers sont trouvés, les derniers seront utilisés comme fichiers de secours des précédents. Pour permettre l'utilisation des fichiers de secours, copy.copy est utilisé pour cloner chaque objet traduction depuis le cache; l'instance courante des données reste partagée avec le cache. Si aucun fichier .mo n'est trouvé, cette fonction retourne IOError si fallback est à false (ce qui est sa valeur par défaut), et elle retourne une instance NullTranslations si fallback vaut true.
Donc la fonction recherchera les fichiers .mo de chacun des languages dans l'ordre de langs, et elle retournera un objet GNUTranslations en cas de succès, ou un objet NullTranslations en cas d'échec (puisque nous avons donné True à la valeur de fallback.)
Nous associons alors l'objet retourné par la fonction de traduction de gettext à _. De la sorte, lorsque nous appelons _("xxx"), c'est un appel à self.lang.gettext("xxx") qui sera effectué. Appeler _("xxx") est juste un raccourci pratique et très usité pour marquer les chaînes à traduire.
"""Install the language, map _() (which we marked our
strings to translate with) to self.lang.gettext() which will
translate them."""
_ = self.lang.gettext
Bien, c'est tout ce que nous avions à faire. Maintenant, si l'on exécute pywine avec les arguments :
PyWine$ LANG=en_CA python pywine.py

Vous afficherez l'entête de colonne "Wine Canadian Style", et si vous l'exécutez comme suit :
PyWine$ LANG=en_CA python pywine.py

Vous affichez l'entête de colonne "Wine US Style". Et enfin, si vous l'exécutez avec du Français :
PyWine$ LANG=fr_FR python pywine.py
Le résultat dépendra de la manière dont est configuré votre système, et vous obtiendrez soit le text Canadien, soit le texte US.
La totalité des sources et des fichiers nécessaires pour ce tutoriel peut être téléchargé sur le site de l'auteur.
Conclusion
Voila, nous en avons terminé avec la traduction. Cette méthode fonctionne sans problème sur n'importe quel système d'exploitation. J'aurai voulu inclure un fichier de langage différent (comme du Français, de l'Espagnol, etc.) mais mes capacités multi-lingues ne sont pas assez costaude.
Si quelqu'un souhaite faire une simple traduction du fichier pywine.pot et me l'envoie, ce serait très apprécié. On pourrait alors avoir des tests sur différents systèmes.
Pendant mes recherches pour ce tutoriel, je suis tombé sur de bonnes informations dispensées par ces sites :
* Python, wxPython and internationalization (i18n) * How do I internationalize a PyGTK and libglade program?
Comme toujours, si vous tombez sur un bug ou trouvez un problème dans le code, faites le moi savoir !
Commentaires
Bonjour !
Je voudrais transformer un fichier shell en application gtk.
J'y connais pas grand chose mais je suis débrouillard !
Le script shell c'est celui pppoeconf qui permet de paramétré sa connexion réseau en adsl
J'ai téléchargé les sources et je comprend à peut pret comment il marche.
Pense tu que c'est possible ?
Peut-t'on passer des commande style shell avec python ou autre chose qui irait avec glade ?
Merci pour les infos !
Jérôme
On peut faire énormément de chose avec Python et il est simple de "passer des commandes shell". Il faut regarder la documentation du module subprocess

Il ne faut pas mélanger glade et python. Glade est l'outil qui permet de faire l'interface graphique. Python est le langage qui va piloter l'application (et son interface glade).
Bon courage
tres bien expliqué, merci.
je me demandais quelle editeur tu utilisais sur ubuntu pour écrire tes codes python?
moi personnelement je viens de découvrir geany mais si il y a d'autres éditeurs plus sympa je suis preneur.