NOTA PREVIA: Permitidme un salto intempestivo en la serie de artículos The WP Functions Library, pero os explico el porqué. Veréis, en mi estudio de los temas WP, me salté el apartado de internacionalización. Ahora, quiero que mi plugin esté disponible para cualquiera, sea de donde sea. Por tanto, debo permitir su traducción a cualquier idioma. Esto también será interesante, porque quien haya seguido la serie verá cuáles son los cambios y cómo se realizan.
Internacionalización
¿Qué es internacionalización?
Internacionalización es el proceso de desarrollar tus plugins de tal manera que sea fácilmente traducido a otros idiomas. La internacionalización, a menudo, se abrevia como i18n (porque hay 18 letras entre la i y la n en la palabra internationalization).
¿Por qué es importante la internacionalización?
WordPress se utiliza en todo el mundo. Entonces, las cadenas de los plugins de WordPress necesitan ser codificadas de una manera especial que permita traducirlas a otros lenguajes. Como desarrollador, no tienes tiempo en proveer localizaciones para todos tus usuarios; en cambio, cualquier traductor puede fácilmente localizar el plugin sin necesidad de modificar el código fuente.
Cómo internacionalizar tu plugin
Para hacer una cadena traducible en tu aplicación, debes envolver la cadena original en una llamada a una de las funciones especiales.
Introducción a gettext
WordPress utiliza las librerías y herramientas gettext para i18n. Si miras código, verás la función _() que refiere a la función nativa PHP de traducción. Con WordPress deberás utilizar la función __() PHP definida por WordPress. Si quieres tener un conocimiento más amplio y profundo de gettext, recomendamos la lectura del Manual online gettext.
Dominios de texto
Es importante utilizar un dominio de texto para indicar todo el texto del plugin. El dominio de texto es un identificador único, que asegura que WordPress puede distinguir entre todas las traducciones cargadas. Ello aumenta la portabilidad y funciona mejor con las herramientas de WordPress existentes. El dominio de texto debe hacer juego con el slug del plugin. Si tu plugin es un único fichero llamado my-plugin.php o está ubicado en un directorio llamado my-plugin, el nombre de dominio debe ser my-plugin. El nombre del dominio de texto utiliza guiones y no subrallados.
Ejemplo:
__( ‘String (texto traducible)’, ‘dominio-texto’ ); // donde dominio-texto se puede cambiar a my-plugin.
El dominio de texto también necesita ser añadido a la cabecera del plugin. WordPress lo utiliza para internacionalizar los metadatos de tu plugin cuando está desactivado. El dominio de texto debe ser el mismo que el utilizado cuando se carga el dominio de texto.
1 2 3 4 5 |
/* * Plugin Name: My Plugin * Author: Plugin Author * Text Domain: my-plugin */ |
Ruta de dominio
La ruta de dominio se utiliza para que WordPress sepa donde encontrar la traducción cuando el plugin está desactivado. Sólo es útil si las traducciones están localizadas en un subdirectorio de idioma separado. Por ejemplo, si los ficheros .mo están localizados en el directorio local, entonces la ruta de dominio será «/languages» y debe tener el primer slash (/):
1 2 3 4 5 6 |
/* * Plugin Name: My Plugin * Author: Plugin Author * Text Domain: my-plugin * Domain Path: /languages */ |
Cadenas básicas
El uso más frecuente es __(). Devuelve la traducción del argumento:
1 |
__( 'Blog Options', 'my-plugin' ); |
Otro uso simple es _e(), el cual muestra la traducción del argumento.
1 2 3 4 5 6 7 |
// Esto es válido echo __('WordPress is the best!', 'my-plugin' ); // Esto es más corto _e( 'WordPress is the best!', 'my-plugin' ); |
Variables
Si utilizas variables en cadenas como el siguiente ejemplo, debes utilizar marcadores de posición.
echo ‘Your city is $city.’;
La solución es utilizar la familia de funciones printf. Especialmente útiles son printf y sprintf. La solución correcta es como:
1 2 3 4 5 |
printf( /* translators: %s: Name of a city */ __( 'Your city is %s.', 'my-plugin' ), $city ); |
Observa que aquí la cadena de traducción es la plantilla «Your city is %s.».
Si tienes más de un marcador de posición en una cadena, es recomendable que utilices intercambio de argumentos. En este caso, las comillas simples (‘) tienen prevalencia: las comillas dobles («) indican a PHP a interpretar $s como la variable s, que no es lo que pretendemos.
1 2 3 4 5 6 |
printf( /* translators: 1: Name of a city 2: ZIP code */ __( 'Your city is %1$s, and your zip code is %2$s.', 'my-plugin' ), $city, $zipcode ); |
NOTA: El siguiente código es incorrecto:
1 2 |
// This is incorrect do not use. _e( "Your city is $city.", 'my-plugin' ); |
Las cadenas para traducir se extraen de las fuentes, luego los traductores tomarán esta frase para traducir: «Your city is $city.».
Plurales
Pluralización básica.
Si tienes una cadena que cambia cuando el número de ítems cambia. En inglés tienes «One comment» y «Two comments». En otros idiomas tienes múltiples formas de plural. Para gestionar esto en WordPress puedes utilizar la función _n().
1 2 3 4 5 6 7 8 9 |
printf( _n( '%s comment', '%s comments', get_comments_number(), 'my-plugin' ), number_format_i18n( get_comments_number() ) ); |
_n() acepta 4 argumentos:
- singular – la forma singular de la cadena
- plural – la forma plural de la cadena
- count – el número de objetos, lo que determina si se devolverá la forma singular o plural.
- dominio de texto del plugin.
El valor devuelto de las funciones es la forma correcta traducida, correspondiente al cómputo dado.
Pluralización posterior
Primero selecciona las cadenas plurales con _n_noop() o _nx_noop().
1 2 3 4 |
$comments_plural = _n_noop( '%s comment.', '%s comments.' ); |
Luego, en un punto posterior del código, puedes utilizar la función translate_nooped_plural() para cargar las cadenas.
1 2 3 4 5 6 7 8 |
printf( translate_nooped_plural( $comments_plural, get_comments_number(), 'my-plugin' ), number_format_i18n( get_comments_number() ) ); |
Desambiguación por contexto
En ocasiones un término es utilizado en varios contextos y a pesar de que es uno y la misma palabra en inglés, tiene diferentes traducciones en otros idiomas. Por ejemplo, la palabra post puede utilizarse como enviar («Click here to post your comment») y como artículo («Edit this post»). En ambos casos las funciones _x() o _ex() se pueden utilizar. Son similares a __() y _e(), pero tienen un argumento adicional, el contexto:
1 2 |
_x( 'Post', 'noun', 'my-plugin' ); _x( 'Post', 'verb', 'my-plugin' ); |
Utilizar este método en ambos casos tendremos la cadena Comment para la versión original, pero los traductores verán dos cadenas Comment para traducir, cada una en diferentes contextos.
Observa que parecidamente a __(), _x() tiene una versión echo: _ex(). El ejemplo anterior se puede escribir así también:
1 2 |
_ex( 'Post', 'noun', 'my-plugin' ); _ex( 'Post', 'verb', 'my-plugin' ); |
Descripciones
Para que los traductores sepan cómo traducir una cadena como __( ‘g:i:s a’ ) puedes añadir un comentario clarificador en el código fuente. Debe empezar con la palabra translators: y ser el último comentario PHP antes de la llamada gettext. Veamos un ejemplo:
1 2 |
/* translators: draft saved date format, see http://php.net/date */ $saved_date_format = __( 'g:i:s a' ); |
También se utilizan para explicar marcadores de posición en una cadena como:
1 2 3 |
/* translators: 1: WordPress version number, 2: plural number of bugs. */ _n_noop( '<strong>Version %1$s</strong> addressed %2$s bug.', '<strong>Version %1$s</strong> addressed %2$s bugs.' ); |
Caracteres de línea nueva
Gettext no quiere \r (código ASCII 13) en cadenas a traducir, por ello emplea en su lugar \n.
Cadenas vacías
Las cadenas vacías están reservadas para usop interno de gettext y debes evitar internacionalizar cadenas vacías. No tiene sentido, porque los traductores no encuentran el contexto.
Si tienes un caso válido para internacionalizar una cadena vacía, añade contexto tanto a la ayuda a traductores y estate en paz con el sistema gettext.
Manejo de ficheros JavaScript
Utiliza wp_localize_script() para añadir cadenas traducibles a scripts previamente añadidos.
Caracteres de escape
Es recomendable escapar todas tus cadenas, de esta manera los traductores no podrán ejecutar código malicioso. Hay unas cuantas funciones de escapado que están integradas con las funciones de internacionalización.
Funciones de localización
Más adelante se explican.
Funciones básicas
__()
_e()
_x()
_ex()
_n()
_nx()
_n_noop()
_nx_noop()
translate_nooped_plural()
Funciones de escape
esc_html__()
esc_html_e()
esc_html_x()
esc_attr__()
esc_attr_e()
esc_attr_x()
Funciones de fecha y numéricas
number_format_i18n()
date_i18n()
Buenas prácticas para escribir cadenas
Aquí están las buenas prácticas para escribir cadenas:
- Utilizar un estilo decente.
- Emplear sentencias enteras.
- Divide con párrafos. Une sentencias relacionadas, pero no incluyas una página entera de texto en una sola cadena.
- Asume que las cadenas pueden doblar su longitud al ser traducidas.
- Evita marcados o caracteres de control no habituales.
- No incluyas marcado HTML innecesario en la cadena a traducir.
- No incluyas URLs en cadenas a traducir. Pueden tener una versión en otro lenguaje.
- Añade las variables como marcadores de posición en la cadena como en algunos idiomas los marcadores de posición cambian de posición.
12345Añade las variables como marcadores de posición en la cadena como en algunos idiomas los marcadores de posición cambian de posición.printf(__( 'Search results for: %s', 'my-plugin' ),get_search_query()); - Utiliza formatos de cadenas en lugar de concatenar cadenas – traduce frases, no palabras.
123456789printf(__( 'Your city is %1$s, and your zip code is %2$s.', 'my-plugin' ),$city,$zipcode);// Es mejor que__( 'Your city is ', 'my-plugin' ) . $city . __( ', and your zip code is ', 'my-plugin' ) . $zipcode; - Intenta utilizar las mismas palabras y los mismos símbolos, para no necesitar traducir múltiples cadenas.
Añade dominio de texto a las cadenas
Debes añadir tu dominio de texto como un argumento a cada llamada __(), _e() y __n(), de otra manera tus traducciones no funcionarán.
Ejemplos:
1 2 3 |
__( 'Post' ) debe ser __( 'Post', 'my-plugin' ) _e( 'Post' ) debe ser _e( 'Post', 'my-plugin' ) _n( '%s post', '%s posts', $count ) debe ser _n( '%s post', '%s posts', $count, 'my-plugin' ) |
Si hay cadenas en tu plugin que también son empleadas por el núcleo de WordPress (p.ej., ‘settings’), debes también añadir tu dominio de texto a las mismas, o de otra manera serán intraducibles si las cadenas del núcleo cambian (que pasa).
Añadir el dominio de texto a mano puede ser una carga si no se hace continuamente mientras se escribe el código y esto es porque puedes hacerlo automáticamente:
- Si tu plugin está en el repositorio oficial de plugins, ve a tu página de «Administración» y baja a «Add Domain to Gettext Calls».
- Sube el fichero para el dominio de texto que quieres añadir.
- Clica en «Get domainified file».
De otra manera:
- Descarga el escript add-textdomain.php a la carpeta donde está el fichero al que quieres añadir el dominio de texto.
- En línea de comandos, ve al directorio en el que está el fichero.
- Ejecuta este comando para crear un nuevo fichero con el dominio de texto añadido:
1php add-textdomain.php my-plugin my-plugin.php > new-my-plugin.php - Si quieres tener el archivo add-textdomain.php en una carpeta diferente, debes definir la localización en el comando.
1php \path\to\add-textdomain.php my-plugin my-plugin.php > new-my-plugin.php - Utiliza este comando y no quieres crear un nuevo fichero.
1php add-textdomain.php -i my-plugin my-plugin.php - Si quieres modificar múltiples ficheros en un directorio, puedes también pasar un directorio al script.
1php add-textdomain.php -i my-plugin my-plugin-directory
Después de hacerlo, el dominio de texto se añadirá al final de las llamadas gettext en el fichero. Si hay un dominio de texto previo, se mantendrá.
Cargar el dominio de texto
Necesitas cargar el fichero MO con tus traducciones del plugin. Puedes cargarlo mediante la función load_plugin_textdomain(). Esta llamada carga {text-domain}-{locale}.mo del directorio base de tu plugin. El ‘locale’ es el codigo de lenguaje y/o código de país del lenguaje seleccionado en ‘Ajustes generales’. Para más información acerca de idiomas y códigos de país, mira WordPress en tu idioma.
En el siguiente ejemplo, el dominio de texto es my-plugin, por lo tanto los ficheros alemanes MO y PO deben llamarse my-plugin-de_DE.mo y my-plugin-de_DE.po:
1 2 3 4 |
function my_plugin_load_plugin_textdomain() { load_plugin_textdomain( 'my-plugin', FALSE, basename( dirname( __FILE__ ) ) . '/languages/' ); } add_action( 'plugins_loaded', 'my_plugin_load_plugin_textdomain' ); |
Packs de lenguaje
Si estás interesado en packs de lenguaje, lee Meta Handbook page about y Translations.
Localización
¿Qué es localización?
Localización describe el proceso siguiente de traducir un plugin internacionalizado. Localización se abrevia como l10n.
Ficheros de localización
- Ficheros POT (Portable Object Template)
Este fichero contiene las cadenas originales (¿en inglés?) de tu plugin. He aquí una entrada ejemplo de un fichero POT:
1 2 3 |
#: plugin-name.php:123 msgid "Page Title" msgstr "" |
- Ficheros PO (Portable Object)
Cada traductor tomará el fichero POT y traducirá las secciones msgstr a su propio idioma. El resultado es un fichero PO con el mismo formato que un fichero POT, pero con traducciones y algunas cabeceras específicas. Existe un fichero PO por idioma.
- Ficheros MO (Machine Object)
De cada fichero PO traducido, se crea un fichero MO. Son ficheros binarios, legibles por la máquina, que las funciones gettext actualmente utilizan (no se preocupan acerca de los ficheros POT o PO), y son una versión «compilada» de los ficheros PO. La conversión se da utilizando la herramienta msgfmt. En general, una aplicación puede utilizar más de un gran módulo lógico traducible y un fichero diferente MO acorde. Un dominio de texto se encarga de cada módulo, que es un fichero MO diferente.
Generar el fichero POT
El fichero POT es el único que necesitas para encargarte de los traductores, para que realicen su faena. Los ficheros POT y PO pueden ser fácilmente intercambiados renombrados para cambiar los tipos de fichero sin problemas. Es una buena idea ofrecer el fichero POT con tu plugin, para que los traductores no tengan que buscar las cadenas. Existen unas cuantas maneras de generar un fichero POT en tu plugin:
- Herramientas del Admin del Repositorio del Plugin
Si tu plugin está registrado en el repositorio oficial de plugins, ve a tu página de «administración» y clica en el botón «Continue» de la sección «Generate POT file». Después, simplemente, clica en el botón «Get POT» para descargar el fichero POT.
- Herramientas WordPress i18n
Si tu plugin no está en el repositorio oficial, puedes revisar el directorio WordPress Trunk de SVN (ver Using Subversion para aprender acerca de SVN). Necesitas revisar el paquete entero porque las herramientas wordpress-i18n utilizan código del núcleo de WordPress para generar el fichero POT. Necesitas que los paquetes gettext y PHP estén instalados en tu servidor u ordenador antes de que ejecutes el comando.
Abre la línea de comandos y cambia el directorio a la carpeta de idioma. Entonces, puedes ejecutar el scrip makepot.php en la línea de comandos así:
1 |
php path/to/makepot.php wp-plugin path/to/your-plugin-directory |
Cuando termine, puedes ver el fichero POT en el directorio actual.
- Poedit
También puedes utilizar Poedit localmente para traducir. Es una herramienta de código abierto para todos los sistemas operativos mayoritarios. La versión por defecto gratuita de Poedit soporta escaneo manual para todos el código fuente con funciones gettext. Está disponible una versión profesional que ofrece una característica de escaneo en un click para los plugins WordPress. Después de generar el fichero PO, puedes renombrarlo a POT. Si creara un fichero MO, puedes eliminarlo, pues no es necesario. Si no tienes la versión pro, puedes tener fácilmente el Blank POT y utilizarlo como base de tu fichero POT. Una vez tienes el POT vacío en el directorio de idiomas, puedes clicar «Update» en Poedit para actualizar el fichero POT con tus cadenas.
- Otro
Con grunt-wp-i18n y grun-pot, necesitas instalar node.js. Es una instalación sencilla. Más información en https://developer.wordpress.org/plugins/internationalization/localization/#grunt-tasks.
Traducir el fichero PO
Existen múltiples maneras de traducir un fichero PO.
Puedes utilizar un editor de textos para introducir la traducción.
También puedes utilizar Poedit.
Una tercera opción es utilizar un servicio de traducción en línea. La idea general es que subes el fichero POT y das permiso a los usuarios o traductores para traducir tu plugin. Esto permite seguir la trayectoria de los cambios, tener siempre la última traducción y evitar hacer la faena dos veces.
WP-Translations, una comunidad «donde los traductores conocen a desarrolladores», corre en Transifex. Puedes enviar proyectos para traducir como desarrollador, o traducir plugins y temas existentes a tu idioma.
Unas cuantas herramientas que puedes emplear para traducir online tus ficheros PO (aparte de las ya descritas):
También puedes utilizar plugins, como Codestyling Localization.
El fichero traducido se guarda como my-plugin-{locale}.mo. ‘Locale’ es el código de idioma y/o país que defines en la constante WPLANG en el fichero wp-config.php.
Generar el fichero MO
- Línea de comandos
El programa msgfmt se utiliza para crear el fichero MO. msgfmt es parte del paquete Gettext. Para ello:
1 |
msgfmt -o filename.mo filename.po |
Si tienes un lote de ficheros PO, puedes hacerlo como un proceso batch:
Para UNIX:
1 2 |
# Find PO files, process each with msgfmt and rename the result to MO for file in `find . -name "*.po"` ; do msgfmt -o ${file/.po/.mo} $file ; done |
Para WINDOWS (previamente necesitas instalar Cygwin):
1 2 3 4 5 6 7 8 9 |
Create a potomo.sh #! /bin/sh # Find PO files, process each with msgfmt and rename the result to MO for file in `/usr/bin/find . -name '*.po'` ; do /usr/bin/msgfmt -o ${file/.po/.mo} $file ; done You can run this command in the command line. cd C:/path/to/language/folder/my-plugin/languages & C:/cygwin/bin/bash -c /cygdrive/c/path/to/script/directory/potomo.sh |
- Poedit
msgfmt está integrado en Poedit, permitiendo generar el fichero MO.
- Otros
grunt-po2mo convierte todos los ficheros.
Ideas para buenas traducciones
- No traduzcas literalmente, traduce orgánicamente.
Al ser bi o multi-idiomas, indudablemente sabes que los idiomas que hablas tienen diferentes estructuras, ritmos, tonos e inflexiones. Los mensajes traducidos no necesitan estructurarse de la misma forma que los originales: toma las ideas presentes y utiliza un mensaje que exprese la misma cosa en una manera natural para el idioma final. Es la diferencia entre crear un mismo mensaje y crear un lenguaje equivalente: no iguales, reemplaza. Lo mismo con más ítems estructurales en los mensajes, tienes licencia creativa para adaptar y cambiar si sientes que será más lógico o más adaptado para tu audiencia.
- Intenta guardar el mismo nivel de formalidad o informalidad.
Cada mensaje tiene un nivel diferente de formalidad o informalidad. Qué nivel exactamente de formalidad utilizar para cada mensaje en tu idioma final es algo que tienes definir tú (o tu equipo), pero los mensajes de WordPress (particularmente los de información) tienden a tener un tono políticamente informal en inglés. Intenta conseguir lo mismo en el idioma final, dentro de tu contexto cultural.
- No utilices argot o términos específicos de audiencia.
Alguna terminología se espera en un blog, pero rehúsa utilizar coloquialismos que sólo conocen ciertas personas. Si el blogger no iniciado instala WordPress en tu idioma, ¿conocerá lo que significan ciertos términos? Palabras como pingback, trackback y feed son excepciones.
- Lee otras traducciones en tu idioma.
Al hacerlo, observarás cuáles son los términos comúnmente utilizados, qué formalidad, etc.
Utilizar localizaciones
A partir de WordPress 4.0, puedes cambiar el idioma en «Ajustes generales». Pero, si no ves una opción o el idioma que quieres, haz lo siguiente:
- Define WPLANG entro del archivo wp-config.php:
1define ('WPLANG', 'fr_FR' ); - Ve a wp-admin/options-general.php o ‘Ajustes’ > ‘General’.
- Selecciona tu idioma en ‘Idioma del sitio’
- Ve a wp-admin/update-core.php.
- Clica ‘Update translations’, cuando esté habilitado.
- Se descargan los archivos de traducción, cuando estén disponibles.
Seguridad en la internacionalización
La seguridad, a menudo, se pasa por alto al hablar de internacionalización, pero hay unas pocas cosas importantes a tener en mente.
Chequea el spam y otras cadenas maliciosas
Cuando un traductor te envía una traducción, asegúrate siempre que no incluye spam o palabras maliciosas en su traducción. Puedes utilizar Google Translate para traducir su traducción inversamente a tu idioma natural, para poder comparar entonces el original y la traducción.
Utiliza marcadores de posición para las URLs
No incluyas URLs en cadenas internacionalizadas, porque un traductor malicioso puede cambiarlas a una dirección diferente. En su lugar, utiliza marcadores de posición para printf() o sprintf().
No seguro:
1 2 3 4 5 |
<?php _e( 'Please <a href="https://wordpress.org/support/register.php"> register for a WordPress.org account</a>.', 'your-text-domain' ); ?> |
Seguro:
1 2 3 4 5 6 7 |
<?php printf( __( 'Please <a href="%s">register for a WordPress.org account</a>.', 'your-text-domain' ), 'https://wordpress.org/support/register.php' ); ?> |
Compila tus propios binarios .mo
Frecuentemente los traductores envían los ficheros compilados .mo junto con el fichero de texto .po, pero debes descartar su fichero .mo y compilarlo tú mismo, porque no tienes manera de saber si se ha compilado del correspondiente archivo .po o de otro diferente. En este último caso, podría contener spam y otras palabras maliciosas si tu conocimiento.
Utilizando Poedit para generar el binario, sobreescribirá las cabeceras en el fichero .po, por lo que es mejor compilarlo desde la línea de comandos:
msgfmt -cv -o /path/to/output.mo /path/to/input.po
Recursos
- Video: i18n: Preparing Your WordPress Theme for the World
- Video: On Internationalization: Plugins and themes for the whole world Slides
- Video: Big in Japan: A Guide for Themes and Internationalization
- Video: Lost in Translation—i18n and WordPress
- Internationalizing And Localizing Your WordPress Theme
- Internationalization: You’re probably doing it wrong
- More Internationalization Fun
- Use wp_localize_script, It Is Awesome
- Understanding _n_noop()
- Language Packs 101 – Prepwork
- Translating WordPress Plugins and Themes: Don’t Get Clever
- How to load theme and plugin translations
- Loading WordPress language files the right way
- Creating .pot file for your theme or plugin
- How To Internationalize WordPress Plugins
- Translating Your Theme
- Blank WordPress POT
- Improved i18n WordPress tools
- How to update translations quickly
- Workflow between GitHub/Transifex
- Gist: Complete Localization Grunt task
- WordPress.tv tags: i18n, internationalization and translation
Conclusiones
Si alguien ha terminado de leer este artículo, ¡enhorabuena!. De todas formas, en el próximo artículo de la serie haremos un resumen práctico en el que internacionalizaremos nuestro plugin para su distribución a escala mundial (caray, qué bien me ha quedado).