Internationalisation de scripts Bash

Bash permet en standard d’internationaliser vos scripts grâce à une syntaxe particulière et qui lui est propre.

La méthode standard

Pour indiquer qu’une chaîne de caractères doit être traduite, il faut utiliser la syntaxe Locale-Specific translation suivante :

$"This is a string to translate"

Quand Bash rencontre une chaîne de cette forme, il fait appel à l’utilitaire GNU gettext pour traduire la chaîne dans la langue courante de l’utilisateur.

Attention, il existe une autre forme très proche qui n’a aucun rapport et n’effectue traduction. Cette forme est nommée ANSI-C Quoting car elle décode les séquences de caractères débutant par un anti-slash :

$'This is a string'

Inconvénients de la méthode standard

La sécurité

Tout serait parfait dans le meilleur des mondes si le fonctionnement de cette syntaxe ne présentait pas un risque au niveau de la sécurité.

Le problème est connu mais, s’il est indiqué dans la documentation de gettext, la documentation de Bash n’en souffle pas un mot !

Le problème vient du traitement de ces chaînes par Bash. Celui-ci effectue la traduction avant toute chose. Une fois la traduction faite, la chaîne résultante est traitée comme une chaîne à quotes double. Les backquotes et le caractère dollar continuent d’être des caractères spéciaux.

Si un traducteur place un caractère spécial sans s’en rendre compte dans la traduction, Bash tentera de le traiter.

La séparation des fichiers de traduction

Un autre problème soulevé par l’utilisation de la forme $"…" est celui de la séparation des fichiers de traduction. La syntaxe standard fonctionne parfaitement pour un script Bash. Malheureusement, elle ne fonctionne plus si vous utilisez l’instruction source ou . de Bash.

Vous ne pouvez donc pas vous créer une bibliothèque de fonctions indépendante car Bash cherchera la traduction dans le domaine du script appelant et non dans le domaine du script appelé.

Bash utilise deux variables d’environnement pour lui indiquer le domaine et le répertoire contenant les fichiers de traduction. Ce sont les variables TEXTDOMAIN et TEXTDOMAINDIR.

Une première précaution doit déjà être prise quant à l’utilisation de ces variables : la variable TEXTDOMAIN doit être initialisée avant la variable TEXTDOMAINDIR.

La valeur affectée à TEXTDOMAIN indiquera à Bash d’aller chercher la traduction de préférence dans le fichier portant le nom du domaine suffixé avec .mo

Une solution

Pour palier aux problèmes décrits, voici un script que vous pouvez inclure dans vos projets.

Il crée une fonction t qui fera tout le travail de traduction.

Cette fonction est appelée de l’une des façons suivantes :

t "String to translate"
t "String format %s" "parameter"
t "String format %s %d" "parameter" 999

Elle renvoie la traduction sur la sortie standard.

Quand un seul paramètre est donné, la chaîne est traduite et envoyée sur la sortie standard.

Quand plusieurs paramètres sont données, la fonction t fonctionne comme la commande printf (qu’elle utilise) : le premier paramètre représente le format de la chaîne et les paramètres suivants les paramètres correspondants aux séquences %.

Elle est généralement utilisée dans une chaîne à quotes doubles :

"$(t "String to translate")"
"$(t "String format %s" "parameter")"
"$(t "String format %s %d" "parameter" 999)"

La seule variable d’environnement que la fonction t utilise est la variable TEXTDOMAINDIR. Le domaine de traduction est déduit du nom du fichier contenant le code en cours d’exécution. Cela fonctionne même pour les scripts chargés via l’instruction source ou ..

Code source de la fonction t

#!/usr/bin/env bash
test "$(type -t t)" = function && return 0

