Internacionalización con PHP y gettext

en php gettext internacionalización publicado el 19 de febrero de 2008

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("messages", "locale");
textdomain("messages");

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("messages", "locale");
textdomain("messages");

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>

12 comentarios

  • Kamara

    25 de septiembre de 2008

    Mira tu a quién me encuentro pro aquí!, vaya sorpresón!!! uno buscando temas de internacionalización y le sale al colega de la vuelta la esquina!!! Bueno, mi enhorabuena, esto (y en castellano para su lectura más rapida y placentera) ayuda bastante! te doy 100 puntos!!! jeje, 1 abrazo y mis mil gracias maestro manu

  • kamara

    27 de septiembre de 2008

    no se si te peleasete con ello, yo llevo toda la mañana incapaz de pasar del paso de la compilación de los .mo. he creado mi lista de archivos y la sentencia para crearlos es exactamente la misma, solo que trabajo todo en utf-8, es esta: $ xgettext -f sources.txt -L PHP -o messages.po --from-code=utf-8 me genera perfectamente el messages.po, pero cuando hago lo siguiente: $ msgfmt -o messages.mo messages.po ,obtengo el siguiente error: atención: El conjunto de caracteres "CHARSET" no es un nombre de codificación portátil. La conversión de mensajes al conjunto de caracteres del usuario podría no funcionar. He buscado info pero no encontré mucha, y he probado con las combinaciones "utf-8", "UTF-8" "utf8", "UTF8", "iso-8859-1" en la primera sentencia, te suena de algo esto?... estoy a un paso... SOLO A UN PASO!!!! pero con un dolor de cabeza terrible. saludo,

  • kamara

    29 de septiembre de 2008

    aquí de nuevo, correcto lo de los parámetros (pasa que cuando aparece algún error o warning ya dudas hasta de que madre me trajo al mundo), thanks. anoche dándome de cabezazos logré hacer funcionar todo, tube más problemillas pero eran referentes ya al entorno que había montado (ubuntu, php5, ...) me faltaba instalar y actualizar los locales del propio sistema operativo (ca_ES.utf8 no estaba en el sistema, había que añadirlo a mano). y con esto y un bizcocho, me quedé de nuevo ayer hasta las ocho!, ahora estoy que me caigo, pero muy happy de haberlo conseguido. la verdad que sin tu artículo no sabía ni como meterle mano, me ha servido de bastante ayuda. gracias maestro!

  • kamara

    27 de septiembre de 2008

    Bueno, lo logré, modifiqué el charset del php.ini en el mbstring.output y iconv y listo, ahora funciona!

  • kamara

    27 de septiembre de 2008

    una pregunta tonta, en las lineas: bindtextdomain("message", "locale"); textdomain("message"); Los primeros parámetros "message" ¿corresponde al nombre del archivo que generamos? message.po y message.mo ?

  • kamara

    27 de septiembre de 2008

    una pregunta tonta, en las lineas: bindtextdomain("message", "locale"); textdomain("message"); Los primeros parámetros "message" ¿corresponde al nombre del archivo que generamos? message.po y message.mo ?

  • Albert Lanchas

    29 de septiembre de 2008

    Manu, sí que me pasó lo que comentas del warning. La razón que yo encontré es que cuando generas el catálogo con todos los literales a internacionalizar, aunque le pases el charset por la línea de comando, en la cabecera del po nunca pone el charset. Si lo editas manualmente ya no muestra ese warning. Sobre los parámetros de las funciones bindtextdomain y textdomain, messages es el nombre de los ficheros po sin la extensión

  • Joaquin Leonel Robles

    23 de noviembre de 2009

    Puede ser que este método sea el que usa Wordpress?

  • Jaume Font

    19 de enero de 2010

    Lo es, Joaquín, lo es...

  • Joaquin Leonel Robles

    11 de febrero de 2010

    efectivamente, es el metodo de wordpress... igualmente, la utilización de un framework como Zend Framework facilita muchisimo la tarea de internacionalizar un sitio...

  • Raul

    18 de enero de 2011

    Buenas, Muy interesante el articulo. Estoy preparando un CMS multiidiomas y estoy usando el mismo sistema que describes en la web. Hasta que he metido el catalán todo funciona perfecto pero ahora cuando se ve en catalán empiezan problemas de accentos. Deseperante es poco. No encuentro la combninacioin perefcta. ¿Te suena el problema?

  • programarivm

    20 de enero de 2012

    Excelente artículo amigos. A continuación adjunto un post que explica cómo gestionar la i18n de forma muy sencilla sin necesidad de gettext. Ánimo y saludos. http://programarivm.com/2012/01/internacionaliza-i18n-tus-aplicaciones-php-de-tamano-pequeno-o-mediano-de-la-forma-mas-rapida-y-sencilla/

¿Pensando en contratar mis servicios?

Lee esto

Algunos apuntes que te pueden resultar útiles:

  • Soy programador web y aunque me encantaría diseñar, no entra dentro de mis competencias. Para poder desarrollar una página web necesito el diseño gráfico final. Si no conoces a ningún diseñador te puedo recomendar alguno con el que haya colaborado y su trabajo me haya parecido bueno.
  • No soy especialista SEO, aunque uso reglas básicas y sentido común a la hora de estructurar el contenido lo que se traduce en una mejora en el posicionamiento.
  • Si es posible envíame toda la información que creas que necesite para presentarte un presupuesto. "Programar una web como esta" no es suficiente, no podré evaluar el coste porque no sabré que funcionalidades tiene. Necesito que me las especifiques.
  • Si el proyecto es de gran envergadura, seguramente que te pida cuál es tu presupuesto. La razón es que si se aleja mucho de lo que a primera vista considero puede costar, te contestaré enseguida que no es un trabajo que pueda hacer.
  • Si existe una fecha de entrega del proyecto, comunícamela. En función de mi disponibilidad te contestaré si es un trabajo que pueda hacer o no.

Para contactarme puedes usar el formulario de contacto.