Skip to content

Commit

Permalink
Qute message bundles: fix localization of enums
Browse files Browse the repository at this point in the history
- support constants with underscores
- fixes #44866
  • Loading branch information
mkouba committed Dec 3, 2024
1 parent 2c31260 commit b322dbf
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 22 deletions.
21 changes: 16 additions & 5 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3007,10 +3007,9 @@ If there is a message bundle method that accepts a single parameter of an enum t
@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:
<1> The value is intentionally not provided. There's also no key/value pair for this method in a localized file.

Then it receives a generated template like:
[source,html]
----
{#when enumParamName}
Expand All @@ -3019,15 +3018,27 @@ Then it receives a generated template:
{/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:
Furthermore, a special message method is generated for each enum constant.
Finally, each localized file must contain keys and values for all enum constants:

[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)}`.
// We need to escape the first underscore
// See https://docs.asciidoctor.org/asciidoc/latest/subs/prevent/
[IMPORTANT]
.Message keys for enum constants
====
By default, the message key consists of the method name followed by the `\_` separator and the constant name.
If any constant name of a particular enum contains the `_` or the `$` character then the `\_$` separator must be used for all message keys for this enum instead.
For example, `methodName_$CONSTANT_1=Value 1` or `methodName_$CONSTANT$1=Value 1`.
A constant of a localized enum may not contain the `_$` separator.
====

In a template, the localized message for an enum constant can be obtained 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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -865,16 +865,20 @@ private Map<String, String> parseKeyToTemplateFromLocalizedFile(ClassInfo bundle
* @param key
* @param bundleInterface
* @return {@code true} if the given key represents an enum constant message key, such as {@code myEnum_CONSTANT1}
* @see #toEnumConstantKey(String, String)
*/
boolean isEnumConstantMessageKey(String key, IndexView index, ClassInfo bundleInterface) {
if (key.isBlank()) {
return false;
}
int lastIdx = key.lastIndexOf("_");
return isEnumConstantMessageKey("_$", key, index, bundleInterface)
|| isEnumConstantMessageKey("_", key, index, bundleInterface);
}

private boolean isEnumConstantMessageKey(String separator, String key, IndexView index, ClassInfo bundleInterface) {
int lastIdx = key.lastIndexOf(separator);
if (lastIdx != -1 && lastIdx != key.length()) {
String methodName = key.substring(0, lastIdx);
String constant = key.substring(lastIdx + 1, key.length());
String constant = key.substring(lastIdx + separator.length(), key.length());
MethodInfo method = messageBundleMethod(bundleInterface, methodName);
if (method != null && method.parametersCount() == 1) {
Type paramType = method.parameterType(0);
Expand Down Expand Up @@ -1021,11 +1025,12 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d
// We need some special handling for enum message bundle methods
// A message bundle method that accepts an enum and has no message template receives a generated template:
// {#when enumParamName}
// {#is CONSTANT1}{msg:org_acme_MyEnum_CONSTANT1}
// {#is CONSTANT2}{msg:org_acme_MyEnum_CONSTANT2}
// {#is CONSTANT_1}{msg:myEnum_$CONSTANT_1}
// {#is CONSTANT_2}{msg:myEnum_$CONSTANT_2}
// ...
// {/when}
// Furthermore, a special message method is generated for each enum constant
// These methods are used to handle the {msg:myEnum$CONSTANT_1} and {msg:myEnum$CONSTANT_2}
if (messageTemplate == null && method.parametersCount() == 1) {
Type paramType = method.parameterType(0);
if (paramType.kind() == org.jboss.jandex.Type.Kind.CLASS) {
Expand All @@ -1036,9 +1041,12 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d
.append("}");
Set<String> enumConstants = maybeEnum.fields().stream().filter(FieldInfo::isEnumConstant)
.map(FieldInfo::name).collect(Collectors.toSet());
String separator = enumConstantSeparator(enumConstants);
for (String enumConstant : enumConstants) {
// org_acme_MyEnum_CONSTANT1
String enumConstantKey = toEnumConstantKey(method.name(), enumConstant);
// myEnum_CONSTANT
// myEnum_$CONSTANT_1
// myEnum_$CONSTANT$NEXT
String enumConstantKey = toEnumConstantKey(method.name(), separator, enumConstant);
String enumConstantTemplate = messageTemplates.get(enumConstantKey);
if (enumConstantTemplate == null) {
throw new TemplateException(
Expand All @@ -1052,6 +1060,10 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d
.append(":")
.append(enumConstantKey)
.append("}");
// For each constant we generate a method:
// myEnum_CONSTANT(MyEnum val)
// myEnum_$CONSTANT_1(MyEnum val)
// myEnum_$CONSTANT$NEXT(MyEnum val)
generateEnumConstantMessageMethod(bundleCreator, bundleName, locale, bundleInterface,
defaultBundleInterface, enumConstantKey, keyMap, enumConstantTemplate,
messageTemplateMethods);
Expand Down Expand Up @@ -1132,8 +1144,21 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d
return generatedName.replace('/', '.');
}

private String toEnumConstantKey(String methodName, String enumConstant) {
return methodName + "_" + enumConstant;
private String enumConstantSeparator(Set<String> enumConstants) {
for (String constant : enumConstants) {
if (constant.contains("_$")) {
throw new MessageBundleException("A constant of a localized enum may not contain '_$': " + constant);
}
if (constant.contains("$") || constant.contains("_")) {
// If any of the constants contains "_" or "$" then "_$" is used
return "_$";
}
}
return "_";
}

private String toEnumConstantKey(String methodName, String separator, String enumConstant) {
return methodName + separator + enumConstant;
}

private void generateEnumConstantMessageMethod(ClassCreator bundleCreator, String bundleName, String locale,
Expand Down Expand Up @@ -1165,7 +1190,7 @@ private void generateEnumConstantMessageMethod(ClassCreator bundleCreator, Strin
// No expression/tag - no need to use qute
enumConstantMethod.returnValue(enumConstantMethod.load(messageTemplate));
} else {
// Obtain the template, e.g. msg_org_acme_MyEnum_CONSTANT1
// Obtain the template, e.g. msg_myEnum$CONSTANT_1
ResultHandle template = enumConstantMethod.invokeStaticMethod(
io.quarkus.qute.deployment.Descriptors.BUNDLES_GET_TEMPLATE,
enumConstantMethod.load(templateId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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 @@ -18,24 +19,47 @@ public class MessageBundleEnumTest {
@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(Messages.class, MyEnum.class)
.addClasses(Messages.class, MyEnum.class, UnderscoredEnum.class, AnotherUnderscoredEnum.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"));
"templates/foo.html")
.addAsResource(new StringAsset(
"{enu:underscored(UnderscoredEnum:A_B)}::{enu:underscored(UnderscoredEnum:FOO_BAR_BAZ)}::{enu:underscored_foo(AnotherUnderscoredEnum:NEXT_B)}::{enu:underscored$foo(AnotherUnderscoredEnum:NEXT_B)}::{enu:uncommon(UncommonEnum:NEXT$B)}"),
"templates/bar.html"));

@Inject
Template foo;

@Inject
Template bar;

@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());
assertEquals("A/B::Foo/Bar/Baz::NEXT::NEXT::NEXT", bar.render());
}

@TemplateEnum
public enum UnderscoredEnum {
A_B,
FOO_BAR_BAZ
}

@TemplateEnum
public enum AnotherUnderscoredEnum {
NEXT_B
}

@TemplateEnum
public enum UncommonEnum {
NEXT$B
}

@MessageBundle(value = "enu", locale = "en")
Expand Down Expand Up @@ -69,6 +93,22 @@ public interface Messages {
@Message
String locFileOverride(MyEnum myEnum);

// maps to underscored_$A_B, underscored_$FOO_BAR_BAZ
@Message
String underscored(UnderscoredEnum val);

// maps to underscored_foo_$NEXT_B
@Message
String underscored_foo(AnotherUnderscoredEnum val);

// maps to underscored$foo_$NEXT_B
@Message
String underscored$foo(AnotherUnderscoredEnum val);

// maps to uncommon_$NEXT$B
@Message
String uncommon(UncommonEnum val);

}

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

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

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.qute.TemplateEnum;
import io.quarkus.qute.deployment.MessageBundleException;
import io.quarkus.qute.i18n.Message;
import io.quarkus.qute.i18n.MessageBundle;
import io.quarkus.test.QuarkusUnitTest;

public class MessageBundleInvalidEnumConstantTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(root -> root
.addClasses(Messages.class, UnderscoredEnum.class)
.addAsResource("messages/enu_invalid.properties"))
.setExpectedException(MessageBundleException.class, true);

@Test
public void testMessages() {
fail();
}

@TemplateEnum
public enum UnderscoredEnum {

A_B,

}

@MessageBundle(value = "enu_invalid")
public interface Messages {

@Message
String underscored(UnderscoredEnum constants);

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,13 @@ locFileOverride={#when myEnum}\
{#is ON}on\
{#is OFF}off\
{#else}undefined\
{/when}
{/when}

underscored_$A_B=A/B
underscored_$FOO_BAR_BAZ=Foo/Bar/Baz

underscored_foo_$NEXT_B=NEXT

underscored$foo_$NEXT_B=NEXT

uncommon_$NEXT$B=NEXT
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,13 @@ locFileOverride={#when myEnum}\
{#is ON}zap\
{#is OFF}vyp\
{#else}nedef\
{/when}
{/when}

underscored_$A_B=A/B
underscored_$FOO_BAR_BAZ=Foo/Bar/Baz

underscored_foo_$NEXT_B=NEXT

underscored$foo_$NEXT_B=NEXT

uncommon_$NEXT$B=NEXT
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
underscored_$A_B=A/B
underscored_$FOO_BAR_BAZ=Foo/Bar/Baz
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
* 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:
* it receives a generated template like:
*
* <pre>
* {#when enumParamName}
Expand All @@ -37,14 +37,20 @@
* </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:
* values for all enum constants.
*
* <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)}.
* By default, the message key consists of the method name followed by the {@code _} separator and the constant name. If any
* constant name of a particular enum contains the {@code _} or the {@code $} character then the {@code _$} separator must be
* used for all message keys for this enum instead. For example, {@code methodName_$CONSTANT_1=Value 1} or
* {@code methodName_$CONSTANT$1=Value 1}.
* </p>
* In a template, the localized message for an enum constant can be obtained with a message bundle method like
* {@code msg:methodName(enumConstant)}.
*
* @see MessageBundle
*/
Expand Down

0 comments on commit b322dbf

Please sign in to comment.