meta données pour cette page
Ceci est une ancienne révision du document !
Script bash internationalisé
Ou comment traduire un script bash.
Bash possède un mécanisme interne d'internationalisation. C'est pas bien difficile à trouver. Par exemple y'a ce how-to, mais il est en anglais, et puis après le forum part en quenouille sur une sombre histoire de locale par défaut…
En fait, la base est simple; Les chaines entre $"
et "
seront candidates à la traduction. On peut donc écrire des choses comme ça :
- essai-001
echo $"Hello World !"
Et si on a fait tout ce qui est nécessaire pour la traduction, on aura :
Bonjour Monde !
Le principe
Pour deviner quel langue utiliser, bash consulte d'abord la variable d'environnement LANGUAGE
. Si elle n'est pas définie, il consulte la variable d'environnement LANG
. Les détails sont dans le how-to de Debian sur l'utilisation des applications en français. Pour un francophone, cette variable devrait être fixée à quelque chose qui commence par fr
. J'ai la valeur fr_FR.UTF-8
car j'utilise le français tel qu'il est parlé en France. Les francophones d'autres régions du monde auront probablement autre chose, mais toujours commençant par fr
.
bash cherche alors à mettre la main sur le catalogue des chaines traduites. Ainsi lorsqu'il rencontre une chaine de la forme $"chaine"
, il va tenter de trouver une correspondance dans le catalogue. Le catalogue est situé dans une arborescence dont le nom est dérivé de la valeur de la variable LANGUAGE
ou LANG
. À partir d'un répertoire de base, bash recherche dans un sous répertoire fr*/LC_MESSAGES/
s'il trouve un fichier catalogue. l'exemple ci dessous montre comment sont répartis les catalogues de chaines pour les langues tchèque de République tchèque (cz_CZ), anglais de Nouvelle Zélande (en_NZ) français de Belgique (fr_BE) et français de France (fr_FR).
locale ├── cz_CZ │ └── LC_MESSAGES │ └── multilingue.mo ├── en_NZ │ └── LC_MESSAGES │ └── multilingue.mo ├── fr_BE │ └── LC_MESSAGES │ └── multilingue.mo └── fr_FR └── LC_MESSAGES └── multilingue.mo
Notez au passage que bash se donne vraiment beaucoup de mal pour trouver le catalogue de chaines traduites. Si on fait exprès de le taquiner un peu et qu'on regarde avec strace
ce qu'il essaie de faire, on peut voir ceci :
open(".../locale/fr_FR.UTF-8/LC_MESSAGES/multilingue.mo", O_RDONLY) = -1 ENOENT (No such file or directory) open(".../locale/fr_FR.utf8/LC_MESSAGES/multilingue.mo", O_RDONLY) = -1 ENOENT (No such file or directory) open(".../locale/fr_FR/LC_MESSAGES/multilingue.mo", O_RDONLY) = -1 ENOENT (No such file or directory) open(".../locale/fr.UTF-8/LC_MESSAGES/multilingue.mo", O_RDONLY) = -1 ENOENT (No such file or directory) open(".../locale/fr.utf8/LC_MESSAGES/multilingue.mo", O_RDONLY) = -1 ENOENT (No such file or directory) open(".../locale/fr/LC_MESSAGES/multilingue.mo", O_RDONLY) = 3
Un exemple
Pour comprendre, prenons un petit script simple appelé multilingue
:
- multilingue
#! /bin/bash TEXTDOMAIN=multilingue TEXTDOMAINDIR="$PWD/locale" echo $"Hello wonderful World !" read -p $"What is your name ? " nom tput smso echo $"Welcome $nom" tput rmso
La variable TEXTDOMAINDIR
indique le répertoire de base où on trouve toutes les traductions dans toutes les langues et variantes. Quant à TEXTDOMAIN
, elle indique quel catalogue de chaines doit être utilisé. J'utilise ici un catalogue du nom du script. Le répertoire de base des traductions est, pour l'instant, le sous répertoire locale
du répertoire courant. Si on installe le script dans /usr/bin
, ce répertoire de base pourrait être /usr/share/locale
.
Il faut maintenant traduire.
traduire en vitesse
Demandons à bash de nous donner les chaines à traduire qui sont dans notre script, et plaçons les dans un fichier .po
que nous traduirons, puis transformerons en fichier .mo
langue=fr_FR mkdir -p locale/$langue/LC_MESSAGES bash --dump-po-strings multilingue >multilingue.po editor multilingue.po msgfmt -o locale/$langue/LC_MESSAGES/multilingue.mo multilingue.po
traduire le fichier .po
est très simple, il est composé de paires de lignes msgid
, msgstr
. msgid
contenant le texte d'origine auquel il faut faire correspondre une traduction dans une ligne msgstr
. Voilà pour notre exemple, les textes qu'on pourrait utiliser.
- multilingue.po
#: multilingue:6 msgid "Hello wonderful World !" msgstr "Bonjour Monde merveilleux !" #: multilingue:7 msgid "What is your name ? " msgstr "Comment vous appelez vous ? " #: multilingue:9 msgid "Welcome $nom" msgstr "$nom, soyez le bienvenu."
Maintenant, si on execute le script, on a ceci :
schplurtz@grumpfs (0) $ ./multilingue Bonjour Monde mervilleux ! Comment vous appelez vous ? Schplurtz Schplurtz, soyez le bienvenu. schplurtz@grumpfs (0) $
C'est pas mal, non ?
Traduire plus sérieusement
Ben pourquoi, c'était pas sérieux ? Disons que c'était maladroit. D'abord, un fichier .po
peut contenir plus d'informations que les seules chaines traduites, le nom et l'adresse de courriel du traducteur pour ne citer qu'eux. Ensuite, remarquons que notre procédure n'est pas adaptée aux changements qui peuvent intervenir dans le script. Si on rajoute une chaine dans le script, ou si on en change une, il faudrait tout retraduire puisque la méthode bash --dump... >toto.po
va écrabouiller le précédent travail de traduction. Notons encore que si une chaine apparait plusieurs fois dans le script, elle se trouvera aussi plusieurs fois dans le fichier à traduire, et ce sera alors à vous d'éliminer les doublons de ce fichier, sinon la génération du catalogue échouera. Voilà, ces réponses suffisent ? pas tout a fait ? Allez, cerise sur la gâteau, si les modifications dans le script sont suffisamment légères, alors on peut conserver automatiquement l'ancienne traduction dans le fichier .po
–celui qu'on édite pour y inclure les traductions– tout en ayant un commentaire qui indique qu'il y a eu un léger changement. Ça allège le travail de traduction.
La meilleure méthode est donc
- utiliser un fichier intermédiaire
.pot
issu des chaines du script et obtenu parxgettext
, - initialiser le fichier de chaines
.po
, - traduire avec poedit,
- ça crée automatiuement le fichier
.mo
En cas de changement dans le script, alors on effectue les actions suivantes, - recréer le fichier intermédiaire
.pot
, - recréer le fichier
.po
en conservant autant que possible les précédentes traductions, - générer un nouveau fichier
.mo
.
Voilà les commandes :
langue=fr_FR mkdir -p locale/$langue/LC_MESSAGES bash --dump-po-strings multilingue | xgettext -L PO -o multilingue.pot - msginit -l $langue -i multilingue.pot -o locale/$langue/LC_MESSAGES/multilingue.po poedit locale/$langue/LC_MESSAGES/multilingue.po # ca crée le fichier locale/$langue/LC_MESSAGES/multilingue.mo
Après modification du script il faudra passer les commandes suivantes :
langue=fr_FR bash --dump-po-strings multilingue | xgettext -L PO -o multilingue.pot - msgmerge -U locale/$langue/LC_MESSAGES/multilingue.po multilingue.pot poedit locale/$langue/LC_MESSAGES/multilingue.po
Il ne peut y en avoir qu'un
Un quoi, en fait ? Un niveau de déréférencement1) de variable.
Non, un quoi j'ai dit.
Ok, voilà un exemple. La chaine contient des variables mais ne cela n'a pas d'importance, et ça fonctionne on l'a déjà vu.
- essai-002
firstname=toto secondname='le heros' echo $"Hello $firstname $secondname !"
Maintenant, un peu plus compliqué, on a plein de chaines de résultats dans un tableau, et on veut afficher le statut en fonction d'une autre valeur.
- essai-003
status=( $"Perfect", $"almost correct", $"could be better", $"all wrong" ) name=toto result=3 echo "$name ${status[$result]}"
Jusque là, on n'a qu'un niveau de variable dans la chaine à afficher. $name
et ${status[$result]}
sont au même niveau, donc tout va bien.
Imaginons maintenant qu'on veuille faire rentrer $name
dans les chaines du tableau, ce qui permettra aux traducteurs de choisir la place que doit occuper le nom dans la phrase. Peut être au début dans une langue ("$name blablabla"
), et à la fin dans une autre langue ("gnak gnak gnik $name"
). Emporté par notre élan, on écrit le script naïf suivant :
- essai-004
status=( $"$name Perfect", $"$name almost correct", $"$name could be better", $"$name all wrong" ) name=toto result=3 echo "${status[$result]}"
Et là, cata ! Rien ne va plus. $name
semble complètement ignoré, disparu, effacé…
Ben oui, mais normal quoi. Si on prend pas à pas ce qui se passe lors de l'affichage, voilà ce qu'on obtient :
- bash voit une chaine
"${status[$result]}"
, - procède à l'expansion des variables, effectue le remplacement par la valeur qui est une traduction déjà réalisée
- et s'arrête là, content de lui.
$name
dans l'histoire ? il n'en est pas question au moment où on le souhaiterai. En fait, $name
a été évalué bien avant, lors de la définition du tableau:
- bash tombe sur la définition du tableau,
- trouve des chaines à traduire
- recherche un équivalent dans le catalogue,
- il trouve des chaines du genre
"parfait $name", "presque bien $name", "$name : tu peux faire mieux", "tout faux $name"
, - fait le remplacement de variable
$name
⇒''
car à ce moment$name
n'est pas encore définie… - range ce résultat dans la tableau et passe à la suite
Si on essaie frénétiquement ceci :
echo $"${status[$result]}"
on a le même résultat, mais pour plus cher : bash voit une chaine à traduire : $"${status[$result]}"
il va chercher dans le catalogue, il trouve …. la même chose : $"${status[$result]}"
2). Il passe ensuite à l'expansion des variables, remplace ${status[$result]}
par sa valeur qui se trouve être une traduction déjà réalisée et s'arrête là, content de son travail. Le reste est identique au cas précédent.
Si on essaie alors ceci :
- essai-005
status=( "$name Perfect", "$name almost correct", "$name could be better", "$name all wrong" ) name=toto result=3 echo $"${status[$result]}"
C'est pas mieux. Voir l'explication ci dessus, mais en plus, les chaines du tableau sont même pas traduites…
Il n'y a pas de solution alors ? ben si. Y'a printf. Au passage, il se trouve, que printf
est une commande interne de bash (et aussi de dash et ash) et donc peu coûteuse – même si le lien donné pointe vers la page de manuel de la commande externe du même nom. Un exemple correct pour ce script est :
- essai-006
#! /bin/bash TEXTDOMAIN=${0##*/} TEXTDOMAINDIR="$PWD/locale" status=( $"%s Perfect\\n", $"%s almost correct\\n", $"%s could be better\\n", $"%s all wrong\\n" ) name=toto result=$(( $RANDOM % 4 )) printf "${status[$result]}" "$name" read -p $"press the return key" # ou alors, si on fait des boites de dialogue, on peut utiliser ceci : result=$(( $RANDOM % 4 )) dialog --msgbox "$( printf "${status[$result]}" "$name" )" 0 0 result=$(( $RANDOM % 4 )) zenity --info --text="$( printf "${status[$result]}" "$name" )"
Les traducteurs pourront mettre les noms aux endroits où ils le veulent. Sauf qu'ils peuvent éventuellement ne même pas vraiment savoir à quoi correspond le %s
dans la chaine à traduire.
Notez l'emploi de TEXTDOMAIN=${0##*/}
. ${0##*/}
est toujours égal au nom du script, quelque soit ce nom.
Voilà le fichier .po pour ceux qui voudraient essayer :
- essai-006.po
#: essai-007:7 msgid "%s Perfect\\\\n" msgstr "%s, C'est parfait !\\\\n" #: essai-006:8 msgid "%s almost correct\\\\n" msgstr "Presque correcte %s\\\\n" #: essai-006:9 msgid "%s could be better\\\\n" msgstr "%s : Peut mieux faire\\\\n" #: essai-006:10 msgid "%s all wrong\\\\n" msgstr "%s a tout faux !\\\\n" #: essai-006:15 msgid "press the return key" msgstr "Appuyez sur la touche « entrée »"
et voilà aussi les commandes à passer :
lang=fr_FR mkdir -p locale/$langue/LC_MESSAGES/ msgfmt -o locale/$langue/LC_MESSAGES/essai-006.mo essai-006.po bash essai-006
Téléchargement bonus
un script d'exemple et des Makefile pour traduire…