domingo, 7 de septiembre de 2008

Internacionalización en PHP usando gettext

En algunas oportunidades se hace necesario que nuestro sitio web tenga versiones en múltiples idiomas.

El concepto de internacionalización o i18n en sitios web consiste no sólo en mostrar los contenidos en el idioma del usuario, sino que también implica ajustarse a sus configuraciones regionales tal como: formatos de fechas, formatos de números y reglas de ordenamiento alfabético. En la actualidad la mayoría de las base de datos y lenguajes de programación modernos tienen soporte para i18n.

En este artículo me enfocaré en el problema de mostrar el contenido de un sitio web en múltiples idiomas, para ello les presentaré mi solución favorita basada en gettext, que es la biblioteca GNU para internacionalización. Los ejemplos que aquí daré están basados en PHP, pero son totalmente aplicables a otros lenguajes de programación como por ejemplo C.


Codificación
Como primera medida en el proceso de internacionalización, nuestros sistemas debieran ser desarrollados utilizando codificación UTF-8 en vez de la tradicional ISO-8859-1 también conocida como LATIN1.

La codificación UTF-8 prácticamente soporta la mayoría de los simbolos (y caracteres) de escritura a nivel mundial. La codificación LATIN1 es mucho más reducida en la cantidad de simbolos (o caracteres) de escritura, pero es suficiente en caso que queramos mantener nuestro sitio en español e inglés.

En caso que optemos por desarrollar un sistema con codificación LATIN1 o si tenemos que dar soporte multi-idioma a sistemas heredados que ya fueron desarrollados con LATIN1, se deberán realizar algunos pasos adicionales que iré comentando en los ejemplos.


Módulo gettext
Cuando generamos contenidos hacia el web, usualmente extraemos la información de dos fuentes: base de datos y textos estáticos.

La traducción del contenido de la base de datos se debe realizar caso a caso de acuerdo a las características de cada sistema y no es cuerto en este artículo.

La traducción de los textos estáticos se realizará usando gettext que usualmente ya está habilitada en sistemas LINUX. En Windows basta con editar el archivo de configuración de PHP y descomentar (o escribir) la siguiente linea:
extension=php_gettext.dll

Adicionalmente, para generar las traducciones es necesario tener instalado gettext, el cual normalmente ya viene instalado en LINUX y puede ser descargado para Windows en la página del proyecto GNU para Windows.



Manos a la obra
Usualmente en nuestro código tenemos instrucciones como las siguientes:
echo "Hola mundo";
print "Hola mundo";
printf("Hola %s", $nombre);

Todas esas instrucciones deberán ser modificadas para ser procesadas por la función gettext() o mejor aún por un alias de esa función llamado simplemente _(), con lo que tendremos los siguientes reemplazos:
echo _("Hola mundo");
print _("Hola mundo");
printf(_("Hola %s"), $nombre);

Esta función no realiza ningún cambio en los textos. Deberemos aplicar estos cambios a todo el contenido de nuestro sistema lo que va a implicar cambiar el estilo de programación, como por ejemplo en los siguientes ejemplos:
echo "Bienvenido $nombre";

echo "
<h1>Página principal</h1>
<p>Bienvenido $nombre</p>
";

hacemos uso de una de las características más amigables de PHP que consiste en incrustar variables directamente en un string. Para trabajar con gettext, deberemos reescribir este trozo de código. Como recomendación, les suguiero utilizar las funciones printf() y sprintf():
printf(_("Bienvenido %s"), $nombre);

printf("<h1>%s</h1><p>%s $nombre</p>",
_("Página principal"), _("Bienvenido"));



Generar traducciones
Una vez que se hayan intervenido todos los textos del sistema, se deberá ejecutar un programa que escanea todo el código fuente buscando las intervenciones que realizamos, esto crea un archivo de traducciones donde se consolidan todos los textos encontrados, dicho archivo de texto deberá ser traducido (existen programas que ayudan a hacerlo) y luego se generará un archivo binario que contiene todas las traducciones.

El primer paso es escanear todos los archivos involcados, por ejemplo para buscar todos los archivos con extensión .php y .inc se ejecuta el siguiente comando:
find . -name \*.php -o -name \*.inc > messages.txt

A continuación realizamos el proceso de escaneo de textos a traducir mediante el comando xgettext:
xgettext --language=PHP --from-code=ISO-8859-1 -o messages.po -f messages.txt

Por defecto, este comando trabaja en UTF-8, por lo que si nuestro código está en LATIN1 (ISO-8859-1) debemos utilizar el parámetro --from-code=ISO-8859-1.

La salida de este comando nos generará un archivo de texto que deberá ser traducido, por ejemplo al inglés. La edición puede ser realizada manualmente o por medio de algún programa como por ejemplo Poedit.

Una vez que tengamos nuestro archivo de traducción, deberemos convertirlo (compilarlo) a formato .mo mediante el comando msgfmt:
msgfmt messages.po -o messages.mo

Y con esto ya hemos generado nuestro archivo de traducción. Eventualmente podemos generar muchos archivos de idiomas. Ahora sólo nos falta decirle a PHP que reconozca estos archivos de idioma.


Habilitar traducciones
Debemos hacerle saber al sistem dónde encontrar nuestro archivo .mo. Los sistemas operativos tienen directorios predefinidos para estos fines, sin embargo, para evitar problemas de permisos y privilegios, prefiero configurar el sistema para indicarle manualmente dónde buscarlos.

Como en este ejemplo hemos traducido los textos al idioma inglés, pondremos el archivo de traducción en la siguiente ubicación:
/tmp/locale/en_US/LC_MESSAGES/messages.mo

Para habilitar la traducción de los textos utilizaremos el siguiente código donde la variable $locale define el idioma que será utilizado que en este caso será en_US:
$locale = 'en_US';
if (!defined('LC_MESSAGES')) define('LC_MESSAGES', 6);
setlocale(LC_MESSAGES, $locale);
bindtextdomain('facturanet', '/tmp/locale');
textdomain('messages');

En caso de que $locale indique un idioma no definido, el sistema mostrará los textos en su idioma original. Por supuesto que deberás cambiar el directorio /tmp/locale por uno más apropiado.

Los archivos binarios .mo son guardados en cache, por lo que luego de actualizar el archivo se recomienda reiniciar el servidor web.


Selección de idioma
Debido a que la variable $locale es la que determina el idioma que se presentará al usuario, les recomiendo que sea una variable de sesión de manera que el sistema recuerde el idioma del usuario entre cada página. Se debería crear una página PHP en que el usuario pueda cambiar de idioma.

Para mejorar la experiencia del usuario, es posible autodetectar el idioma del usuario consultando la variable del servidor $_SERVER['HTTP_ACCEPT_LANGUAGE'].



Otras características
Hay otras características que no alcanzaré a revisar ahora y que permiten un mejor trabajo con la boblioteca gettext:

  • En caso de que modifiquemos nuestro sistema y tengamos que realizar nuevas traducciones, no es necesario repetir todo el proceso, gettext provee comandos para incorporar nuevas palabras a un archivo ya existente mediante msgmerge.
  • Además de la función _() existe otras funciones gettext para realizar traducciones más complejas, como por ejemplo diferenciación de singuales y plurales.