Publi

Scripts multilingües en bash con gettext / Traducciones en scripts


Hace tiempo, empecé un proyecto en el que comparto algunos scripts que utilizo para hacerme la vida un poco más fácil. En esta colección, encontramos algunos scripts que ejecutaremos desde consola, y otros que, suelo tenerlos vinculados a una tecla rápida para ejecutarlos más rápidamente. El caso es que, los mensajes que se muestran en pantalla a través de diálogos suelo ponerlos en español, pero me gusta compartirlos con el mundo, y comprendo que cada uno quiera verlo en su idioma.

Bien, pues tal y como hacíamos en C y en PHP, ahora le toca el turno a nuestros scripts de Bash.

En este ejemplo, incluyo algunos consejos y soluciones a problemas que me he encontrado en el proceso.

Lo primero que se me ocurre

Bueno, lo primero que se nos pasa por la cabeza es meter los mensajes en variables, y, dependiendo del idioma cargar uno u otro archivo de mensajes:
es.sh

1
2
MENSAJE1="MENSAJE 1"
MENSAJE2="MENSAJE 2"

en.sh

1
2
MENSAJE1="MESSAGE 1"
MENSAJE2="MESSAGE 2"

test.sh

1
2
3
4
5
LCN=`locale | grep LC_NAME | cut -d'=' -f2 | cut -d'_' -f1`

. $LCN.sh

echo $MENSAJE1

El problema que tiene esto, es que, queda un poco como cogido con pinzas, parece que en cualquier momento va a fallar y no tiene pinta de ser muy estable. Además, el mantenimiento es incómodo, tienes que saber los nombres de las variables y luego tienes que hacer que se correspondan los mensajes.

Solución con gettext

La solución con gettext es mucho más larga, bueno, vamos a ver, es algo más larga pero mucho más fácil de mantener que la anterior. No tenemos que mantener los nombres de las variables, tenemos en cuenta directamente el mensaje y nos basamos en él.
Eso sí, tenemos que tener un fichero de traducciones aparte, bueno, antes teníamos ficheros sh, ahora tendrán una ruta y nombre específico; y en el resto del post veremos poco a poco cómo construirlo.
Aunque en posts anteriores he puesto alguna forma de hacerlo, aquí voy a empezar desde cero con un método nuevo (para que tengamos variedad, y para no hacer que nadie tenga que visitar otro post para conseguir una parte de la información y luego volver).

Nuestro programa: preparando el entorno

A la hora de crear nuestro programa, a mi me gusta crear un archivo “common.sh” (lo podéis llamar como queráis) situado en el mismo directorio del script, donde almacenamos funciones y variables que no tienen que ver con nuestro script propiamente dicho, son generales, en este caso, relativas a las traducciones (voy paso a paso, si sois impacientes, id al final del post, pero mientras voy poniendo experimentos):

common.sh

1
2
3
4
5
6
7
#!/bin/bash

function __()
{
    ARGS=$@
    echo "$ARGS"
}

multilin.sh

1
2
3
4
5
6
7
#!/bin/bash

SCRIPT_SOURCE=`dirname $BASH_SOURCE[0]`
. $SCRIPT_SOURCE/common.sh

echo $(__ "This is a message for you")
echo $(__ "This is another message for you")

¡Valiente tontería acabo de hacer! Pero quiero que esto vaya tomando forma. He creado una función __ que me servirá para introducir mensajes, y esta función se encargará de traducirlos. Todavía no, ahora mismo hace un echo y listo, ya llegaremos a las traducciones en el siguiente paso.

