GWT UiBinder et i18n, vous avez dit un cauchemard ?

GWT avec nuiton-i18n (et Maven) et ça coule de source

Le fonctionnement classique de l'i18n dans GWT

Si vous avez des écrans que vous avez créé avec UiBinder et que vous souhaitez les internationaliser, vous aller vous tourner vers la page ad-hoc de la documentation de GWT (http://code.google.com/intl/fr/webtoolkit/doc/latest/DevGuideUiBinderI18n.html). Dans cette documentation, on vous apprend que vous devez utiliser, pour l'internationalisation, la syntaxe suivante :

  <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
    ui:generateFormat='com.google.gwt.i18n.rebind.format.PropertiesFormat'
    ui:generateKeys="com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator"
    ui:generateLocales="default">
    <div><ui:msg description="Greeting">Hello, world.</ui:msg></div>
  </ui:UiBinder>

Cela vous donnera un magnifique fichier de propriété qui ressemble à ça :

  # Generated from my.app.HelloWorldMyBinderImplGenMessages
  # for locale default
  # Description: Greeting
  022A824F26735ED0582324BE34F3CAE1=Hello, world.

Ok, cool, mais ce beau fichier de propriété, vous le donnez à un traducteur, il vous regarde avec des gros yeux, il claque la porte et vous le revoyez jamais. D'autant plus que vous allez avoir un fichier de propriété par fichier UiBinder. Oui oui, vous avez bien lu.

Bon admettons, on va chercher à améliorer ce fichier de propriétés alors. Déjà, en changeant le générateur on arrive à avoir de beaux fichiers. Bien. Mais on a toujours pleins de fichiers de propriétés, qui en plus sont générés dans pleins d'endroits différents. Un beau cauchemard.

En cherchant un peu sur le net, on voit que si on met toutes nos chaines dans un fichier LocalizableResources.properties, on n'a plus besoins de tous les autres ficheirs de propriétés. On trouve un script python qui nous met tout ça en un seul endroit mais vous vous voyez utiliser un script python a l'heure de l'automatisation de toutes les taches répétitives. Un script python, alors que vous faites du Java !

Si vous êtes arrivés jusqu'ici et que vous avez persévéré, vous voyez bien que l'internationalisation en utilisant UiBinder est un cauchemard.

Le fonctionnement attendu

Bon, et si on se posait les bonnes questions. Qu'est ce que veulent toutes les personnes impliquées dans la boucle ? Les développeurs ? Les traducteurs ?

Les développeurs, ils veulent surtout en écrire le moins possible. Répéter les informations de génération en haut de chaque fichier c'est un peu répétitif et, comment dire, pénible. Appliquer un script python c'est pas optimum, si on pouvait faire ça avec un outil de gestion des builds (comme Maven) ça serait bien quand même. Le développeur ne doit faire que coder, toutes les manipulations de fichier doivent être laissées à l'outil de gestion de build.

Les traducteurs, ils veulent des fichiers facile à manipuler, au maximum un fichier par langue. Il faut donc grouper les traductions de tous les fichiers. Il ne faut aps aller chercher les nouvelles clés dans le code. Tout doit être dans le fichier de propriété, seule la traduction doit être à la charge du traducteur.

Donc en résumé, on veut que les chaînes soient extraites de façon automatisées et placées dans un fichier de propriété unique (un par langue). Il faut que l'on conserve les chaînes déjà traduites d'une version à l'autre. Il faut que la configuration de la génération soit centralisée et que cette génération soit gérée par l'outil de gestion de build.

Et comment on s'y prend ?

Le processus de build peut déjà être géré par Maven (http://mojo.codehaus.org/gwt-maven-plugin/), on va donc se baser sur Maven, d'autant plus que la librairie nuiton-i18n (http://maven-site.nuiton.org/i18n/) fournit un plugin qui gère les clés de traduction et leur extraction déjà très bien. On va donc se baser sur ces deux outils.

On va considérer que vous avez réussi à compiler/lancer votre projet avec Maven pour se consacrer à ce qui nous intéresse, la traduction de nos fichiers UiBinder. On va donc utiliser nuiton-i18n et plus particulièrement le parser xml avec nos propres règles.

Création de notre règle de parsing

Nuiton-i18n fournit un parser XML qui est très puissant et peut être configuré par des règles externes. Nous allons donc créer une règle pour parser les fichiers UiBinder de GWT. Notre règle s'appelera 'gwt.rules' et sera placée dans notre répertoire *src/main/i18n*. Son contenu sera uniquement :

//ui:msg/@key

La valeur est du jxpath indiquant que l'on recherche tous les attributs key des éléments ui:msg. Jusque là, pas de soucis. Vous pourrez améliorer cette règle si vous avez plus de champs à traduire, mais elle devrait suffire à la plupart des cas.

Configuration du plugin

Nous allons configurer le plugin nuiton-i18n pour arriver à nos fins.

Dans une première exécution, nous allons lui dire de parser nos fichiers UiBinder (*.ui.xml) situés dans notre dossier src/main/java en utilisant la rule créée précédemment. Il ne faut pas oublier de préciser les namespace pour que le plugin s'y retrouve dans les path.

Dans une deuxième execution, on génère les fichiers de propriété qui seront situés dans src/main/resources/i18n.

Dans une dernière exécution, on génère le bundle. On précise le fichier et répertoire de sortie, soit LocalizableResource (les langues et le .properties sont ajoutés automatiquement) et WEB-INF/com/google/gwt/i18n/client dans le répertoire de votre hostedWebapp (ici src/main/webapp).

<plugin>
  <groupId>org.nuiton.i18n</groupId>
  <artifactId>i18n-maven-plugin</artifactId>
  <executions>
    <execution>
      <id>scan-gwt-sources</id>
      <goals>
        <goal>parserXml</goal>
      </goals>
      <configuration>
        <basedir>${project.basedir}/src/main/java</basedir>
        <includes>**/*.ui.xml</includes>
        <userRulesFiles>
          <file>${basedir}/src/main/i18n/gwt.rules</file>
        </userRulesFiles>
        <namespaces>
          <gwt>urn:import:com.google.gwt.user.client.ui</gwt>
          <ui>urn:ui:com.google.gwt.uibinder</ui>
        </namespaces>
      </configuration>
    </execution>
    <execution>
      <id>gen</id>
      <goals>
        <goal>gen</goal>
      </goals>
    </execution>
    <execution>
      <id>make-bundle</id>
      <goals>
        <goal>bundle</goal>
      </goals>
      <configuration>
        <generateDefinitionFile>false</generateDefinitionFile>
        <addBundleOuputDirParent>false</addBundleOuputDirParent>
        <bundleOutputPackage>com/google/gwt/i18n/client</bundleOutputPackage>
        <bundleOutputName>LocalizableResource</bundleOutputName>
      </configuration>
    </execution>
  </executions>
</plugin>

Premier build

Lancez un premier build, vous allez avoir vos fichiers de propriétés vides qui vont venir se placer dans votre répertoire src/main/resources/i18n. Remplissez-les (pensez à bien échapper les caractères spéciaux en les remplaçant par leur code unicode.

Second build, c'était compliqué ?

Lancez un deuxième build, lancez votre application. Vos langues sont disponibles ? Bien... C'est toujours un cauchemard ?