Traducciones y tripas

Hola chavales,

He sacado un rato para contaros una cosilla que me ha tocado investigar últimamente. Las herramientas de traducción de aplicaciones, también conocidas i18n y l10n por internationalization y localization (contad las letras de las palabras y entenderéis el número).

Esas herramientas de traducción no son simplemente eso. Sirven para traducir y localizar las aplicaciones, no sólo traducen las frases o palabras, también saben elegir si usar un singular o un plural dependiendo de un número o utilizar cuantificadores como “mucho” o “poco” (pluralization). A parte de eso, valen para transformar la moneda, el formato de las fechas, las unidades de medida y ese tipo de cosas. El l10n hace unas cosas y el i18n otras leed al respecto que no es malo informarse (y se escapa a lo que quiero contar aquí).

Hoy quiero contarlos cómo hacer una aplicación de éstas de forma simple, y sólo me centraré en las traducciones. Además, tendréis mi solución de i18n de la que hablé en la entrada anterior, que es rematadamente simple y trata exactamente de lo que hablaré aquí hoy1. Ni más ni menos.

Esta entrada pretende que se os ocurra a vosotros cómo hacer una librería de este estilo, tirando a lo simple, para que los más novatos entendáis cómo es posible hacer cosas útiles con el conocimiento que ya tenéis. No es tan difícil programar. Pensadlo: si lo fuera, posiblemente el que os escribe no sería capaz de hacerlo.

Vale, vamos a por ello.

¿Cómo creéis que funciona una mierda de éstas?

Pensad un poco conmigo. Recordad que sólo queremos que pueda traducir, no queremos pluralización ni extras (luego hablaremos también de eso).

A nivel de usuario de la librería, normalmente funcionan de la siguiente manera:

  1. Importamos la librería en el programa que queramos que pueda ser traducido.
  2. En todo el texto que vaya a ser visto por el usuario se pone un identificador de algún tipo. Normalmente se trata de una llamada a una función entregada por la librería. Ejemplos:

i18n("texto a traducir")

_("texto a traducir")

gettext("texto a traducir")

  1. De alguna manera, se vuelcan todos los strings para traducir en unos ficheros con espacio para su traducción. Uno por idioma.
  2. Se traducen.
  3. Durante la ejecución, la aplicación busca los strings a traducir antes de mostrarlos en los ficheros. Cuando los encuentra, usa la traducción adecuada al lenguaje configurado por el usuario.

Así es como funciona. Simple ¿No?

Si no me he explicado bien podéis buscar algunas librerías de i18n y mirar su documentación para ver qué formato de ficheros utiliza o qué herramienta extrae los strings para la traducción.

Una vez entendido esto, hay que empezar a darle vueltas a cómo lo implementaríamos. Durante el desarrollo de esto haré algunas trampitas, porque soy un poco malévolo, pero os prometo que tendrán sentido.

¿Cómo lo haríais vosotros? ¿Por dónde empezaríais? ¿Qué os parece lo más difícil?

Nosotros vamos empezar por el paso 2 de la lista anterior. La forma de indicar que los strings son traducibles. La mejor forma es meterlos en una función, aunque habría otras. Si le damos al usuario de la librería una función a utilizar será simple: la aplicará a los strings que le interesen y todo irá bien.

Hay muchas formas de hacer esto, la más fácil es que los ficheros de traducción sean simples ficheros tipo JSON donde las claves sean el texto a traducir y los valores la traducción:

{
"hola": "hi",
"adiós": "bye"
}

Con esto así, cuando se llame a la función con un string sería suficiente con buscar en ese JSON la clave y devolver el valor. En JavaScript y simplificando muchísimo:

var traduccion = carga_traduccion();
function _( string ){
  return( traduccion[string] );
}

Lo que no hemos hecho todavía es esa función carga_traduccion que vemos en el snippet y tampoco gestionamos cuál es el idioma actual del usuario.

