vendredi 30 décembre 2011

Supporting i18n inside Javascript backend JAVA

Résumé :

Quand je code du javascript avec du .jsp (ou .gsp ou .ceQueJeVeux) j'aime bien écrire mon code dans une classe javascript. Mais quand je commence à toucher à l'internationalisation, je suis tenté (30 secondes) de mettre mon javascript tout moche dans mes belles pages afin de pouvoir utiliser les resources i18n.

Cet Article va vous montrer trois façons différentes de pouvoir gérer cette problématique plus ou moins proprement. La troisieme technique vous permettra de plus de briller en société (ou pas).

J'exclus toute idée de faire de l'ajax pour faire ces traduction parce que franchement ça peut etre vite crade et faire plein de requêtes pas franchement utiles.

1 - Du javascript dans mes pages...

La premiere idée est la plus simple mais sans doute la moins bonne. L'idée est de faire une page qui contient que les traductions des éléments javascript et l'importer dans nos pages (quelque soit la technique)

Une telle page peut resembler à ca :

<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>

<script type="text/javascript">
var i18n = new Object();
i18n.disclaimerTitle = "<fmt:message key="disclaimer.title"/>";
i18n.cancelDisclaimer = "<fmt:message key="disclaimer.cancelButton"/>";
i18n.confirmDisclaimer = "<fmt:message key="disclaimer.confirmButton"/>";
i18n.serverTimeOut = "<fmt:message key="serverTimeOut"/>";
i18n.pleaseWait = "<fmt:message key="pleaseWait"/>";
</script>


Ensuite dans son javascript on à plus qu'a utiliser notre objet par exemple : <script type="text/javascript">
alert(i18n.disclaimerTitle); </script>

C'est bien parce que :

  • C'est simple à mettre en place.

C'est nul parce que :

  • On récupère du coup toutes les traductions même si on en a pas besoin.
  • Nos javascript ne fonctionnent pas sans la traduction (ca peut etre résolu je vous laisse réfléchir comment)

2- Du javascript généré à la volée.

L'idée c'est d'appeler le javascript en passant par une servlet (ou un autre truc du genre).

Notre servlet à plusieurs responsabilités.
  • Récupérer la langue (ça peut être un paramètre de la requête)
  • Récupérer le javascript non traduit
  • Traduire les parties à traduire
  • Renvoyer le fichier
  • Eventuellement, géré un cache
pour traduire les parties à traduire on peut utiliser se bout de code     protected String translateJS(String jsString, String lang) {
        Pattern pattern = Pattern.compile("i18n\\{[a-zA-Z0-9.]*\\}");
        Matcher matcher = pattern.matcher(jsString);

        Boolean find = false;
        StringBuffer translatedJsString = new StringBuffer();
        while (matcher.find()) {
            find = true;
            String key = matcher.group().substring(5, matcher.group().length() - 1);
            matcher.appendReplacement(translatedJsString, translate(key, lang));
        }
        if (!find) {
            translatedJsString.append(jsString);
        }

        return translatedJsString.toString();
    }


Le test unitaire qui va avec pour mieux comprendre : public class TestI18nJavascriptServlet {
    private I18nJavascriptServlet i18nJavascriptServlet;

    @Before
    public void initMock() {
        i18nJavascriptServlet = Mockito.spy(new I18nJavascriptServlet());
        Mockito.doReturn("test1-en").when(i18nJavascriptServlet).translate(Mockito.eq("test1"), Mockito.eq("en"));
        Mockito.doReturn("test2-en").when(i18nJavascriptServlet).translate(Mockito.eq("test2"), Mockito.eq("en"));

    }

    @Test
    public void testNoTranslation() {
        String js = "no translation'";
        Assert.assertEquals(js, i18nJavascriptServlet.translateJS(js, "en"));
    }

    @Test
    public void testSimpleTranslation() {
        String js = "translation for i18n{test1}'";
        Assert.assertEquals("translation for test1-en", i18nJavascriptServlet.translateJS(js, "en"));
    }

    @Test
    public void testMultiTranslation() {
        String js = "translation for i18n{test1} and i18n{test2}'";
        Assert.assertEquals("translation for test1-en and test2-en", i18nJavascriptServlet.translateJS(js, "en"));
    }

}

C'est bien parce que :

  • On récupere les traductions au besoin.
  • Les pages javascript sont fonctionnelles en static mais pas top (on à pas de traduction par défaut. Quoi qu'on peut reprendre l'idée du chapitre suivant).

C'est nul parce que :

  • C'est un peut chiant à mettre en place.
  • Ca me permet pas de briller en société

3- Du javascript généré à la volée version spring EL.

C'est presque la même solution que la précédente mais elle devient un peu plus élégante. Au lieu de faire du parsing comme précédement, on va utiliser les expression langage de Spring (SpEL)

Le code type POC que j'explique apres:

    public class TemplateParserContext implements ParserContext {
        public String getExpressionPrefix() {
            return "i18n(";
        }
        public String getExpressionSuffix() {
            return ")";
        }
        public boolean isTemplate() {
            return true;
        }
    }

    @Test
    public void elParser() throws SecurityException, NoSuchMethodException {
        String jsTextToTranslate = "alert(i18n.translate('hello','Salut les filles')))";
        jsTextToTranslate = jsTextToTranslate.replaceAll("i18n.translate", "i18n(#translate");
        ExpressionParser parser = new SpelExpressionParser();
        StandardEvaluationContext context = new StandardEvaluationContext();
        context.registerFunction("translate", TestI18NSpelJavascript.class.getDeclaredMethod("translate", String.class, String.class));
        String translated = parser.parseExpression(jsTextToTranslate, new TemplateParserContext()).getValue(context, String.class);
        Assert.assertEquals("alert(\"translated:hello->Salut les filles\")", translated);
    }

    public static String translate(String key, String defaultValue) {
        return '\"' + "translated:" + key + "->" + defaultValue + '\"';
    }
WTF ? me direz vous...

  • l'inner classe me sert à définir pour SPel l'endroit ou chercher les expression langages : dans mon cas je dis que c'est dans une chaine i18n(monEl)
  • j'enregistre une function translate pour le parser SpEL qui comme son nom l'indique permet de faire le travail de traduction.
  • La transformation du javascript avec le replaceAll me permet d'avoir du coté javascript original : i18n.translate(maclef,matraductionparDefault) et du coté du javascript à transformer via el : i18n(#translate(maclef,matraductionparDefault)). Car coté SpEL, une fonction commence toujours par # (c'est le code antlr généré donc y a pas moyen de modifier ça à la volée)
  • je fais gaffe que ma méthode translate me renvoie une string au sens js. (les petites quotes qui vont bien)
  • J'importe dans ma page original "i18n.js" qui défini un objet i18n qui possède la fonction translate qui renvoie la défault value

C'est bien parce que :

  • On récupere les traductions au besoin.
  • Les pages javascript sont fonctionnelles en static.
  • On peut envisager de faire d'autres choses via SpEL même si je n'ai pas d'idée comme ça.  (si on s'en fous que le js ne marche plus en static sinon ... )
  • Ca me permet de briller en société

C'est nul parce que :

  • Il faut spring.
  • C'est quand même un petit peu beaucoup
/// TODO finish translation Summary When i work with javascript + .JSP (or .gsp or .whatEverYouWant), i like to write my javascript code inside js class... but when internationalization involved, we put js code inside our pages in order to use java i18n ressources.

Aucun commentaire :

Enregistrer un commentaire