Internacionalización con PHP y gettext

gettext es una librería para internacionalizar aplicaciones, es decir para construir aplicaciones que soporten varios idiomas. PHP a partir de la versión 4 viene con una extensión para poder utilizarla pero debe de estar habilitada en el servidor en que se vaya a usar. Ejecutando phpinfo() se puede consultar esta información.

Ventajas

  • acceso rápido porque los ficheros de mensajes traducidos están compilados
  • facilita mucho el trabajo cuando hay varias personas traduciendo los textos
  • herramientas que se ejecutan desde la línea de comandos que ahorran mucho tiempo

Inconvenientes

  • puede ser un poco complejo al principio
  • no es thread safe, lo que significa que si el servidor es multithread, como IIS o Apache sobre Windows, no va a funcionar correctamente ya que gettext depende del proceso global locale. Básicamente, si desde un thread se cambia el locale, afecta a todos los otros, con lo cual se podrían mostrar los mensajes en un idioma que no es el esperado.
    Está previsto que en el futuro, posiblemente la versión 0.15, gettext sea thread safe.

Instalación

En esta página hay una lista de servidores ftp donde se puede descargar.

Funcionamiento

getext trabaja con dos tipos de ficheros, los PO, ficheros de texto con las traducciones y los MO, ficheros binarios obtenidos después de compilar los PO que son los utilizados por PHP para mostrar los literales traducidos. Se necesitan tantos ficheros PO como idiomas se vayan a soportar.

Estructura de carpetas

gettext necesita una estructura de carpeta concreta. A partir de una carpeta raíz, que por ejemplo se puede llamar locale, hay que crear tantas carpetas como idiomas se vaya a soportar y dentro de cada una de ellas tiene que existir una carpeta con el nombre LC_MESSAGES.

La carpeta del idioma debe seguir el formato ii_pp donde ii es un código ISO-639 de dos letras que representa el idioma y pp es un código ISO-3166 de dos letras que representa el país. Por ejemplo en_GB significa inglés hablado en el Reino Unido mientras que en_US significa inglés hablado en Norteamérica.

Inicialización

Antes de hacer una llamada a la función gettext de PHP, se debe definir una serie de parámetros para que gettext sepa con que idioma se quiere trabajar. Estos parámetros son el idioma con el que se quiere trabajar, donde están ubicados y como se llaman los ficheros PO. Solamente es necesario ejecutarlo una vez para cada script.

// Define el idioma a castellano
putenv("LANG=es_ES");
setlocale(LC_ALL, "es_ES");

// Define la ubicación de los ficheros de traducción
bindtextdomain("message", "locale");
textdomain("message");

Este trozo de código se puede hacer un poco más flexible y definir el idioma en función de una variable. Así que si suponemos que el idioma con el que se tiene que visualizar la página llega por la url con el nombre de idioma y en la web se da soporte a los idiomas castellano, catalán e inglés, quedaría así:

switch ($_GET["idioma"]) {
   case 1:
      $idioma = 'es_ES';
      break;
   case 2:
      $idioma = 'ca_ES';
      break;
   case 3:
      $idioma = 'en_GB';
      break;
}
// Define el idioma
putenv("LANG=$idioma");
setlocale(LC_ALL, $idioma);

// Define la ubicación de los ficheros de traducción
bindtextdomain("message", "locale");
textdomain("message");

Mostrar literales internacionalizados

Para mostrar literales internacionalizados una vez que se ha ejecutado la inicialización se usa la función gettext.

<h1><?php echo gettext("Servicios") ?></h1>

En este caso, Servicios actúa como identificador de literal.

Generación del catálogo de identificadores de literales

Para generar el catálogo de identificadores de literales, gettext viene con una herramienta muy útil, xgettext. Recorre una lista de ficheros que se le indica buscando todas las llamadas a la función gettext y crea un fichero de texto con todos los identificadores. Se llama desde la línea de comandos así:

xgettext -f sources.txt -L PHP -o messages.po --from-code=iso-8859-1