# Verify gettext presence
if which gettext > /dev/null
then
    # We can use gettext
    function t {
        local string script translated arguments

        # Get the string to translate
        string="$1"
        shift
        arguments=( "$@" )

        # Find the caller to define the gettext domain
        set $(caller)

        # The domain is the script basename
        script="$(basename "$2")"

        # Retrieve the translation for the script domain
        translated=$(gettext -n --domain="$script" -- "$string")

        # Check the remaining parameters
        if [ ${#arguments[@]} -eq 0 ]
        then
            # No remaining parameters, the string is output as is
            printf -- '%s' "$translated"
        else
            # Parameters remain, the translated string is a format string
            printf -- "$translated" "${arguments[@]}"
        fi
    }
else
    # We cannot use gettext, just output the same string
    function t {
        local string

        # Get the string to translate
        string="$1"
        shift

        # Check the remaining parameters
        if [ $# -eq 0 ]
        then
            # No remaining parameters, the string is output as is
            printf -- '%s' "$string"
        else
            # Parameters remain, the translated string is a format string
            printf -- "$string" "$@"
        fi
    }
fi

La création des fichiers de traduction

La création des fichiers de traduction se déroule en plusieurs étapes :

Le but à atteindre est de disposer d’une arborescence de la forme suivante :

.
└── locale
    ├── fr
    │   ├── LC_MESSAGES
    │   │   ├── script1.mo
    │   │   ├── script2.mo
    │   │   └── script3.mo
    │   ├── script1.po
    │   ├── script2.po
    │   └── script3.po
    ├── script1.pot
    ├── script2.pot
    └── script3.pot

Seuls les fichiers .mo seront nécessaires pour l’installation des traductions sur un système en gardant toutefois l’arborescence (donc dans locale/fr/LC_MESSAGES dans le cas du français).

Génération des gabarits de traduction

L’un des avantages d’utiliser la syntaxe standard de Bash est qu’il fournit un outil permettant d’extraire automatiquement les chaînes à traduire d’un script. C’est l’option --dump-po-strings de Bash qui fait tout le travail. La sortie ainsi générée est un fichier .po directement éditable par des outils comme poedit. Une fois la traduction faite, la commande msgfmt permet de convertir le fichier .po en un fichier .mo compréhensible par gettext.

Avec la solution proposée, il n’est plus possible d’utiliser Bash pour extraire les chaînes à traduire d’un script. Pour le remplacer, il faut utiliser le script dumptstring.

Ce script est moins fin que Bash pour détecter les chaînes à traduire mais il devrait remplir son office pour la plupart des utilisations.

Il est utilisé de la façon suivante :

dumptstring script > script.pot

Création des fichiers de traduction pour chaque langue

Pour créer un fichier de traduction pour une langue, il faut créer un répertoire pour chaque langue à traduire. Chaque répertoire est nommé en utilisant le code à deux chiffres ISO 639-1.

Il suffit ensuite de copier le gabarit créé précédemment en supprimant le t.

cp script.pot fr/script.po

Fusion des fichiers de traduction

L’étape précédente est appliquée uniquement lors de la première traduction des chaînes d’un script. Par la suite, à chaque évolution du script, il faut fusionner les nouveaux gabarits avec les traductions existantes.

Pour cela, l’utilitaire msgmerge vient à notre rescousse. Il s’utilise de la façon suivante :

msgmerge --update fr/script.po script.pot

Édition des fichiers de traduction

Pour éditer les fichiers de traduction, il est préférable d’utiliser un logiciel dédié. En mode graphique, le plus connu est poedit. Dans un terminal, il existe po.vim, un greffon pour Vim.

Compilation des fichiers de traduction

Les fichiers de traduction compilés doivent être placés dans le sous-répertoire LC_MESSAGES de chaque répertoire de langue. Pour compiler les fichiers de traduction afin qu’ils soient utilisables par gettext, il faut utiliser la commande msgfmt de la façon suivante :

msgfmt fr/script.po -o fr/LC_MESSAGES/script.mo

Déclaration des fichiers de traduction à utiliser dans un script

Si vous placez votre répertoire locale dans le répertoire /repertoire/de/mon/script, vous devrez le déclarez dans votre script au moyen d’une ligne comme celle-ci :

export TEXTDOMAINDIR=/repertoire/de/mon/script/locale

Si vous placez vos fichiers dans les répertoires standards sous Linux /usr/share/locale, vous n’aurez pas besoin de préciser le répertoire dans votre script.

Code source de l’utilitaire dumptstring

#!/usr/bin/env bash
script="$1"
vtab=$'\v'
nl=$'\n'

# tr '\n' '\v' translate newline character to vertical tab character, it
# allows grep to work with multi line strings
# read -r ask read not to consider backslash a special character
cat "$script" | \
    tr '\n' '\v' | \
    egrep -o '\$\(t "([^\]\\"|[^"])*"' | \
    while read -r line
do
    # Cut the '$(t "' before the string
    line="${line:4}"

    # Replace \n (2 chars string) with \\n (3 chars string)
    line="${line//\\n/\\\\n}"

    # Replace Vertical tabs with \n"\n"
    # (2 chars string, quote, newline, quote)
    multiline="${line//$vtab/\\n\"$nl\"}"

    # Output pot lines
    if [ "$multiline" == "$line" ]
    then
        # This is a single line string
        printf 'msgid %s\n' "$line"
    else
        # This is a multi line string
        printf 'msgid ""\n'
        printf '%s\n' "$multiline"
    fi

    printf 'msgstr ""\n\n'
done