Por otra parte, en mi script, lo único que tendré que hacer es llamar al common.sh (las dos líneas nos servirán para ubicar el directorio donde se encuentra el archivo (y con ello, ubicar common.sh que está en el mismo directorio), e incluirlo en nuestro script.

Lo siguiente será que, cada vez que quiera escribir un mensaje, debo introducirlo entre $(__ mensaje), es decir, llamaré a la función que está en common que me devolverá la traducción.

Generando el archivo de mensajes

Ahora tenemos que generar un archivo que contenga todos los mensajes que se encuentran en nuestro proyecto. Lo primero es crear un directorio para esos mensajes (podemos usar directamente /usr/share/locale/) que es el directorio por defecto, pero prefiero no utilizar el usuario root para nada, por el momento. Así que crearemos un directorio, hijo del directorio donde se encuentran nuestros script:

mkdir -p locale/es/LC_MESSAGES

El directorio locale lo podemos llamar como queramos, por otro lado el directorio es corresponde al idioma de las traducciones, en este caso español, por ejemplo, Francés será fr_FR, alemán, de, etc. El directorio LC_MESSAGES hay que dejarlo como está, no nos queda otra, porque lo que vamos a modificar serán los mensajes del programa.

Bueno, a partir de ahora, podemos utilizar los comandos que voy a decir, o utilizar poedit, como en otros posts, que es una herramienta gráfica que nos permitirá hacerlo todo cómodamente.

Desde el directorio de nuestros scripts (por ejemplo), corremos el siguiente programa

$ xgettext -o multilin.pot –from-code=UTF-8 –keyword –keyword=__ *.sh

Nos creará un archivo .pot que podemos editar para rellenar los campos que consideremos oportunos (nombre del proyecto, e-mails, versiones, etc).

Entramos en LC_MESSAGES para crear un archivo de idioma español y ejecutamos:

$ msginit -i ../../../multilin.pot -l es_ES.UTF-8 -o multilin.po

A continuación, se habrá creado multilin.po , es un archivo de texto que podemos editar sin miedo, en el que pondremos los textos traducidos

# Spanish translations for PACKAGE package
# Traducciones al español para el paquete PACKAGE.
# Copyright (C) 2015 THE PACKAGE’S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Gaspar Fernández , 2015.
#
msgid “”
msgstr “”
“Project-Id-Version: PACKAGE VERSION\n”
“Report-Msgid-Bugs-To: \n”
“POT-Creation-Date: 2015-04-04 03:53+0200\n”
“PO-Revision-Date: 2015-04-04 04:18+0200\n”
“Last-Translator: Gaspar Fernández \n”
“Language-Team: Spanish\n”
“Language: es\n”
“MIME-Version: 1.0\n”
“Content-Type: text/plain; charset=UTF-8\n”
“Content-Transfer-Encoding: 8bit\n”
“Plural-Forms: nplurals=2; plural=(n != 1);\n”

#: multilin.sh:6
msgid “This is a message for you”
msgstr “Este es un mensaje para ti”

#: multilin.sh:7
msgid “This is another message for you”
msgstr “Este es otro mensaje para ti”

Y una vez generados todos los mensajes, debemos compilar este archivo, para ello, debemos ejecutar:

$ msgfmt -v multilin.po -o multilin.mo

Una modificación importante a common.sh

Nuestro common.sh quedará parecido a este:

1
2
3
4
5
6
7
8
9
#!/bin/bash

export TEXTDOMAINDIR=$SCRIPT_SOURCE/locale

function __()
{
    ARGS=$1
    gettext "multilin" "$ARGS"
}

En lugar de hacer echo directamente, he llamado a gettext con el dominio multilin (que es el que estamos usando para nuestros scripts.

En este momento cuando ejecutemos multilin.sh, lo que mostrará por pantalla será:

$ ./multilin.sh
Este es un mensaje para ti
Este es otro mensaje para ti

Bueno, ya tenemos la traducción automática, ya sólo nos queda mejorar esto un poco más.

Mejoras

common.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
GETTEXT=`which gettext`
DOMAIN="multilin"

if [ -z "`ls -R /usr/share/locale | grep $DOMAIN.mo`" ]; then
    export TEXTDOMAINDIR=$SCRIPT_SOURCE/locale
fi

function __()
{
    ARGS=$@

    if [ -z "$GETTEXT" ]; then
    printf "$ARGS"
    else
    LINE="$1"
    shift
    printf "$(gettext "$DOMAIN" "$LINE")" $@
    fi
}

Así quedaría mi common.sh, en este caso, miramos si tenemos la herramienta gettext disponible, porque es posible que el usuario final no la tiene instalada, y no podemos ponernos a mostrar fallos a cada mensaje en pantalla.

Lo siguiente es saber si el usuario ha instalado los idiomas de nuestra aplicación a nivel de sistema (si es así, esos archivos estarán en /usr/share/locale y usaremos estos. Si no, buscaremos en el directorio de nuestro script (SCRIPT_SOURCE lo definimos en el script multilin.sh).

Ahora, en la función __(), miramos si gettext está disponible, lo utilizamos para mostrar las traducciones (en lugar de usar DOMAIN, podemos utilizar TEXTDOMAIN, que es una variable de entorno de gettext y no tenemos que pasarle nada más, yo prefiero dejarlo así por si en mi proyecto incorporo varios dominios de traducciones.

Podéis ver que en lugar de echo, he usado printf. Y es para poder utilizar cadenas de formato con %s. Así un programa como:

1
2
3
4
5
6
#!/bin/bash

SCRIPT_SOURCE=`dirname $BASH_SOURCE[0]`
. $SCRIPT_SOURCE/common.sh

echo $(__ "You have %d apples and %d bananas" 12 30)

Daría una salida como:

You have 12 apples and 30 bananas

Y si en multilin.po lo traducimos como:

#: multilin.sh:8
msgid “You have %d apples and %d bananas”
msgstr “Tienes %d manzanas y %d plátanos”

Tendremos una salida como:

Tienes 12 manzanas y 30 plátanos

Lo que nos ayuda tremendamente a incrustar valores dentro de nuestras traducciones.

Instalar traducciones en el sistema

Si queremos instalar las traducciones a nivel de sistema, sólo tenemos que hacer esto:

$ sudo install locale/es/LC_MESSAGES/multilin.mo /usr/share/locale/es/LC_MESSAGES/multilin.mo

Así con cada idioma.

Actualización 30/11/2016 : Se añadió export antes de TEXTDOMAINDIR. Anteriormente funcionaba sin esto, pero ahora es obligatorio.

También podría interesarte....

Leave a Reply