Voy a continuar explicando novedades y curiosidades sobre FileOptimizer. Nos habíamos quedado con FileOptimizer 11, y ya tenemos aquí FileOptimizer 12 con bastantes novedades.

La principal característica es el soporte multiidioma, algo que los usuarios llevaban tiempo demandando, y que debido a falta de tiempo, había ido posponiendo. Lo cierto es que una vez más, y renunciando en gran parte a mis fines de semana, ¡FileOptimizer es ya multilenguaje!

Lo mejor de todo, es en mi opinión su diseño y su implementación. En cuanto al diseño, opté por usar archivos de traducción estándar, en ese caso .PO. No es un formato que me agrade particularmente, y tampoco es demasiado eficiente ni en términos de uso de espacio en disco, ni de rendimiento. Pero es un formato conocido y para el que existen multitud de herramientas, de manera que es más probable que los usuarios se animasen a traducirlo a sus propios idiomas.

Un archivo PO es esencialmente un archivo de texto en codificación UTF8 sin BOM (esa es la primera cosa que no me gusta), y con una estructura de esta forma:

msgid "&Optimize all files"
msgstr "&Optimiza todos los archivos"

O sea pares de lineas que contienen después de msgid el texto original en la aplicación, y en msgstr el texto traducido para ese idioma. No es un formato demasiado eficiente, puesto que para cada literal, tendremos que buscar en ese archivo la linea msgid correspondiente, y una vez encontrada, leer hasta encontrar un msgstr con la traducción. Si hubiera dependido de mi, habría preferido un esquema como este:

frmMain.mnuOptimize.Caption="&Optimiza todos los archivos"

Esencialmente una estructura tipo archivos .INI, donde la clave (frmMain.mnuOptimize.Caption) es el ámbito del texto a traducir, en este caso la propiedad Caption del elemento de menú mnuOptimize dentro del formulario frmMain. Evitamos tener que incluir en el archivo nuevamente el texto en inglés.

Sea como fuere, tenía claro que usaría archivos PO por el motivo que os explicaba. Estudié la posibilidad de usar gettext, la librería GNU en C que se usa por ejemplo en PHP y que es habitual en CMS como WordPress. No me gustó, es tan portable, y tan flexible que son algo más de 100 Kb. de código fuente. ¡Y eso solamente para buscar pares de cadenas!



La decisión estaba clara, implementarlo yo mismo. Reconozco que el parser de archivos PO que he implementado es bastante limitado y sencillo, pero es muy rápido que es lo que buscaba. En realidad es la segunda implementación que hice. Tras haberlo programado de una forma decidí tirarlo puesto que el rendimiento no me satisfacía. Tal vez pienses que no tiene mucha importancia que traducir una cadena lleve 0,01 segundos o 0,1 segundos, pero te confundirías. La pantalla de Opciones en FileOptimizer hay unas 150 cadenas, es decir 150 llamadas al motor de traducción. Si cada una requiere 0,1 segundos, necesitaríamos 15 segundos de espera antes de que el usuario pudiera ver la ventana de opciones traducida a su idioma. No es asumible. Si son 0,01 segundos, el tiempo de espera sería solamente 1,5 segundos. Poco tiempo, pero evidente. Logré hacerlo en 0,003 segundos. Así el formulario más complejo del programa, necesita menos de 0,4 segundos para aplicar las traducciones. ¡Está muy bien!

¿Cómo lo logré? Pues bien, descartada la opción que utilizan muchos, y que parece la más sencilla, consistente en abrir el archivo PO, leerlo para buscar la cadena, y al final cerrarlo, cosa que tendríamos que hacer cada vez que pidiéramos una traducción. Pongamos un caso práctico. La traducción al español o castellano de FileOptimizer son algo más de 600 lineas de texto. En el peor de los casos hay que leer esas 600 lineas, multiplicado por 150 llamadas, es una lectura de 90.000. Está claro que no es buen enfoque, como sugería en Corrección para la función “Como leer un archivo PO desde PHP para traducir nuestra web”.

La cuestión era evitar la lectura del archivo a cada petición, es decir, guardarlo en memoria, y la otra, evitar el acceso linea por linea, y cargarlo de golpe en memoria. Esa fue mi primera versión. Un bloque de memoria tenía el contenido completo del archivo, y se buscaba dentro de ese bloque a nivel binario para encontrar la traducción.

El inconveniente era esa búsqueda, algo que me pilló por sorpresa. Porque recorrer el bloque de 20 Kb completo tantas veces distaba de ser óptimo. Ahí es donde entra en juego el tipo VCL (Delphi, Lazarus, C++ Builder, …) llamado THashedStringList. Un tipo abstracto de datos que contiene una lista de cadenas. Cada una tiene asociado un hash, lo que hace que para buscar si un elemento existe, no sea necesario recorrer la lista completa.

Ahora que ya teníamos un motor de traducción que funcionaba y era rápido, quedaba el segundo reto. La forma de utilizarlo. Este es el primer proyecto que creo que es multiidioma en una fase tardía. Digamos que crear un programa o una web con varios idiomas desde el principio es relativamente sencillo, pero una vez que todo está montado, va a ser más laborioso.

