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>
10 comentarios
-
Kamara
25 de Septiembre de 2008, 23:47
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, 15:52
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, 11:51
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, 16:27
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, 17:23
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, 17:23
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, 10:31
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, 21:00
Puede ser que este método sea el que usa Wordpress?
-
Jaume Font
19 de Enero de 2010, 12:26
Lo es, Joaquín, lo es...
-
Joaquin Leonel Robles
11 de Febrero de 2010, 17:23
efectivamente, es el metodo de wordpress...
igualmente, la utilización de un framework como Zend Framework facilita muchisimo la tarea de internacionalizar un sitio...