Incluir ficheros DFM condicionalmente

En los últimos meses he participado en la traducción de un programa hecho con Delphi. Conté por aquí los problemas del Translation Manager incluido con Delphi hasta el punto de recomendar que no se use.

En esa última historia contaba cómo terminé por crear una herramienta propia que usa el parser de DFM de la VCL para filtrar los ficheros DFM y sustituir las cadenas de acuerdo a un diccionario. El resultado final es un juego de ficheros DFM en catalán e inglés.

La primera idea fue hacer un script que copiase los fuentes del proyecto a otro directorio, machacase sus DFM con los traducidos y compilase la versión en catalán o inglés. Pero finalmente se me ocurrió la manera de ponerlo todo junto y controlar el idioma con un fichero incluido y $define.

En principio parece fácil. Todas las units que tienen un DFM asociado llevan la siguiente línea, generada automáticamente por el IDE:

{$R *.dfm}

La directriz del compilador R se usa para incluir ficheros de recursos. Los DFM originales eran siempre ficheros de recursos (los que suelen tener la extensión *.RES) con la definición del formulario metida en un bloque binario del tipo RC_DATA. A partir de Delphi 5 (creo) existe la posibilidad de guardar los DFM en disco en formato texto, aunque antes también era posible acceder a ese texto con Alt-F12 o a través del portapapeles. Sea como sea, $R sigue sirviendo para incluir el DFM, aunque sea en formato texto. El asterisco representa el nombre del fichero actual, así que *.dfm en unit1.pas se referiría a unit1.dfm.

Primer intento

Lo más sencillo parece hacer un simple condicional. Por una parte tendríamos la enumeración de idiomas:

{$DEFINE CASTELLANO}
{ DEFINE CATALAN}
{ DEFINE INGLES}

Al faltar el signo del dólar, sólo la primera línea es válida. Pero es fácil cambiar el signo de sitio.

Después podemos usar un condicional. Parece lógico usar lo siguiente:

{$IFDEF CASTELLANO}{$R *.dfm}{$ENDIF}
{$IFDEF CATALAN}{$R *.cat}{$ENDIF}
{$IFDEF INGLES}{$R *.eng}{$ENDIF}

Pero no funciona. Al menos Delphi 6 no se traga como DFM un fichero que tenga extensión .CAT. Creo que el error era algo sobre el formato. En cualquier caso se puede arreglar así:

{$IFDEF CASTELLANO}{$R *.dfm}{$ENDIF}
{$IFDEF CATALAN}{$R *.cat.dfm}{$ENDIF}
{$IFDEF INGLES}{$R *.eng.dfm}{$ENDIF}

Bien, esto funciona. Pero es muy poco práctico tener el define de idioma en cada uno de las units con DFM asociado. Nueva idea: poner todo en un fichero incluido y dejar sólo la directiva de inclusión:

{$I IDIOMA.INC}

Donde lógicamente habríamos puesto lo siguiente:

{$DEFINE CASTELLANO}
{ DEFINE CATALAN}
{ DEFINE INGLES}
 
{$IFDEF CASTELLANO}{$R *.dfm}{$ENDIF}
{$IFDEF CATALAN}{$R *.cat.dfm}{$ENDIF}
{$IFDEF INGLES}{$R *.eng.dfm}{$ENDIF}

La peculiaridad

Esa simple línea no funciona. La peculiaridad de los DFM es que el entorno va a su bola buscando la directriz {$R *.dfm} para saber si tiene que asociar el DFM. El entorno no entra en los ficheros incluidos, así que nos quedaríamos sin poder usar el diseñador de formularios.

Nuevo intento. Pongo no uno, sino dos ficheros incluidos así:

{$DEFINE CASTELLANO}
{ DEFINE CATALAN}
{ DEFINE INGLES}

{$IFDEF CASTELLANO}

Aquí en medio queda el {$R *.dfm}

{$ENDIF}
{$IFDEF CATALAN}{$R *.cat}{$ENDIF}
{$IFDEF INGLES}{$R *.eng}{$ENDIF}

En el principal tendríamos algo así:

{$I IDIOMA.INC}{$R *.dfm}{$I IDIOMAFIN.INC}

Por desgracia a Delphi tampoco le sienta bien que se deje un bloque de compilación condicional abierto entre ficheros.

Lo que sí funciona

Supongo que se puede resolver de varias formas. Lo que he puesto es una línea ligeramente más larga que la anterior.

{$I IDIOMA.INC}{$IFDEF CASTELLANO}{$R *.dfm}{$ELSE}{$IDIOMAFIN.INC}{$ENDIF}

El fichero IDIOMA.INC contiene sólo los defines de idiomas:

