Blog-traduction.jpg

Olivier Bonnet-Torrès / FAQ, Insight
23/01/24 09:24

"Les traductions dans Liferay, pour moi c'est du chinois!"

Qui serait surpris d'une telle exclamation? La gestion de l'internationalisation (i18n) et de la localisation (l10n) est une plaie en général, d'autant que la plupart des produits (et Liferay n'y échappe pas) sont bâtis sur une vision anglophone de la vie et donc des interfaces.

Petit retour: de quoi parlons-nous?

L'internationalisation (que nous appellerons indifféremment traduction dans la suite) correspond à la mise en place d'outils qui découplent le code applicatif des messages d'interface utilisateur. Ceux-ci peuvent alors être traduits, indépendamment de la logique applicative. Ils sont habituellement stockés sous forme de fichiers, et en Java, on parle de fichier au format "properties" (typiquement une liste <clé-technique>=<valeur traduite>). Dans Liferay, ce sont les fichiers language.properties et language_<locale>.properties. La locale, c'est un ensemble de paramètres de langue (notamment l'alphabet, la représentation des nombres, le sens de lecture, l'encodage par défaut des caractères...) désigné par 2 lettres pour la langue, puis 2 lettres optionnelles pour le pays, voire un autre groupe pour désigner des variantes régionales! Par exemple: fr, zh, en_US, fr_CA, sr_RS_Latin ou ca_ES_Valencia. Vous pouvez consulter l'article Wikipedia (en anglais) sur la locale ou celui (en français) sur les paramètres régionaux.

La localisation, c'est l'étape d'après: il s'agit d'adapter l'interface aux usages culturels de la cible du site... Bon, Liferay propose des mécanismes pour cette partie, comme les expériences, les segments, mais cela reste au-delà de notre sujet du jour.

Dans ce propos liminaire, nous avons donc évoqué un premier outil d'internationalisation: les fichiers language.properties. Il s'agit d'une solution de "développeur", correspondant à une vision plutôt figée (hard-codée) du sujet. Liferay propose depuis la version 7.3/7.4 un mécanisme de surcharge: Language translation overrides. Nous reviendrons sur son fonctionnement dans un second temps.

Le fonctionnement des fichiers language.properties, côté back: un outil efficace, et délicat

Les fichiers language.properties suivent le cadre des Properties de Java. Il s'agit donc de fichiers texte, encodés par défaut en ISO-8859-1, composés de couples <clé>=<valeur> disposés par ligne.

Exemple pour un fichier de traduction pour la langue française: Language_fr_FR.properties

hello-world=Bonjour le Monde!
my-profile=Mon profil

Le même en anglais: Language_en_GB.properties

hello-world=Hello World!
my-profile=My profile

Comment fonctionne le chargement des fichiers de langue? Les fichiers eux-mêmes sont chargés sous la forme de ResourceBundles, via le chargeur d'OSGi. Mais une partie de la magie opère dans le filter I18nFilter (module du portail portal-i18n-filter). C'est ici que le choix de la langue appliquée pour la requête est réalisé. Ce filtre de servlet s'applique donc sur toutes les requêtes et donne la priorité pour l'attribution de la langue selon l'ordre suivant:

- locale de la session

- locale de l'utilisateur

- locale stockée dans les cookies

- locale par défaut du site.

A partir de là, le reste se passe dans LanguageUtil et son implémentation LanguageImpl, à partir de ResourceBundleUtil, lui-même permettant d'accéder aux ressources OSGi du module! Ouf!

