quarkus-renarde icon indicating copy to clipboard operation
quarkus-renarde copied to clipboard

Add or test internationalisation

Open FroMage opened this issue 3 years ago • 13 comments

We should try to internationalise the sample TODO app and see if we have all the required bits from Qute/Quarkus in order to be able to set the language and get translations in the views.

https://www.playframework.com/documentation/1.4.x/guide12 can be used for inspiration, and for http://rivieradev.fr (code at https://github.com/FroMage/RivieraDEV/blob/master/app/controllers/Application.java#L55) we had these controller actions to change the language from the top menu:

    public static void fr(String url) {
        Lang.change("fr");
        redirect(url);
    }

    public static void en(String url) {
        Lang.change("en");
        redirect(url);
    }

So a way to change the current language from a Controller would be great. I'm pretty sure this sets a language cookie that overrides the browser language headers when set.

FroMage avatar Jan 26 '22 16:01 FroMage

Is a custom internationalization format preferred instead of using an existing one as GNU gettext format or XLIFF because of better integration in the Java or Quarkus world?

cdhermann avatar Jan 26 '22 16:01 cdhermann

Qute already has https://quarkus.io/guides/qute-reference#type-safe-message-bundles so we have to check if that is what we want or make it simpler if it has to.

FroMage avatar Jan 26 '22 16:01 FroMage

Ideas for improving type-safe messages/templates, and grouping them together.

Using records:

interface View {}

public class Application extends Controller {
	// This defines a template
    record Index(String name) implements View {
    	// This defines localisation with key `Application_Index_my_greeting`,
    	// available in the view as {i18n:my_greeting(name)} and outside as {i18n:Application_Index_my_greeting(name)}
    	void my_greeting(String name) {}
    }
    
    public View index() {
    	return new Index("Stef");
    }
}

Extending the current type-safe messages:

 public static class Application extends Controller {

        @CheckedTemplate
        public static class Templates {
            public static native TemplateInstance index(String name);
            @MessageBundle
            interface index {
                @Message("fr", "bla bla {name}")
                @Message("en", "yada yada {name}")
                String my_greeting(String name);
            }
        }

        public TemplateInstance index() {
            return Templates.index("Stef");
        }
}

The current solution:

@MessageBundle
interface Messages {
    @Message
    String views_application_index_my_greeting(String name);
}

public class Application extends Controller {

    @CheckedTemplate
    public static class Templates {
        public static native TemplateInstance index(String name);
    }

    public TemplateInstance index() {
        return Templates.index("Stef");
    }
}

A more nested version:

public class Application extends Controller {

    @CheckedTemplate
    public static class Templates {
        public static native TemplateInstance index(String name);
        @MessageBundle
        interface index {
            @Message
            String my_greeting(String name);
        }
    }

    public TemplateInstance index() {
        return Templates.index("Stef");
    }
}

FroMage avatar Feb 17 '23 09:02 FroMage

I need to look at the records idea. It's quite interesting.

@FroMage Would it help in the mean time if we change the default name of a bundle declared in a nested class? Currently, it's always msg. We could say that for a nested class the simple name of the class is used together with _msg. So that you could use something like index_msg:my_greeting('Stef') in the template. Or even Application_index_msg:my_greeting('Stef') if we want to include the names of declaraing classes. I think that the last proposal makes sense. WDYT?

mkouba avatar Feb 20 '23 09:02 mkouba

The reason why ATM my application has all keys prefixed with views.Controller.method. is that I define them all in a single messages.properties file. But this makes them hard to use in views, because each view could have a default that makes the views.Controller.method. optional (since we know the view path).

I understand you proposal, and it makes a lot of sense to make them available as Application_index:my_greeting, but mostly for accessing them outside the Application/index.html view, no? For that one, an implicit msg:my_greeting should be enough, no?

Also, in terms of IDE completion, probably it's better to make the prefix msg_Application_Index, no? Any reason why we can't make it msg.Application.Index:my_greeting?

FroMage avatar Feb 20 '23 09:02 FroMage

I understand you proposal, and it makes a lot of sense to make them available as Application_index:my_greeting, but mostly for accessing them outside the Application/index.html view, no? For that one, an implicit msg:my_greeting should be enough, no?

Hm, the problem is that qute does not have a notion of view and controllers. Only Renarde does. And message bundles are "global objects". We could make it work using TemplateInstance attributes though. Something like TemplateInstance.setAttribute("messageBundleSuffix", "Application_Index") (Renarde could do this) and then msg would become msg_Application_Index when resolving the message. I will do some experiments...

Also, in terms of IDE completion, probably it's better to make the prefix msg_Application_Index, no?

It could be, yes.

Any reason why we can't make it msg.Application.Index:my_greeting?

A qute namespace can only consist of alphanumeric characters and underscores.

mkouba avatar Feb 20 '23 09:02 mkouba

Hm, the problem is that qute does not have a notion of view and controllers

Well, it does for nested @CheckedTemplate classes ;) So it would be a regular fit to follow that convention too.

FroMage avatar Feb 20 '23 10:02 FroMage

Hm, the problem is that qute does not have a notion of view and controllers

Well, it does for nested @CheckedTemplate classes ;) So it would be a regular fit to follow that convention too.

It's not a view/controller per se. It's a method that defines a type-safe template. That's all.

mkouba avatar Feb 20 '23 10:02 mkouba

Call it what you want, as long as the convention is the same, it works the same ;)

FroMage avatar Feb 20 '23 10:02 FroMage

We could make it work using TemplateInstance attributes though. Something like TemplateInstance.setAttribute("messageBundleSuffix", "Application_Index") (Renarde could do this) and then msg would become msg_Application_Index when resolving the message. I will do some experiments...

Hm, but similar tricks would break message validation... We need something more like "local namespaces" or "namespace aliases". So that msg can be translated to msg_Application_Index when validating templates..

mkouba avatar Feb 20 '23 10:02 mkouba

I was thinking we could first look up the msg: namespace and if it doesn't exist, lookup msg_Application_index:.

FroMage avatar Feb 20 '23 10:02 FroMage

I was thinking we could first look up the msg: namespace and if it doesn't exist, lookup msg_Application_index:.

Ok, but when you use {msg:hello(name)} in your template then we validate at build time that there is a message in the bundle msg for key hello that accepts one parameter (and if name has a type then the type is validated as well). So it's not as easy...

mkouba avatar Feb 20 '23 14:02 mkouba

@FroMage In fact, I think that you can just ignore the msg namespace because we generate an implementation of the message bundle interface and so you can just add a msg parameter your @CheckedTemplate and obtain the implementation via @Inject MyBundle or MessageBundles.get(MyBundle.class) and use {msg.hello(name)} in the template. Methods will be validated as usual.

E.g.

@CheckedTemplate
class Templates {
    TemplateInstance index(MyBundle msg);
}

and then something like:

Templates.index(MessageBundles.get(MyBundle.class)).render()

mkouba avatar Feb 21 '23 12:02 mkouba