Construir aplicaciones multilenguaje con gettext

Publicado el 15 de octubre de 2005 - Han habido 10926 lecturas de este escrito

Hace algún tiempo tuve la necesidad de crear una aplicación en varios idiomas. La primera manera que se me ocurrió de hacerlo es la de toda la vida, la cutre: hacer una matriz con todas las cadenas que debían ser traducidas e incluir una uno u otro archivo con esta matriz dependiendo del idioma elegido.

Esto, como se puede suponer, no es nada recomendable, ni que decir de copiar y pegar la aplicación en cuestión en varios directorios e ir traduciendo (como más de uno me recomendó en ese momento). Los problemas a la hora de actualizar el contenido estático que estuviese en varios idiomas serían muchísimos.

Para suplir esta tarea tenemos una biblioteca del grupo GNU: gettext, está programada en C, aunque está soportada por PHP a partir de la versión 3.0.7 siempre que se disponga de la extensión disponible (puedes consultar si la tienes operativa con una llamada a la función phpinfo()). Esta librería es muy popular y ha sido usada por proyectos tan prestigiosos y ampliamente usados como GNOME o XMMS. Gracias a que es de código abierto es multiplataforma: está disponible tanto como para sistemas Microsoft Windows como para sistemas UNIX y derivados.

Este escrito presupone conocimientos de nivel medio-alto de PHP, obviamente aquí no se va a enseñar a programar en este lenguaje. También se espera un dominio básico del uso de la terminal de tu sistema.

La motivación que me ha llevado a escribir esto es que he encontrado pocos o ningún recurso sobre esta librería aplicada a PHP en castellano, si tienes algún enlace relacionado con el tema siempre se acepta y agradece que lo compartas.

Se tratarán y explicarán los siguientes temas:

Una recomendación importante (y que te puede ahorrar quebraderos de cabeza) es que uses la codificación UTF-8 tanto para los archivos de tu aplicación como para los usados por la librería. A lo largo de este escrito se va a decir a los comandos que tengamos que usar para trabajar con gettext que usen esta codificación en los ficheros de entrada y salida, si decides no usarla simplemente bórralos.

Funcionamiento

Entender como funciona la librería puede ser un poco lioso al principio, aunque una vez lo sepas no te va a costar demasiado programar con ella.

Básicamente lo que hace es obtener los datos que se le piden (mediante funciones como gettext() o ngettext()) de un archivo binario de tipo MO, que compilaremos a partir de un archivo PO con el ejecutable msgfmt. Es normal que no te hayas enterado de mucho, pero al menos debes diferenciar entre esos dos tipos de archivo, por el momento.

Instalación

La instalación consta de dos partes: primero la librería y sus ejecutables y después la extensión de PHP que la implementa. Que haya que seguir unos u otros pasos depende en gran parte del sistema operativo que use la máquina en la que vamos a probar el funcionamiento de gettext.

Todos los archivos que has de descargar directamente relacionados con gettext sería recomendable que los obtuvieses de su página oficial, tienes a tu disposición una lista de servidores desde donde poder bajarlos. Los paquetes comprimidos específicos para sistemas Microsoft Windows tienen en el nombre la cadena woe32. Descarga siempre la última versión disponible que cumpla tus requisitos.

Ten cuenta que algunos servidores están más actualizados que otros. Uno que me ha dado buenos resultados ha sido el de la Universidad de California del Sur.

Windows

Descarga estos archivos y descomprímelos todos en el mismo directorio, preferiblemente C:gettext para un fácil acceso:

El archivo DLL de la extensión viene por defecto en el archivo comprimido con el que instalamos PHP, podemos mirar si ya lo tenemos en el directorio ext/ o extensions/ dentro del de PHP.

¡Regalito! Esto es opcional, pero nos puede ser útil: para que se puedan usar las gettext-tools y el ejecutable de PHP desde cualquier parte del sistema sin que haya que especificar la ruta completa debemos tener el directorio en el que los tenemos ubicados (en este caso C:gettext para las utilidades de gettext, el directorio en el que está PHP me temo que deberás saberlo tú) en el PATH, podemos añadirlo con el siguiente comando:

path=%path%;c:gettext;c:PHP

Activar la extensión gettext de PHP es realmente sencillo: busca la siguiente línea en tu archivo php.ini (posiblemente ubicado en C:php o C:Windows) y quita el primer caracter (el punto y coma):

;extension=php_gettext.dll

Tras esto ya deberías tener funcionando la extensión, puedes comprobarlo con el comando php -m (debes realizarlo desde el directorio donde tengas ubicado el binario de PHP):

$ php -m
[PHP Modules]
bcmath
calendar
ctype
curl
date
dom
gd
gettext