{$DEFINE CASTELLANO}
{ DEFINE CATALAN}
{ DEFINE INGLES}

Mientras que el segundo incluido lleva esto:

{$IFDEF CATALAN}{$R *.cat}{$ENDIF}
{$IFDEF INGLES}{$R *.eng}{$ENDIF}

A pesar el condicional el IDE siempre va a pillar el fichero *.dfm, que es lo que veremos siempre en tiempo de diseño, mientras que el compilador escogerá el que se decida en idioma.inc.

Finalmente podemos meter todos los DFM de todos los idiomas en el mismo directorio y controlar el idioma de compilación modificando un solo fichero.

Comentarios

Mi experiencia al respecto

Yo llevo un par de días estudiando el asunto de traducir algunas aplicaciones Delphi a otros idiomas (bueno, en principio sólo a portugués) y esto es lo que te puedo contar:

He probado a usar el gettext, y lo que no menos me gusta es la herramienta para hacer la traducción (podría intentar montar alguna distribución Linux para probar la de KDE, que tiene fama de ser la mejor). Además, me imagino que tengo algo mal instalado, y me faltan funcionalidades con respecto a lo que veo en la documentación. También me tengo que preocupar de que no se traduzcan determinadas propiedades de determinados componentes (como FieldName, DatabaseName, etc)

He probado algunas herramientas comerciales, y de todas ellas la que más me ha gustado es sisulizer. La parte buena es que permite editar los forms de forma visual. La parte mala es que este trabajo se hace a partir del exe (no del código fuente original) y que lo que hace es simular los componente visuales. Cuando me he encontrado con alguno que no está soportado, pues "hasta aquí hemos llegado". Hay la opción de montar algo para que el programa reconozca esos componentes, pero claro, te lo tienes que currar tú (de algunos componentes de pago ya se ha preocupado el fabricante de hacer ese trabajo, pero a ver quién se encarga de hacer todo eso con los componentes de la Jedi, máxime cuando ellos "apuestan" por gettext para este tema)

Al final he probado el ITE de Dephi 2007, que tiene como cosa buena el que es parte del entorno, y puedes acceder a todos los componentes desde el mismo. Como cosa "chunga", pues que la edición visual parece que no es todo lo potente que debería ser. Por ejemplo, a la hora de trabajar con un control multihoja, no puedes cambiar la pestaña activa para editar los componentes de las otras hojas. Además, no sé si es por tener "sólo" 1 GB de RAM, pero demasiadas veces me ha pasado que el Delphi 2007 empieza a chupar cpu y memoria como loco y, al final, peta (realmente desaparece por completo el proceso, sin ni siquiera mostrar un triste mensaje de error) y he perdido parte del trabajo (que, de momento, son sólo pruebas, menos mal)

Así que ya ves, ando un poco descontento con todo. Lo que sí tengo claro es que la solución óptima (para mi caso concreto, todo es código Delphi) debe estar en algo integrado en el entorno, es la única manera de poder editar visualmente todas las propiedades que haga fata, pero el ITE no me acaba de convencer.

La solución que se me ha ocurrido, y que hoy pienso probar, es aprovechar el ITE para tener un repositorio de las cosas comunes (y, así, no repetir la traducción de algunos forms comunes a varios proyectos). Eso sí, para editar los forms en portugués (al menos ahora, que tengo que hacer todo el trabajo, empiezo de cero el proceso de traducción) copiaré el .dfm de cada idioma a otro directorio, pondré el .pas bueno, y abriré ese form en el entorno como si fuera un fichero externo al proyecto. Así, modificaré los componentes/propiedades con todas las facilidades que me da Delphi. Cuando acabe, vuelvo a copiar el .dfm al directorio correspondiente y que todo siga su curso. Supongo que, al hacer esto, el ITE no me dirá si una propiedad ya ha sido traducida o no, pero creo que ganaré bastante en comodidad. Como mencioné antes, para mí es esenciar el poder editar visualmente los forms, con la cantidad de textos que tengo, sería un desastre tener que compilar para poder ver si todo queda bien alineado.

Lo que no me ha convencido de tu solución es que, además de los .dfm deberás tener otros ficheros de recursos para controlar los textos que se usan en el código, o bien un .pas con unos cuantos {$IfDef ...} para definir esos textos. Además, se basa en "engañar" al entorno y, cuando debas hacer cambios a los forms, tendrás bastante trabajo para replicarllos en los otros .dfm