Petite note en passant: par le passé, c'est-à-dire avant OSGi (donc sur les versions antérieures à la version 7.0), le chargement passait par le framework Properties de Java. Une sensibilité particulière au chargement était de mise, avec en particulier l'impossibilité de prendre en compte des propriétés chargées dynamiquement par un hot deploy de webapp (oui, c'est ça, les WARs). Alors merci OSGi, qui permet un chargement dynamique plus propre des ressources des modules.

Retour aux traductions. Le ResourceBundle étant lié à un bundle OSGi, le chargement des traductions est donc limité au module lui-même. Ceci limite les interférences entre fichiers de traductions, et c'est bien. A savoir, les fichiers déclarés dans le module prennent précédence sur les fichiers du portail. Le défaut de l'approche est que les traductions doivent être répétés d'un module à l'autre, sauf à utiliser un bundle global, dont tous les autres modules dépendent...

Autre point particulier, qui peut montrer les limites des fichiers de traduction: le nommage des clés de traduction. Plusieurs stratégies existent: soit utiliser la traduction anglaise en normalisant la clé (stratégie privilégiée par Liferay), soit mettre en place une stratégie "sémantique" par laquelle on désigne plutôt l'usage de la clé. Le problème de la première approche se matérialise par les différences culturelles: en particulier les homonymes homographes. Par exemple, la clé vote désigne le mot lui-même en anglais, alors que le français pourra correspondre suivant les cas "Vote", "Voter", "Votez"... Vous voyez le topo? Pour cela, nous préconisons la seconde approche...

 

Le fonctionnement des Language translation overrides: ça se corse

Liferay 7.3 a introduit la notion de surcharge des traductions.


 

Cette fonctionnalité est présente au niveau de l'administration du serveur, et donc commune à l'ensemble d'une compagnie.

Son fonctionnement est assez simple: il s'agit d'un tableau de toutes les surcharges déclarées, stocké en base de données. Lors de l'application d'une traduction, le tableau entier est parcouru à chaque fois. 

Vous voyez le problème? Oui, c'est ça! Les performances se dégradent assez vite et finissent par ralentir notablement le temps de chargement de la page lorsque les surcharges sont massivement utilisées.

Alors, oui pour corriger temporairement une traduction erronée. Mais la résolution doit être appliquée directement sur les fichiers de propriétés et supprimée des surcharges. L'écueil serait d'utiliser ce mécanisme pour "donner la main au contributeur afin de gérer ses traductions". CE serait une fausse bonne idée.

 

Le fonctionnement des fichiers language.properties, côté front: le piège se referme

Passons maintenant du côté obscur de la force. Une fois n'est pas coutume, ce sera le front-end cette fois!

Les "appels" à Liferay.Language.get utilisés dans le front-end sont en fait une mascarade. Effectivement, on se rend facilement compte de l'absurdité de charger des milliers de clés de traduction dans chaque page, ou pire de déclencher un appel d'API à chaque apparition du get.

Le fonctionnement de Liferay est effectivement différent de ces deux approches! En réalité, Liferay utilise un autre filtre de servlet: LanguageFilter. Celui-ci déguise un appel à LanguageUtil et donc LanguageImpl sur la méthode process. Le traitement est très simple: Liferay.Language.get(XXX) est utilisé en marqueur de remplacement et le LanguageImpl.get est appelé sur la clé ainsi identifiée.

Vous me direz: "malin!"... Certes, mais cela empêche d'avoir recours à une construction de la forme Liferay.Language.get(maCondition ? 'key1' : 'key2'). Alors quelles solutions pour le chargement dynamique d'une traduction? Deux possibilités:

- écrire maCondition ? Liferay.Language.get('key1') : Liferay.Language.get('key2')

- ou créer un objet LANGUAGE = {'key1': Liferay.Language.get('key1'), 'key2': Liferay.Language.get('key2')} puis utiliser LANGUAGE[maCondition ? 'key1' : 'key2']

On oubliera pour le moment la surcharge du filtre pour directement prendre en charge ce genre de construction. Cela ferait néanmoins un très bon exercice de kata sur l'utilisation d'un Lexer...

 

Conclusion

En espérant avoir démystifié le sujet, rappelons que les traductions dans Liferay sont basées sur l'utilisation de fichiers language.properties qui sont appliqués en fonction de la locale de l'utilisateur. Le fonctionnement en front-end (JavaScript) repose sur un parsing via un filtre côté back-end et interdit donc l'application dynamique d'une clé de traduction. Des solutions existent pour contourner cette particularité.

Partager cet article :
Lien copié

FAQ, Insight

Autres articles qui pourraient vous plaire…

Card image cap

/

Card image cap

/

Card image cap

/