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.
Sí, yo también odio UTF8.
Yo lo tengo en inglés, y solo me aparece esa opción de lenguaje…, no se si estaré haciendo algo mal 😀
Seguramente sea yo el que ha hecho algo mal. ¿Puedes confirmar que tiene varios archivos .po donde está FileOptimizer32.exe / FileOptimizer64.exe? Deberías tener en concreto uno llamado 10.po que es la traducción al español, y que debería ser el que te seleccionara automáticamente FileOptimizer al arrancar, aunque puedes cambiarlo desde Optimizar -> Opciones -> Apariencia.
no hay ningún «po» de esos… A los pobres nos dejas sin el «po»… 🙁 Mira que eres malo.
Tampoco pasa nada, de hecho quita el soporte multiidioma pero mantén la opción, así parece que lo trae y que nadie sabe usarlo, jajajaja!
Estimado usuario saboteador bianamaran:
¿No será que como usted, al igual que yo, es un fanático de la eficiencia, haya eliminado esos archivos una vez descargado FileOptimizer?
Le digo esto porque personalmente acabo de verificar que tanto el 7z como el instalador contienen esos archivos PO en su interior.
Afectuosamente, su mayor fan literario.
sí señor 😀 perdona Guti, no me di cuenta. Mira que si por mi culpa tienes que reescribir de nuevo todo el código… 😀
Suerte que me lo has dicho, porque ya me veía otra noche sin dormir 🙂
Además con el quebradero de cabeza del UTF8, los PO y el BOM a estas horas uno puede acabar verdaderamente loco.
Descargando… 🙂
Estupendo Manuel ya me contarás qué te parece. De hecho entre algunos otros, tu fuiste uno de los que se interesaron por FileOptimizer en nuestro idioma, así que me hace muy feliz que por fin lo puedas tener.
«nuestro idioma»…. ¿cual de ellos?, porque gracias a tu trabajo ahora podemos elegir.
Muchas gracias Fernando.
Hay una cosa que no acabo de entender. Conozco varias aplicaciones plurilingües que utilizan archivos ini para los idiomas, suelen ser id_cadena=»cadena a mostrar».
Donde he visto el formato PO ha sido en WordPress o proyectos propios con PHP. En ambos casos hay dos ficheros, el fuente que es el «.po» propiamente dicho y es como has explicado. Pero luego está el fichero «compilado» que es el «.mo».
La diferencia es que el «.mo» almacena todas las cadenas msgid juntas, como una secuencia de cadenas UTF-8Z (en comparación del ASCIZ), vamos cadenas UTF-8 terminadas por ZERO (0x00), luego añade una «cabecera» del idioma traducido y las cadenas msgstr de la traducción también en UTF-8Z y en el mismo orden que las cadenas msgid, de forma que facilita la carga en memoria… y esta es la parte que no me cuadra: ¿no has utilizado el formato «.mo»?
Aquí la documentación al respecto del .mo:
http://www.gnu.org/software/gettext/manual/gettext.html#MO-Files
La verdad es que no me había preocupado porque cuando quise traducir algo de lo que hago (PHP) usé las funciones Gettext sin preocuparme de más, ya que son extremadamente sencillas de usar (http://php.net/manual/es/book.gettext.php). Lo que si vi es que necesitaba convertir los «.po» en «.mo» para que la cosa funcionara.
Fernando tu comentario es muy acertado. Como digo, mi idea inicial era usar archivos .INI, soy un gran fan de ellos, son sencillos, rápidos, y desde Windows muy fáciles de manejar. Sin embargo poca gente está familiarizado con ellos, y al no estar orientados a la traducción, no hay herramientas específicas para ello, más allá de un editor de texto.
Un problema que tenía es que yo iba a desarrollar la traducción en español también. No quería pasarme 5 o 6 horas haciéndolo y se me ocurrió aprovechar las herramientas de traducción de Google, en pocos minutos tuve una versión automática bastante buena, que sólo debía corregir a mano. O sea, usar PO en vez de INI fue simplemente para facilitar la traducción (por mi parte y por parte de otros), no porque el formato me guste (que no me gusta), ni por su facilidad de programación. Ciertamente PO se utiliza mucho en CMS web (WordPress, Joomla, …), no es raro en entornos UNIX, pero sí lo es bastante en Windows.
En cuanto a los MO tienes mucha razón, es algo que no expliqué en el artículo. MO tiene algunas ventajas, pero una vez decidido que implementaría yo la gestión, manejar un MO era bastante complicado. Hay variantes en cuanto a encoding, el hash puede existir o puede no existir, y al final habría que lidiar todo con eso. El beneficio objetivo sólo es tener todas las cadenas de todos los idiomas en un mismo archivo, porque en cuanto a rendimiento, una vez parseado el PO en memoria es igual o mejor.
Tener todos los idiomas en un mismo archivo, lo veo como un inconveniente. Primero porque los traductores deberían compilar los PO, o sea requieren conocimientos y herramientas esopecíficas. Pero además eso me implicaría un trabajo extra, porque al final tendría que ser yo el que hiciera la compilación final. Simplemente, aunque MO tenía algunas ventajas, los inconvenientes no me compensaron.
Creo que no lo has entendido. Es un PO y MO por idioma, la diferencia es que el PO está orientado a la traducción y el MO es el «binario» donde las cadenas original y traducción no van por parejas, sino que tienes un bloque con las cadenas originales y otro con la traducción. Repito, no tienes varios idiomas en un MO, sólo uno.
De todas formas si el programa hace una carga en memoria, la ventaja es mínima.
Pues no sólo es que no te hubiera entendido Fernando, sino que recordaba que desde el principio los .mo encapsulaban varios idiomas, pero veo que no es así. Gracias por el apunte. Suponía que msgfmt compilaría diferentes PO en un MO, pero sólo convierte un PO en un MO.
Ya actualice Fileoptimizer en la Pc del trabajo, por cierto ese windows está en inglés. Me aparece la opción de usar idioma del sistema e inglés. Veo la carpeta de instalación y hay un archivo po para español,pero fileoprimizer no lo detecta. Saludos.
Manuel, ¿podrías enviarme una captura de la lista desplegable de idiomas dentro de las opciones para ver los que está detectando?