vendredi 4 décembre 2009

Une seule traduction vous manque, et tout est cassé

Problème

Comme expliqué précédemment, je passe une bonne partie de mon temps de travail à développer Okawix, un navigateur en mode non connecté. Le logiciel est codé avec le Framework Mozilla, donc avec XUL/Javascript, et utilise aussi le système de traduction du Framework, enfin... les systèmes : des DTD pour la partie XUL et des fichiers properties pour Javascript.
Ces deux systèmes ont l'avantage d'être cohérents (genre, des DTD pour du XML, c'est la sauce, quoi), mais à l'usage présentent rapidement un inconvénient : toutes les chaînes doivent être traduites. Si une traduction manque dans un .properties, une exception sera levée à l'exécution ; bon là, c'est pas si grave, une exception ça peut encore se gérer. Si une traduction manque dans une DTD, les fichiers XUL utilisant cette traduction ne peuvent plus être affiché ; hu... là, ça peut faire mal.
J'ai donc pris rapidement l'habitude de traduire toutes les nouvelles chaînes en anglais et en français, les deux seules langues proposées par Okawix... avant. En effet, les traductions d'Okawix sont dorénavant gérées par le projet translatewiki.net (TWN), ce qui veut dire plein de traductions, dont certaines ne sont pas complètes ; ce qui veut aussi dire mise à jour de notre SVN automatiquement depuis TWN. De plus, Okawix créé automatiquement une liste des langues disponibles ; et pour finir, une fois une langue incomplète choisie il devient difficile de relancer le logiciel sans manipuler à la main la configuration du logiciel. Ouch...
Il fallait donc trouver une solution et après avoir demandé en vain à Google, j'en suis arrivé à la conclusion qu'il fallait que je trouve cette solution tout seul :)

Solution

L'idée est d'avoir une traduction "par défaut", qui est toujours complète et qu'on va utiliser pour combler les manques dans les autres traductions ; dans Okawix, la traduction par défaut est l'anglais.
On commence par déclarer un "content provider" pour la traduction par défaut dans le manifest de l'application, dans le cas d'Okawix, ça donne :
content defaultlocale locale/en-US/interfacewiki/
qui déclare donc un "content provider" nommé "defaultlocale" et qui pointe vers la traduction anglaise (au passage, promis, un de ces jours, je vire tous ces "interfacewiki" qui trainent un peu partout dans Okawix).
Pour la partie DTD, il faut alors charger la DTD de la traduction par défaut en dernier, dans Okawix ça ressemble à ça :
<!DOCTYPE window [
<!--ENTITY % wikiDTD SYSTEM "chrome://interfacewiki/locale/okawix.dtd"-->
<!--ENTITY % defaultDTD SYSTEM "chrome://defaultlocale/content/okawix.dtd"-->
%wikiDTD;
%defaultDTD;
]>
L'idée est que une entité ne peut déclarée qu'une seule fois, celles qui sont déclarées dans la traduction en cours sont chargées en premier et celles de la traduction par défaut ne font que "combler les trous".

On applique le même principe, ou presque, pour les fichiers properties :
<stringbundleset id="wk-strings-set">
<stringbundle src="chrome://interfacewiki/locale/okawix.properties"/>
<stringbundle src="chrome://defaultlocale/content/okawix.properties"/>
</stringbundleset>
On charge d'abord la traduction en cours, puis on "comble les trous", tout pareil, quoi... sauf que dans le cas du javascript, tout n'est pas terminé : il nous faut une fonction pour récupérer la traduction dans le stringbundleset. Ce qui donne a peu près ça :
my_translate_function = function( aBundleset, aString ) {
for( var i = 0 ; i < aBundleset.childNodes.length ; i++ ) {
var bundle = aBundleset.childNodes[ i ];
try {
return bundle.getString( aString );
}
catch( someerror ) {
}
}
return aString;
}
La fonction parcourt l'ensemble des stringbundle et appelle leur fonction getString, si la traduction existe elle est renvoyée, sinon une exception est levée, on l'attrape et on continue. Si la traduction n'existe nul part, on renvoie la chaîne d'origine.

Voila... je ne sais pas exactement ce que les puristes penseront de ce genre de méthode, mais au moins avec ça Okawix est capable de survivre à des traductions partielles :)