Lo que hacen mejor algunas herramientas comerciales es que, con tu solución tienes 3 programas diferenes, uno en cada idioma; la solución del ITE es un exe con varias dlls para cambiar de idioma; con localizer puedes tener un único exe que tenga autocontenidos los recursos para cambiar de idioma. Es decir, tienes un único exe multilenguaje. El ITE es algo intermedio, tienes un único exe y varias dlls (una para cada idioma adicional) el sistema de GNU gettext es parecido, aunque en lugar de dlls tiene ficheros de traducción.

En fin, ya suponía que la solución perfecta (para mis necesidades) no existía, pero también debo confesar que esperaba algo más de algunas herramientas comerciales (después de haber visto el precio) Lo cierto es que con todas ellas parece muy fácil traducir un "hola mundo" con 2 botones y un menú, pero cuando te sales de ahí me he encontrado con que no es que ayuden especialmente a acelerar el tema. Lo que veo mejor llevado es la posibilidad de tener separada la figura del programador de la del traductor, mantener repositorios de traducciones, facilitar la opción de cambiar el idioma sobre la marcha, comprobar qué llevo traducido y qué me falta, etc. Es decir, el control de las diferentes etapas del proceso, pero lo que para mí es básico, modificar visualmente las propiedades de los componentes, pues no lo veo muy conseguido en ninguna de ellas.

Bueno, perdona por el rollo que te he soltado. Espero que nos sirva para algo (a mí, al menos, me está ayudando a estructurar las ideas que tenía al respecto)

Un saludo.

Información incompleta

Antes que nada, gracias por el largo y detallado comentario. Me he enterado de unas cuantas cosas. Por ejemplo de que el problema con componentes "raritos" es común a todos los programas. Lo he mencionado en alguna historia anterior: fue un error de diseño dar tanta libertad a los programadores de componentes para usar y abusar del DFM.

Mi consejo es que no uses el ITE. Yo sí lo he usado para más que pruebas y no va. Si te metes por ese camino, vas a tener problemas y van a salir cuando sea demasiado tarde. El que avisa no es traidor :-) No sé si has visto el enlace, pero ya he escrito sobre el Translation Manager antes. Ese programa tiene agujeros por todas partes. El que he probado yo es el externo, pero el integrado no creo que sea mejor, al contrario.

Respecto a cómo lo hacemos, no he dado la información completa, y eso es en parte porque no hemos creado una herramienta completa, sino que se ha quedado a medias. A ver si puedo aclarar un poco más.

Lo de que se pueda cambiar en tiempo de ejecución en vez de tener tres ejecutables, es perfectamente posible. ¿Que por qué no lo hemos hecho así? La base de datos es distinta para cada idioma, así que no se puede tener ejecutables para los dos idiomas, sino que a cada cliente se le manda el del suyo. Hay una manera de cargar un DFM distinto en tiempo de ejecución (con TForm.CreateNew) o simplemente se puede usar el mismo sistema que el Translation Manager y generar una DLL distinta para cada idioma. CreateNew habría sido laborioso, ya que hubiese tenido que repasar el código para cambiar la forma de creación de los formularios. Lo de generar la DLL no tenía incentivo, aunque igual a ti sí te interesaría. Si es así, puedes echar un vistazo a lo que hace el Translation Manager y hacer lo mismo.

Lo que quieres de poder comprobar en el IDE si ha quedado bien sin compilar también es posible apañarlo con un .BAT. Cambias los DFM del idioma por los del castellano, repasas los formularios y después vuelves a ponerlo como estaba. Pero evidentemente ya estamos en un terreno resbaladizo. ¿Qué ocurre si cambias el tamaño de algo para hacer sitio a una cadena más larga? Es donde se manifiesta que tenemos una heramienta a medias.

A nosotros nunca nos ocurre el problema de los tamaños, siempre son cambios de cadenas por otras, será porque hay espacio y no están los controles apiñados. En caso contrario habría que tenerlo montado bien: tener una base de datos con todas las propiedades, valor original, un flag para decidir si se va a traducir o no, un repositorio aparte, etcétera. Después de cada operación, bien sea importando, bien sea editando visualmente, habría que examinar los cambios, detectar las diferencias y actualizar la base de datos. Todo es factible, pero es laborioso, el tipo de trabajo que se espera de una herramienta de pago. Pero ya ves cómo está la cosa :-)

Lo que dices de que con mi sistema hay que tener ficheros de recursos adicionales no lo veo. Eso lo necesitas para cualquier sistema que tengas. Si usas resourcestring, puedes meter las cadenas en una DLL de idioma. Pero de nuevo: eso lo puedes hacer de todas formas.

En fin, no sé qué más te puedo contar, salvo que la herramienta del KDE en efecto está bien (la usé con Drupal hace tiempo) y que si quieres te envío el código del filtro de DFM como está (incompleto).