====== 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 [[http://www.pclinuxos.com/forum/index.php?topic=59145.0|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 ! /* Ami francophone du monde entier, les exemples donnés ici sont pour le français tel qu'il est parlé en France. la variable ''LANG'' qui décrit la langue à utiliser a pour valeur ''fr_FR'' ou quelque chose comme ça, il te faudra adapter les exemples à ta valeur de langue Hum, si je commence avec ce genre de trucs, ça va rendre le bazar encore plus compliqué. Plutot utiliser des exemples auto-démerdant */ ===== 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 [[http://www.debian.org/doc/manuals/fr/debian-fr-howto/ch3.html|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 ''[[http://man7.org/linux/man-pages/man1/strace.1.html|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'' : #! /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: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 par ''xgettext'', - 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érencement((putain, j'ai du mal à écrire ça)) 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. 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. 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, 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]}"%%'' qui n'est pas une chaine à traduire, * procède à l'expansion normale des variables et remplace ''%%"${status[$result]}"%%'' par la valeur idoine du tableau ''status''; cette valeur est une traduction **déjà** réalisée, * et il 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"%%'', * remplace les chaines d'origine par leur traduction, * puis, au moment où il exécute la commande, c'est à dire l'affectation au tableau,\\ 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]}"%%''((sauf si vous avez pris des libertés de traduction)). 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 : 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 [[https://www.gnu.org/software/bash/manual/bash.html#index-printf|printf]], une commande interne de bash. Cette fonction interne reprend et étend la **commande externe** [[http://linuxmanpages.com/man1/printf.1.php|printf]]. Cette commande étant un interne de bast (et aussi de dash et ash) est donc peu coûteuse. Un exemple correct pour ce script est : #! /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-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 : langue=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... {{:envrac:multilingue.tar.bz2|}}