quarkiverse / quarkus-renarde

Server-side Web Framework with Qute templating, magic/easier controllers, auth, reverse-routing
Apache License 2.0
74 stars 19 forks source link

Add or test internationalisation #3

Open FroMage opened 2 years ago

FroMage commented 2 years ago

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.

cdhermann commented 2 years ago

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?

FroMage commented 2 years ago

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 commented 1 year ago

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");
    }
}
mkouba commented 1 year ago

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?

FroMage commented 1 year ago

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?

mkouba commented 1 year ago

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.

FroMage commented 1 year ago

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.

mkouba commented 1 year ago

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.

FroMage commented 1 year ago

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

mkouba commented 1 year ago

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..

FroMage commented 1 year ago

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

mkouba commented 1 year ago

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 commented 1 year ago

@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()