que en castellano significa "lee del fichero sources.txt la lista de ficheros que quiero que recorras buscando llamadas a gettext, que por cierto son PHP y están codificados en ISO-8859-1, y genera un fichero que se llame messages.po".

Traducción de literales

La primera vez que se va a hacer las traducciones hay que copiar el catálogo en cada una de las carpetas de idioma que se hayan creado como se ha visto en estructura de carpetas.

Un fichero messages.po se parece a algo así:

msgid "Servicios"
msgstr ""

Dentro de las comillas después de msgstr es donde debe escribirse la traducción. Si es igual que el identificador no hace falta escribir nada así que el messages.po para el castellano no debería tener traducciones a menos que haya algún literal que no corresponde con su identificador.

Compilación de las traducciones

Para generar los ficheros binarios PO, hay que utilizar la herramienta msgfmt desde la línea de comandos de la forma siguiente:

msgfmt -o messages.mo messages.po

En vez de ir carpeta por carpeta ejecutando este comando, es más cómodo tener un fichero batch y que compile todos los mensajes de golpe.

msgfmt -o ca_ES\LC_MESSAGES\messages.mo ca_ES\LC_MESSAGES\messages.po
msgfmt -o en_GB\LC_MESSAGES\messages.mo en_GB\LC_MESSAGES\messages.po
msgfmt -o es_ES\LC_MESSAGES\messages.mo es_ES\LC_MESSAGES\messages.po

Actualización de las traducciones

gettext viene con una herramienta que permite actualizar los ficheros PO con los identificadores de mensaje no encontrados, es decir los creados nuevos desde la última vez que se modificó. Así que después de haber generado el catálogo se debe ejecutar msgmerge que como las demás se ejecuta desde la línea de comandos. En este caso también es más cómodo tener un fichero batch que lo haga para todos los idiomas de golpe.

msgmerge ca_ES\LC_MESSAGES\messages.po ca_ES\LC_MESSAGES\messages.po -U
msgmerge en_GB\LC_MESSAGES\messages.po en_GB\LC_MESSAGES\messages.po -U
msgmerge es_ES\LC_MESSAGES\messages.po es_ES\LC_MESSAGES\messages.po -U

La opción -U le indica a msgmerge que debe actualizar el fichero de traducciones.

Abstracción de la lógica en una clase

Una buena práctica es abstraer qué mecanismo se utiliza para internacionalizar, de esta manera la aplicación no sabe que sistema se está utilizando. La ventaja es que si por alguna razón se decidiese cambiar de sistema, por ejemplo porque finalmente el cliente quiere modificar los mensaje y le es más cómodo hacerlo en un fichero XML o porque hay que cambiar de servidor y en el nuevo no está habilitado gettext, la aplicación no se verá alterada, es decir no habrá que ir sustituyendo todas las llamadas a la función gettext por otras nuevas, simplemente habrá que modificar la lógica de la clase.

Para esto utilizo una clase de instancia única o singleton.

class i18n {
   function &_instance() {
      static $instance = null;
      if (is_null($instance)) {
         $instance = new i18n();
      }

      return $instance;
      }

   function init() {
      switch ($_GET['idioma']) {
         case 'es':
            $idioma = 'es_ES';
            break;
         case 'ca':
            $idioma = 'ca_ES';
            break;
         case 'en':
            $idioma = 'en_GB';
            break;
      }

      // Define el idioma
      putenv("LANG=$idioma");
      setlocale(LC_ALL, $idioma);

      // Define la ubicación de los ficheros de traducción
      bindtextdomain("messages", "locale");
      textdomain("messages");

      $instance = &i18n::_instance();

      return $instance;
   }

   function getLabel($label) {
      return gettext("$label");
   }
}

En este caso la inicialización se haría de la siguiente forma:

$i18n = i18n::init();

Y para mostrar los literales internacionalizados:

<h1><?php echo $i18n->getLabel("Servicios") ?></h1>

Sin comentarios

Publicar comentario