GNU/Linux

En este caso podemos usar perfectamente el sistema de paquetes de nuestra distribución: apt-get en Debian, urpmi en Mandriva, etc. Aquí se explicará el método manual de compilación, para que, en caso de que quieras calentarte un poco la cabeza, puedas hacerlo.

Descarga este archivo (ponlo en el directorio /usr/src/ para tenerlo accesible en cualquier momento):

Abre una terminal y teclea los siguientes comandos para moverte al al directorio /usr/src/ y descomprimir el archivo:

$ cd /usr/src/
$ tar xvfz gettext-0.14.5.tar.gz

Ahora procede a la compilación. Si lo has hecho antes no debe causarte ningún problema que no puedas resolver rápidamente. Teclea la serie de comandos siguiente:

$ cd gettext-0.14.5
$ ./configure
$ make

Ahora identifícate como superusuario (con el comando su) y termina de instalar:

# make install

Prueba la instalación de los binarios con el comando gettext -V, si todo ha ido bien mostrará un mensaje parecido a este:

$ gettext -V
gettext (GNU gettext-runtime) 0.14.5
Copyright (C) 1995-1997, 2000-2005 Free Software Foundation, Inc.
Esto es software libre; vea el código fuente para las condiciones de copia.
No hay NINGUNA garantía; ni siquiera de COMERCIABILIDAD o IDONEIDAD PARA UN
FIN DETERMINADO.

Compilar PHP puede ser ligeramente más complicado, aunque muy útil si quieres añadir soporte a cualquier librería que no esté activada por defecto (la de MySQL por ejemplo no está activada). Si no lo has hecho antes te recomiendo que te leas la sección del manual sobre instalación en sistemas UNIX.

Solo tendrás que añadir la opción --with-gettext al ./configure para tener la librería activa y después compilar (siguiendo el mismo proceso que hemos visto antes).

Ten en cuenta a la hora de usar locales que puede pasar que el sistema no te los reconozca, esto puede deberse a varios motivos, el más común es que no los tengas generados. Para comprobar los que tienes generados puedes ejecutar el siguiente comando:

$ locale -a

Si no los tienes generados deberás reconfigurar el paquete que te los instaló para añadir más.

Ahora se supone que debes tener tanto gettext y sus utilidades como la extensión de PHP funcionando en tu sistema, ya podemos empezar con lo "serio".

Uso en PHP

El idioma se selecciona ajustando la localización con la función setlocale(), aunque en ocasiones también han de ajustarse las variables de entorno LANG o LANGUAGE a través de putenv(). El código del locale que uses ha de especificar idioma y país según los estándares ISO-639 e ISO-3166, por ejemplo: es_ES, es_MX, en_GB o en_US. Esto no quiere decir que no puedas usar un código que no especifique exactamente el país donde se ejecute la aplicación, esto sería ridículo. Puedes perfectamente mostrar mensajes en chino estando en Albacete.

AVISO: En Windows no se respeta este tipo de nomenclatura para los locale, tienen una manera un tanto especial de llamarlos. He dejado un enlace en el último apartado de este escrito para que puedas ver como se ajustan en este tipo de sistemas.

gettext se basa en cadenas identificadoras para los mensajes. Tienes que escribir tus archivos normalmente, pero cuando tengas que mostrar un texto lo haces pasándosele el texto identificador que quieras usar a la función (o preferiblemente a su alias _, que nos ahorrará bastante código).

Un ejemplo de archivo para ser interpretado puede ser el siguiente:

<?php
setlocale
(LC_ALL'es_ES');
bindtextdomain('messages''./locale');
echo 
_('Hello') . "n";
?>

Es bastante simple entender estas líneas:

Si no se encuentra la localización correcta o hay algún problema a la hora de ajustarla el mensaje mostrado será el identificador. Ten en cuenta que, si los archivos van a ser traducidos por varias personas, es mejor que los textos de origen (identificadores y mensajes) estén en inglés, ya que es un idioma universal.

Estructura de directorios

La librería busca los archivos compilados con las definiciones de lenguaje en un directorio y sus subdirectorios. Estos directorios siguen un patrón concreto, que nos ayudará a tener organizados los archivos de lenguaje.

El nombre del directorio raíz será el que especifiquemos en la llamada a la función bindtextdomain(), en el ejemplo de la sección superior (y a lo largo de todo este escrito) su nombre es locale. En ese directorio tenemos que crear uno por cada idioma que queramos utilizar en nuestra aplicación, su nombre debe ser el código de locale, arriba se ha explicado como saber cual tenemos que usar para cada país e idioma. Estos directorios a su vez contendrán otro directorio llamado LC_MESSAGES.

