Skip to content

Commit

Permalink
Qute type-safe messages: add convenient way to localize enum constants
Browse files Browse the repository at this point in the history
- resolves quarkusio#40089
  • Loading branch information
mkouba committed Apr 18, 2024
1 parent 200652e commit 9840da1
Show file tree
Hide file tree
Showing 11 changed files with 462 additions and 49 deletions.
34 changes: 34 additions & 0 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2858,6 +2858,40 @@ public class MyBean {
----
<1> The annotation value is a locale tag string (IETF).

===== Enums

There is a convenient way to localize enums.
If there is a message bundle method that accepts a single parameter of an enum type and has no message template defined:

[source,java]
----
@Message <1>
String methodName(MyEnum enum);
----
<1> The value is intentionally not provided. There's also no key for the method in a localized file.

Then it receives a generated template:

[source,html]
----
{#when enumParamName}
{#is CONSTANT1}{msg:methodName_CONSTANT1}
{#is CONSTANT2}{msg:methodName_CONSTANT2}
{/when}
----

Furthermore, a special message method is generated for each enum constant. Finally, each localized file must contain keys and values for all constant message keys:

[source,poperties]
----
methodName_CONSTANT1=Value 1
methodName_CONSTANT2=Value 2
----

In a template, an enum constant can be localized with a message bundle method like `{msg:methodName(enumConstant)}`.

TIP: There is also <<convenient-annotation-for-enums,`@TemplateEnum`>> - a convenient annotation to access enum constants in a template.

==== Message Templates

Every method of a message bundle interface must define a message template. The value is normally defined by `io.quarkus.qute.i18n.Message#value()`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
import org.jboss.jandex.MethodInfo;

import io.quarkus.builder.item.MultiBuildItem;
import io.quarkus.qute.deployment.TemplatesAnalysisBuildItem.TemplateAnalysis;

/**
* Represents a message bundle method.
* <p>
* Note that templates that contain no expressions don't need to be validated.
* Note that templates that contain no expressions/sections don't need to be validated.
*/
public final class MessageBundleMethodBuildItem extends MultiBuildItem {

Expand Down Expand Up @@ -36,14 +37,27 @@ public String getKey() {
return key;
}

/**
*
* @return the template id or {@code null} if there is no need to use qute; i.e. no expression/section found
*/
public String getTemplateId() {
return templateId;
}

/**
* For example, there is no corresponding method for generated enum constant message keys.
*
* @return the method or {@code null} if there is no corresponding method declared on the message bundle interface
*/
public MethodInfo getMethod() {
return method;
}

public boolean hasMethod() {
return method != null;
}

public String getTemplate() {
return template;
}
Expand All @@ -65,4 +79,19 @@ public boolean isDefaultBundle() {
return isDefaultBundle;
}

/**
*
* @return the path
* @see TemplateAnalysis#path
*/
public String getPathForAnalysis() {
if (method != null) {
return method.declaringClass().name() + "#" + method.name();
}
if (templateId != null) {
return templateId;
}
return bundleName + "_" + key;
}

}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -714,10 +714,12 @@ public void beforeParsing(ParserHelper parserHelper) {
MessageBundleMethodBuildItem messageBundleMethod = messageBundleMethodsMap.get(templateId);
if (messageBundleMethod != null) {
MethodInfo method = messageBundleMethod.getMethod();
for (ListIterator<Type> it = method.parameterTypes().listIterator(); it.hasNext();) {
Type paramType = it.next();
String name = MessageBundleProcessor.getParameterName(method, it.previousIndex());
parserHelper.addParameter(name, getCheckedTemplateParameterTypeName(paramType));
if (method != null) {
for (ListIterator<Type> it = method.parameterTypes().listIterator(); it.hasNext();) {
Type paramType = it.next();
String name = MessageBundleProcessor.getParameterName(method, it.previousIndex());
parserHelper.addParameter(name, getCheckedTemplateParameterTypeName(paramType));
}
}
}
}
Expand Down Expand Up @@ -759,9 +761,7 @@ public void beforeParsing(ParserHelper parserHelper) {
for (MessageBundleMethodBuildItem messageBundleMethod : messageBundleMethods) {
Template template = dummyEngine.parse(messageBundleMethod.getTemplate(), null, messageBundleMethod.getTemplateId());
analysis.add(new TemplateAnalysis(messageBundleMethod.getTemplateId(), template.getGeneratedId(),
template.getExpressions(), template.getParameterDeclarations(),
messageBundleMethod.getMethod().declaringClass().name() + "#" + messageBundleMethod.getMethod().name()
+ "()",
template.getExpressions(), template.getParameterDeclarations(), messageBundleMethod.getPathForAnalysis(),
template.getFragmentIds()));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package io.quarkus.qute.deployment.i18n;

import static org.junit.jupiter.api.Assertions.assertEquals;

import jakarta.inject.Inject;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.qute.Template;
import io.quarkus.qute.i18n.Message;
import io.quarkus.qute.i18n.MessageBundle;
import io.quarkus.test.QuarkusUnitTest;

public class MessageBundleEnumTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(Messages.class, MyEnum.class)
.addAsResource("messages/enu.properties")
.addAsResource("messages/enu_cs.properties")
.addAsResource(new StringAsset(
"{enu:myEnum(MyEnum:ON)}::{enu:myEnum(MyEnum:OFF)}::{enu:myEnum(MyEnum:UNDEFINED)}::"
+ "{enu:shortEnum(MyEnum:ON)}::{enu:shortEnum(MyEnum:OFF)}::{enu:shortEnum(MyEnum:UNDEFINED)}::"
+ "{enu:foo(MyEnum:ON)}::{enu:foo(MyEnum:OFF)}::{enu:foo(MyEnum:UNDEFINED)}::"
+ "{enu:locFileOverride(MyEnum:ON)}::{enu:locFileOverride(MyEnum:OFF)}::{enu:locFileOverride(MyEnum:UNDEFINED)}"),
"templates/foo.html"));

@Inject
Template foo;

@Test
public void testMessages() {
assertEquals("On::Off::Undefined::1::0::U::+::-::_::on::off::undefined", foo.render());
assertEquals("Zapnuto::Vypnuto::Nedefinováno::1::0::N::+::-::_::zap::vyp::nedef",
foo.instance().setLocale("cs").render());
}

@MessageBundle(value = "enu", locale = "en")
public interface Messages {

// Replaced with:
// @Message("{#when myEnum}"
// + "{#is ON}{enu:myEnum_ON}"
// + "{#is OFF}{enu:myEnum_OFF}"
// + "{#is UNDEFINED}{enu:myEnum_UNDEFINED}"
// + "{/when}")
@Message
String myEnum(MyEnum myEnum);

// Replaced with:
// @Message("{#when myEnum}"
// + "{#is ON}{enu:shortEnum_ON}"
// + "{#is OFF}{enu:shortEnum_OFF}"
// + "{#is UNDEFINED}{enu:shortEnum_UNDEFINED}"
// + "{/when}")
@Message
String shortEnum(MyEnum myEnum);

@Message("{#when myEnum}"
+ "{#is ON}+"
+ "{#is OFF}-"
+ "{#else}_"
+ "{/when}")
String foo(MyEnum myEnum);

@Message
String locFileOverride(MyEnum myEnum);

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateEnum;
import io.quarkus.qute.i18n.Message;
import io.quarkus.qute.i18n.MessageBundle;
import io.quarkus.test.QuarkusUnitTest;
Expand All @@ -20,7 +19,7 @@ public class MessageBundleLogicalLineTest {
@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(Messages.class)
.addClasses(Messages.class, MyEnum.class)
.addAsResource("messages/msg_cs.properties")
.addAsResource(new StringAsset(
"{msg:hello('Edgar')}::{msg:helloNextLine('Edgar')}::{msg:fruits}::{msg:myEnum(MyEnum:OFF)}"),
Expand Down Expand Up @@ -58,11 +57,4 @@ public interface Messages {

}

@TemplateEnum
public enum MyEnum {
ON,
OFF,
UNDEFINED
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public void testResolvers() {
foo.instance().render());
assertEquals("Hello world! Ahoj Jachym! Hello you guys! Hello alpha! Hello! Hello foo from alpha!",
foo.instance().setAttribute(MessageBundles.ATTRIBUTE_LOCALE, Locale.forLanguageTag("cs")).render());
assertEquals("Hallo Welt! Hallo Jachym! Hello you guys! Hello alpha! Hello! Hello foo from alpha!",
assertEquals("Hallo Welt! Hallo Jachym! Hallo you guys! Hello alpha! Hello! Hello foo from alpha!",
foo.instance().setLocale(Locale.GERMAN).render());
assertEquals("Dot test!", engine.parse("{msg:['dot.test']}").render());
assertEquals("Hello world! Hello Malachi Constant!",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.quarkus.qute.deployment.i18n;

import io.quarkus.qute.TemplateEnum;

@TemplateEnum
public enum MyEnum {
ON,
OFF,
UNDEFINED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
myEnum_ON=On
myEnum_OFF=Off
myEnum_UNDEFINED=Undefined

shortEnum_ON=1
shortEnum_OFF=0
shortEnum_UNDEFINED=U

locFileOverride={#when myEnum}\
{#is ON}on\
{#is OFF}off\
{#else}undefined\
{/when}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
myEnum_ON=Zapnuto
myEnum_OFF=Vypnuto
myEnum_UNDEFINED=Nedefinováno

shortEnum_ON=1
shortEnum_OFF=0
shortEnum_UNDEFINED=N

locFileOverride={#when myEnum}\
{#is ON}zap\
{#is OFF}vyp\
{#else}nedef\
{/when}
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,39 @@
* {@link MessageBundle#defaultKey()}.
* <p>
* The {@link #value()} defines the template of a message. The method parameters can be used in this template. All the message
* templates are validated at build time.
* templates are validated at build time. If there is no template defined the template from a localized file is taken. In case
* the value is not provided at all the build fails.
* <p>
* Note that any method declared on a message bundle interface is consireded a message bundle method. If not annotated with this
* annotation then the defaulted values are used for the key and template.
* <p>
* All message bundle methods must return {@link String}. If a message bundle method does not return string then the build
* fails.
*
* <h2>Enums</h2>
* There is a convenient way to localize enums.
* <p>
* If there is a message bundle method that accepts a single parameter of an enum type and has no message template defined then
* it
* receives a generated template:
*
* <pre>
* {#when enumParamName}
* {#is CONSTANT1}{msg:methodName_CONSTANT1}
* {#is CONSTANT2}{msg:methodName_CONSTANT2}
* {/when}
* </pre>
*
* Furthermore, a special message method is generated for each enum constant. Finally, each localized file must contain keys and
* values for all constant message keys:
*
* <pre>
* methodName_CONSTANT1=Value 1
* methodName_CONSTANT2=Value 2
* </pre>
*
* In a template, an enum constant can be localized with a message bundle method {@code msg:methodName(enumConstant)}.
*
* @see MessageBundle
*/
@Retention(RUNTIME)
Expand Down Expand Up @@ -69,6 +94,8 @@
* This value has higher priority over a message template specified in a localized file, and it's
* considered a good practice to specify it. In case the value is not provided and there is no
* match in the localized file too, the build fails.
* <p>
* There is a convenient way to localize enums. See the javadoc of {@link Message}.
*
* @return the message template
*/
Expand Down

0 comments on commit 9840da1

Please sign in to comment.