Eso tampoco es taaan difícil ¿no?

Podemos tirar por un formato orientado a objetos para hacerlo sencillo. Suponemos que el sistema de traducciones es una clase/prototipo (no me voy a poner a discutir esto ahora) con un campo de “idioma actual” y lo de arriba se transformaría en algo un poco más complejo si suponemos que el JSON de idiomas tiene un nivel de más que referencia al idioma. En JavaScript de nuevo:

{
"hola": { "english": "hi", "euskera": "kaixo" }
"adiós": { "english": "bye", "euskera": "agur" }
}

//...
function _( string ){
  this.traduccion[string][this.idioma];
}
//...

Una vez tenemos esto, sólo necesitamos hacer que nuestra clase/prototipo pueda configurar el idioma, leer los ficheros de idioma automáticamente al iniciar (el carga_traduccion de antes) y que le entregue la función _ al hilo principal.

Cargar los ficheros en principio es sólo leer una carpeta y ya. Aunque si estamos en el browser tenemos que hacer alguna magia añadida que no debería costaros mucho. Tema resuelto.

¡Coño! ¿Y ya está?

En realidad no, porque el traductor tendría un desastre de ficheros de idioma, y encima tendría que rellenarlos a mano. Lo suyo es que haya una herramienta tipo gettext que extraiga todos los strings a traducir y los vuelque en ficheros que luego tengan que traducirse. Ésta es la trampa, este punto es el más difícil. En mi solución de i18n, como sé que el lugar donde va a usarse lo permite, la propia clase vuelca los ficheros (también llamados catalog) al terminar la traducción, pero esto no es factible siempre porque si la ejecución del programa nunca pasa por mostrar el string nunca aparecería en los catálogos. (¿Quizás con unos tests brutos podríamos hacerlo?)

Otro punto interesante es tener en cuenta que hemos usado strings con texto, en nuestro caso en castellano. Entonces si el usuario tuviese seleccionado el idioma en castellano no tendrían que traducirse y tendrían que entregarse igual. Hay dos formas sencillas de atacar este problema:

  1. Obligar al usuario a no poner el string en concreto y poner un identificador para siempre se busque en la traducción.
  2. Tener configurado un idioma por defecto que sea el original.

Otra cosa que quiero tocar es que esos ficheros de JSON pueden ser la muerte (a pesar de que algunas herramientas los usan) porque si el texto a traducir es muy largo nos quedaríamos con unas claves gigantescas en una única línea (JSON no permite claves multilínea). El contenido también sería difícil de traducir porque también sería en una sola línea y tendría también caracteres especiales por el medio para marcar los saltos. ¡Quizás sea mejor usar otro tipo de fichero! Mi solución i18n usa YAML1 pero también tiene un sistema diferente para gestionar las claves. Si os interesa me preguntáis o lo miráis.

Otro punto extra es que no tenemos ninguna información sobre lo que estamos traduciendo. La misma palabra puede tener que traducirse de dos maneras diferentes dependiendo de dónde esté porque el contexto puede ser distinto. En este caso, nuestro programa sólo contempla que textos iguales se traducen igual.

Tampoco nos dice en qué línea del programa original está lo que traducimos así que es difícil encontrarlo.

Ni nos da ningún tipo de detalle sobre la traducción, cosa que en algunas soluciones i18n existe: Te permiten poner palabras clave para definir el contexto mejor como guías para los traductores que se ignoran en el programa pero que se vuelcan en los ficheros de traducción. Ejemplo:

i18n("Abrir", "Abre un fichero"); // siendo Abrir el string a traducir

Ya os he hablado de la pluralización antes, y de la internacionalización también. Eso también complica las cosas…

Joder…

Bueno eso, que era fácil…

¿No?

😉

Un abrazo.

Anuncios

Un pensamiento en “Traducciones y tripas

  1. Traducciones y tripas | PlanetaLibre

Los comentarios están cerrados.