Este último párrafo puede parecer un poco lioso la primera vez, pero puedes leerlo varias veces hasta que lo entiendas. Para ayudarte a comprender lo explicado puedes guiarte por el siguiente árbol:

$ tree
.
|-- aplicacion.php
`-- locale
    |-- de_DE
    |   `-- LC_MESSAGES
    |-- en_GB
    |   `-- LC_MESSAGES
    `-- es_ES
        `-- LC_MESSAGES

Dentro de los directorios LC_MESSAGES tendremos alojados los ficheros compilados.

Archivos utilizados por gettext

Los archivos PO son los que traduciremos, pues en ellos estarán los identificadores y sus cadenas relacionadas, su estructura es muy simple y puedes poner comentarios iniciando la línea con una almohadilla (#). Aquí tienes una muestra del formato a seguir:

#esto es un comentario
msgid "identificador"
msgstr "cadena en el lenguaje que prefiramos como base"

Por lo general nosotros, como programadores, no tendremos que crear (directamente) estos archivos con todas los identificadores que usemos en nuestra aplicación, de eso se encargará una de las utilidades que hemos instalado, xgettext.

Este programa toma como argumento una cantidad variable de opciones y un patrón de nombres de archivo (puede ser un archivo concreto, por supuesto). Irá leyendo los archivos que coincidan en ese patrón, buscando llamadas a las funciones de la librería y escribiendo en un archivo los identificadores, esto nos ahorra un montón de trabajo. Las opciones de este ejecutable son muchas, te recomiendo que mires la ayuda (con el comando xgettext -h) para elegir las que más te convengan, personalmente uso xgettext --omit-header --no-location --from-code=UTF-8, que genera un archivo bastante limpio, aunque puede resultar lioso.

Una vez llamemos a ese binario (si no hemos elegido otro método o nombre de archivo de salida) nos generará un archivo con el nombre messages.po con la sintaxis que se ha explicado. Este nuevo fichero debes abrirlo con tu editor de texto preferido (el Microsoft Word no es recomendable para este tipo de cosas, ¡recuérdalo!) e ir escribiendo las cadenas correspondientes a cada identificador, ya que, obviamente, el binario no tiene superinteligencia ni nada por el estilo, debes ser tú quien añada sus definiciones.

Cuando completemos esta tarea (haced copia de seguridad cada poco tiempo) haz una copia de seguridad (si, otra más, pica bastante perder tiempo de trabajo) y pasemos a una etapa importante: la transformación en archivo binario del messages.po.

Una de las aplicaciones que tenemos nos sirve para este fin, su nombre es msgfmt, puedes consultar su manual o un resumen de la ayuda (con msgfmt -h, como casi siempre) para obtener una información extendida sobre ella. Básicamente le tenemos que pasar el nombre del archivo en formato PO y punto, así de simple:

$ msgfmt messages.po

Esto nos generará un archivo messages.mo. Tendremos que repetir esta acción por cada archivo de lenguaje que tengamos (una vez por idioma). Podemos simplificar este paso haciendo una miniaplicación en PHP que nos recorra los directorios en busca de archivos messages.po y llame el binario msgfmt, por eso (entre otras cosas) es importante que tengamos los archivos .po organizados en el directorio de su correspondiente lenguaje.

Actualización de traducciones

Partimos de la base de que un programa se actualiza y evoluciona, por tanto, en algún momento, sus textos también van a cambiar. Bien, una vez entendido esto puedes pensar (o no)… yo tengo mis archivos ya traducidos en varios idiomas, si quiero añadir una nueva cadena para que sea traducida… ¿tengo que cargarme todos los archivos y volver a empezar a traducir?. Obviamente no, alguien pensó en eso antes que usted.

En teoría tendríamos nuestro messages.po ya traducido, y las aplicaciones tendrían cadenas faltantes o sobrantes (pues se habrían añadido/borrado, según conveniese), pues bien, atento a lo siguiente.

Haz una copia de respaldo del archivo de traducciones antiguo (siempre), a continuación ejecuta xgettext como hemos aprendido antes para generar un archivo .po a partir de las cadenas nuevas. El recién generado archivo lo mezclaremos (como si de un café con leche se tratase) con el antiguo, para esto usaremos msgmerge.

La sintaxis de este comando es muy sencilla, mirando la ayuda (msgmerge -h) nos podremos guiar perfectamente. Tienes que darle como parámetros (además de las opciones que necesites, por supuesto) el nombre del archivo .po que contiene las traducciones y el que acabas de crear, que contendría los más actualizados identificadores. Ojo con el orden en el que pones el nombre de los archivos, debes ponerlos en el orden mencionado, pues en caso de ponerlos al revés puede irse al garete tu traducción.

Enlaces de interés