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.

mercredi 14 décembre 2011

Tdd is not enough


Je viens de faire un petit truc en TDD et je me suis rendu compte que mon code était tout crade par rapport à ce qu'on pouvait faire.


L'idée c'était de faire une methode qui transforme les sauts de ligne ASCII en paragraphe ou en saut de ligne HTML
ex

Lalala
lilili

toto
tata

se transforme en

<p>Lalala<br/>Lilili</p><p>toto<br/>tata</p>

c'est fou non ?


Bref pour faire ça je me suis dis que j'allais sortir mon copain le TDD. Alors je fais mes tests au fur et à mesure du dev et j'obtiens ce jeu de tests

    @Test
    public void testFormatDisclaimerForHTMLPrinting() {
        Assert.assertEquals("<p>test</p>", DisclaimerAction.formatDisclaimerForHTMLPrinting("  \ntest"));
        Assert.assertEquals("<p>test</p>", DisclaimerAction.formatDisclaimerForHTMLPrinting("  \ntest\n"));
        Assert.assertEquals("<p>test<br/>test</p>", DisclaimerAction.formatDisclaimerForHTMLPrinting("  \ntest\ntest"));
        Assert.assertEquals("<p>test</p><p>test</p>", DisclaimerAction.formatDisclaimerForHTMLPrinting("  \ntest\n   \ntest"));
        Assert.assertEquals("<p>test</p><p>test</p>", DisclaimerAction.formatDisclaimerForHTMLPrinting("  \ntest\n \n \n  \ntest"));
        Assert.assertEquals("<p>test</p><p>test</p>", DisclaimerAction.formatDisclaimerForHTMLPrinting("  \ntest\n \n \n  \ntest"));
        Assert.assertEquals("<p>test</p><p>test<br/>toto</p>",
                DisclaimerAction.formatDisclaimerForHTMLPrinting("  \ntest\n \n \n  \ntest\ntoto"));

    }

et surtout ce code tout crade :
    public static String formatDisclaimerForHTMLPrintingOld(String toFormat) {
        String[] lines = toFormat.trim().split("\n");
        String toReturn = "<p>";
        for (int i = 0; i < lines.length; i++) {
            if (StringUtils.isEmpty(lines[i].trim())) {
                toReturn += "</p><p>";
                while (i < lines.length - 1 && StringUtils.isEmpty(lines[i + 1].trim())) {
                    i++;
                }

            } else {
                toReturn += lines[i];
                if (i + 1 < lines.length && !StringUtils.isEmpty(lines[i + 1].trim())) {
                    toReturn += "<br/>";
                }
            }
        }

        return toReturn + "</p>";
    }

J'ai pas trop essayer de reflechir en écrivant ça. Juste de prendre le test qui marche pas et d'écrire le code qui le fait marcher.

Alors ce matin je regarde d'un oeil coupable ce code, je commence à réfléchir et j'écris ça :

public static String formatDisclaimerForHTMLPrinting(String toFormat) {
        String toReturn = toFormat.trim();
        toReturn = toReturn.replaceAll("\\s*\\n\\s*\\n\\s*", "</p><p>");
        toReturn = toReturn.replaceAll("\\n", "<br/>");
        return "<p>" + toReturn + "</p>";
}

plus concis, plus simple, plus joli... et il passe bien mes tests.


Dans le TDD y a bien cette phase de refactoring qui est précisée cependant c'est quand même un peu difficile parce qu'une fois que j'ai écris mon code crade j'ai beau temps de le laisser comme ça s'il y a une autre urgence.

hum, je ne sais quelle conclusion tirer de tout ça. Si ce n'est que l'approche originale de TDD est peut être un peu trop dogmatique et qu'il faut toujours réfléchir avant de coder (... arf toujours pas de solution contre ce mal).