Schplurtzeries
Le wiki de schplurtz
Dokuwiki

24. February 2012 [Téléchargement bonus] ztrulphcs

Ceci est une ancienne révision du document !


Script bash internationalisé

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 :

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 paire 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. 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 certain léger changement. Ça allège le travail de traduction.

La meilleure méthode est donc

  1. utiliser un fichier intermédiaire .pot issu des chaines du script et obtenu par xgettext,
  2. initialiser le fichier de chaines .po,
  3. traduire avec poedit,
  4. ça crée automatiuement le fichier .mo
    En cas de changement dans le script, alors on effectue les actions suivantes,
  5. recréer le fichier intermédiaire .pot,
  6. recréer le fichier .po en conservant autant que possible les précédentes traductions,
  7. 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 qui fonctionne, on l'a déjà vu. il y a une variable dans la chaine à traduire, tout est nickel.

firstname=toto
secondname='le heros'
echo $"Hello $firstname $secondname !"

Maintenant, un peu plus compliqué, on a plein de chaine de résultats dans un tableau, et on veut afficher le statut en fonction d'une autre valeur.

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 :

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 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 et s'arrête là, content de son travail. Le reste est identique au cas précédent.

Si on essaie alors ceci :

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. Un exemple correct pour ce script est :

status=(
  $"%s Perfect",
  $"%s almost correct",
  $"%s could be better",
  $"%s all wrong"
)
name=toto
result=3
printf "${status[$result]}" "$name"
dialog --msgbox "$( printf "${status[$result]}" "$name" )" 0 0
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.

Téléchargement bonus

un script d'exemple et des Makefile pour traduire…

multilingue.tar.bz2

1) putain, j'ai du mal à écrire ça
2) sauf si vous avez pris des libertés de traduction