casid / jte

Secure and speedy templates for Java and Kotlin.
https://jte.gg
Apache License 2.0
737 stars 54 forks source link

internationalization with MessageFormat #321

Closed maxwellt closed 6 months ago

maxwellt commented 6 months ago

In order to do internationalization I have implemented the LocalizationSupport interface as per the instructions in the JTE documentation.

The issue I'm facing is that if you want to make use of Java's MessageFormat, which can have a message value like:

Your total is {0, number, currency}

If you use the LocalizationSupport the method you need to implement does the message key lookup, the returned value will then be the message above, but LocalizationSupport doesn't know how to interpret this.

If I bypass the LocalizationSupport and use writeContent for messages without params and writeUserContent for messages with params I don't get the advantage that LocalizationSupport offers, which will only use writeUserContent when substituting the placeholders for their values, which means a message like Hello <b>{0}</b> becomes impossible because the message contains variables so I would write the complete message using writeUserContent.

Is there any way I can get the best of both worlds?

casid commented 6 months ago

At work we have extra methods that localize a number, money, currency etc., which has the benefit that you don't need to know the oddities of message support. Also, if you change the parameter format you don't have to adjust that for all translations.

For instance, the key would simply be this:

total.key=Your total is {0}

And used like this:

${localize("total.key", localize(money))}
maxwellt commented 6 months ago

Sure, that's certainly one way to go, but when using something like ChoiceFormat it make it slightly harder again (not impossible but it would feel a bit hacky):

You have {0, choice, 0#no messages|1#a message|2#two messages|2<{0, number, integer} messages}.

printing out:

localize("key", 0) -> You have no messages.
localize("key", 1) -> You have a message.
localize("key", 2) -> You have two messages.
localize("key", 3) -> You have 3 messages.
casid commented 6 months ago

Oh, that's right. In fact, we have a localizePlural() exactly for this purpose..

Maybe you could implement only that using MessageFormat?

Otherwise you could try to create some kind of MessageFormatAwareLocalizationSupport.

maxwellt commented 6 months ago

I will look into it if it's possible and report back here :-).

maxwellt commented 6 months ago

I had a look at it and came to the conclusion that the only thing I really need to do is HtmlEscape the incoming params myself and then sending the sanitizedParams to the Java MessageSource, in that case there's no need for the LocalizationSupport anymore as that's basically what it does for me.

So this is what my JteContext method looks like that can be called from a template:

public static Content get(String key, Object... params) {
    Object[] sanitizedParams = Arrays.stream(params)
            .map(param -> StringEscapeUtils.escapeHtml4((String) param))
            .toArray();

    // it's safe to use writeContent because the variables have been sanitized
    return (output) -> output.writeContent(TranslateService.getMessage(key, sanitizedParams));
}
casid commented 6 months ago

Nice, yes that should do the trick.