La primera idea que me surgió, y que hasta donde yo se es cosecha propia, consistía en recorrer todos los elementos de un formulario de manera recursiva, y solicitar automáticamente la traducción de cada texto. La idea tiene sentido. Si el código encuentra un botón que tiene el texto “OK”, le pide al archivo PO el texto en el idioma del usuario, y luego lo cambia por “Aceptar”. Si repetimos este paso con todos los elementos, tenemos que añadiendo solamente la llamada a la función de traducir el formulario, tenemos toda la interfaz ya en nuestro idioma. Interesante, ¿verdad?

Pues sí, interesante, pero insuficiente, porque el programa contiene también textos que no están dentro del formulario. Por ejemplo cuando el usuario quiere salir de FileOptimizer, se le puede preguntar “Are you sure you want to exit?” ese texto no está en el formulario, así que no se traduciría.

Aquí el enfoque era copiar lo que hacen otros CMS como WordPress. Creamos la función _ que tiene por parámetro una cadena, y que nos devuelve la cadena traducida. Por tanto donde nuestro código hacía algo como esto:

puts("Hello world");

Lo que hacemos es esto:

puts(_("Hello world"));

Hemos añadido solamente 3 caracteres, pero ahora nuestro programa ya sacará “Hello world” en el idioma del usuario. Dado las particularidades de C++ Builder, con cadenas que pueden ser de tipo char/wchar/tchar como en C/C++, pero también las de Pascal tipo String, había que hacer dos funciones dependiendo del tipo de argumentos. Si se llamaba con String retornaría un String, y si se hacía con TCHAR retornaría TCHAR. Una de las gracias de C++ es que permite métodos sobrecargados, así que podemos definir esas dos funciones, y el compilador se encargará sin que tengamos que hacer nada de llamar a una u otra en función del tipo de datos.

Finalmente había otro problema que también había anticipado al principio. El tedioso proceso de crear el archivo de traducciones. Imaginaros, recorrer el código y los formularios, y para cada string, colocarlo manualmente en el archivo PO. Viable, pero algo que no suele gustar. Había otra cosa peor, que era que cada vez que creara un nuevo texto en FileOptimizer, tendría que acordarme de añadirlo al PO… Precisamente ese era el objetivo de la informática ¿no? Automatizar tareas, y liberar a los humanos del trabajo repetitivo.

Vale, pues aquí hay otra idea que se me ocurrió, y que me parece muy buena. Hacer que fuera la propia librería de traducción la que generase ese archivo PO con cada cadena que se le pedía traducir. En el caso del Hello world de antes, cuando se llamase a la función de traducir, la librería buscaría si en el archivo PO con el idioma inglés original existe ya ese mensaje de Hello world, y si no existe lo guardaría. Al final tenemos un archivo PO con todas las cadenas en inglés para traducir. Sólo quedaba un detalle, y era que esta característica era como si se tradujeran las cosas 2 veces, los esfuerzos por hacer que todo fuera rápido, se difuminaban. Entonces corté por lo sano. Dándome cuenta que sería algo que se usaría con poca frecuencia, corté por lo sano. Sólo se actualizaría el PO original cuando el usuario lo pidiese, así que agregué el parámetro por linea de comandos /SAVELANG, que activa esta funcionalidad. En caso contrario, está desactivada.

Parecía que el trabajo estuviera terminado, pero quedaba un pequeño detalle. Crear un idioma, algo que sirviera de ejemplo a la gente que quisiera traducirlo, y que me permitiera decir que FileOptimizer es multiidioma. Porque claro, si tiene un motor de traducciones rapidísimo, pero cuando lo usas sólo está en inglés, es como si no hubieras hecho nada.

Después de todo el trabajo anterior, no quería pasarme más horas del fin de semana traduciendo todos los textos. Descubría Google Translation Toolkit, un servicio online gratuito que te permite traducir automáticamente archivos, entre ellos en formato PO. Las traducciones no son perfectas, pero sí que son un buen comienzo. Bastaba con repasar lo que no hubiera quedado bien, y arreglarlo, pero el grosso del trabajo de traducción, lo hizo Google.



He obviado algunos detalles, que simplemente son largos de explicar, y dependientes en cómo Windows gestiona los idiomas. Los conceptos de LCID (Locale ID), Primary Language, etcétera. Finalmente clsLanguage.cpp son menos de 10 Kb. en C++ y unas 300 lineas de código, que quizás parezcan sencillas, pero son bastante brillantes, y que contemplan todo lo que os he explicado en este artículo.

Irónicamente como en el artículo de una imagen vale más que 1000 palabras, el contenido de este post explicando el proceso son 12 Kb. de texto, más que la propia implementación en código, así que por esta vez, me tendréis que perdonar si nos os explico el resto de novedades en FileOptimizer 12, y tendréis que verlas vosotros mismos.