Skip to content

Commit a21a9e9

Browse files
authored
Merge pull request #44883 from mkouba/issue-44866
Qute message bundles: fix localization of enums
2 parents 57f0e20 + b322dbf commit a21a9e9

File tree

8 files changed

+167
-22
lines changed

8 files changed

+167
-22
lines changed

docs/src/main/asciidoc/qute-reference.adoc

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3007,10 +3007,9 @@ If there is a message bundle method that accepts a single parameter of an enum t
30073007
@Message <1>
30083008
String methodName(MyEnum enum);
30093009
----
3010-
<1> The value is intentionally not provided. There's also no key for the method in a localized file.
3011-
3012-
Then it receives a generated template:
3010+
<1> The value is intentionally not provided. There's also no key/value pair for this method in a localized file.
30133011

3012+
Then it receives a generated template like:
30143013
[source,html]
30153014
----
30163015
{#when enumParamName}
@@ -3019,15 +3018,27 @@ Then it receives a generated template:
30193018
{/when}
30203019
----
30213020

3022-
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:
3021+
Furthermore, a special message method is generated for each enum constant.
3022+
Finally, each localized file must contain keys and values for all enum constants:
30233023

30243024
[source,poperties]
30253025
----
30263026
methodName_CONSTANT1=Value 1
30273027
methodName_CONSTANT2=Value 2
30283028
----
30293029

3030-
In a template, an enum constant can be localized with a message bundle method like `{msg:methodName(enumConstant)}`.
3030+
// We need to escape the first underscore
3031+
// See https://docs.asciidoctor.org/asciidoc/latest/subs/prevent/
3032+
[IMPORTANT]
3033+
.Message keys for enum constants
3034+
====
3035+
By default, the message key consists of the method name followed by the `\_` separator and the constant name.
3036+
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.
3037+
For example, `methodName_$CONSTANT_1=Value 1` or `methodName_$CONSTANT$1=Value 1`.
3038+
A constant of a localized enum may not contain the `_$` separator.
3039+
====
3040+
3041+
In a template, the localized message for an enum constant can be obtained with a message bundle method like `{msg:methodName(enumConstant)}`.
30313042

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

extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -865,16 +865,20 @@ private Map<String, String> parseKeyToTemplateFromLocalizedFile(ClassInfo bundle
865865
* @param key
866866
* @param bundleInterface
867867
* @return {@code true} if the given key represents an enum constant message key, such as {@code myEnum_CONSTANT1}
868-
* @see #toEnumConstantKey(String, String)
869868
*/
870869
boolean isEnumConstantMessageKey(String key, IndexView index, ClassInfo bundleInterface) {
871870
if (key.isBlank()) {
872871
return false;
873872
}
874-
int lastIdx = key.lastIndexOf("_");
873+
return isEnumConstantMessageKey("_$", key, index, bundleInterface)
874+
|| isEnumConstantMessageKey("_", key, index, bundleInterface);
875+
}
876+
877+
private boolean isEnumConstantMessageKey(String separator, String key, IndexView index, ClassInfo bundleInterface) {
878+
int lastIdx = key.lastIndexOf(separator);
875879
if (lastIdx != -1 && lastIdx != key.length()) {
876880
String methodName = key.substring(0, lastIdx);
877-
String constant = key.substring(lastIdx + 1, key.length());
881+
String constant = key.substring(lastIdx + separator.length(), key.length());
878882
MethodInfo method = messageBundleMethod(bundleInterface, methodName);
879883
if (method != null && method.parametersCount() == 1) {
880884
Type paramType = method.parameterType(0);
@@ -1021,11 +1025,12 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d
10211025
// We need some special handling for enum message bundle methods
10221026
// A message bundle method that accepts an enum and has no message template receives a generated template:
10231027
// {#when enumParamName}
1024-
// {#is CONSTANT1}{msg:org_acme_MyEnum_CONSTANT1}
1025-
// {#is CONSTANT2}{msg:org_acme_MyEnum_CONSTANT2}
1028+
// {#is CONSTANT_1}{msg:myEnum_$CONSTANT_1}
1029+
// {#is CONSTANT_2}{msg:myEnum_$CONSTANT_2}
10261030
// ...
10271031
// {/when}
10281032
// Furthermore, a special message method is generated for each enum constant
1033+
// These methods are used to handle the {msg:myEnum$CONSTANT_1} and {msg:myEnum$CONSTANT_2}
10291034
if (messageTemplate == null && method.parametersCount() == 1) {
10301035
Type paramType = method.parameterType(0);
10311036
if (paramType.kind() == org.jboss.jandex.Type.Kind.CLASS) {
@@ -1036,9 +1041,12 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d
10361041
.append("}");
10371042
Set<String> enumConstants = maybeEnum.fields().stream().filter(FieldInfo::isEnumConstant)
10381043
.map(FieldInfo::name).collect(Collectors.toSet());
1044+
String separator = enumConstantSeparator(enumConstants);
10391045
for (String enumConstant : enumConstants) {
1040-
// org_acme_MyEnum_CONSTANT1
1041-
String enumConstantKey = toEnumConstantKey(method.name(), enumConstant);
1046+
// myEnum_CONSTANT
1047+
// myEnum_$CONSTANT_1
1048+
// myEnum_$CONSTANT$NEXT
1049+
String enumConstantKey = toEnumConstantKey(method.name(), separator, enumConstant);
10421050
String enumConstantTemplate = messageTemplates.get(enumConstantKey);
10431051
if (enumConstantTemplate == null) {
10441052
throw new TemplateException(
@@ -1052,6 +1060,10 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d
10521060
.append(":")
10531061
.append(enumConstantKey)
10541062
.append("}");
1063+
// For each constant we generate a method:
1064+
// myEnum_CONSTANT(MyEnum val)
1065+
// myEnum_$CONSTANT_1(MyEnum val)
1066+
// myEnum_$CONSTANT$NEXT(MyEnum val)
10551067
generateEnumConstantMessageMethod(bundleCreator, bundleName, locale, bundleInterface,
10561068
defaultBundleInterface, enumConstantKey, keyMap, enumConstantTemplate,
10571069
messageTemplateMethods);
@@ -1132,8 +1144,21 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d
11321144
return generatedName.replace('/', '.');
11331145
}
11341146

1135-
private String toEnumConstantKey(String methodName, String enumConstant) {
1136-
return methodName + "_" + enumConstant;
1147+
private String enumConstantSeparator(Set<String> enumConstants) {
1148+
for (String constant : enumConstants) {
1149+
if (constant.contains("_$")) {
1150+
throw new MessageBundleException("A constant of a localized enum may not contain '_$': " + constant);
1151+
}
1152+
if (constant.contains("$") || constant.contains("_")) {
1153+
// If any of the constants contains "_" or "$" then "_$" is used
1154+
return "_$";
1155+
}
1156+
}
1157+
return "_";
1158+
}
1159+
1160+
private String toEnumConstantKey(String methodName, String separator, String enumConstant) {
1161+
return methodName + separator + enumConstant;
11371162
}
11381163

11391164
private void generateEnumConstantMessageMethod(ClassCreator bundleCreator, String bundleName, String locale,
@@ -1165,7 +1190,7 @@ private void generateEnumConstantMessageMethod(ClassCreator bundleCreator, Strin
11651190
// No expression/tag - no need to use qute
11661191
enumConstantMethod.returnValue(enumConstantMethod.load(messageTemplate));
11671192
} else {
1168-
// Obtain the template, e.g. msg_org_acme_MyEnum_CONSTANT1
1193+
// Obtain the template, e.g. msg_myEnum$CONSTANT_1
11691194
ResultHandle template = enumConstantMethod.invokeStaticMethod(
11701195
io.quarkus.qute.deployment.Descriptors.BUNDLES_GET_TEMPLATE,
11711196
enumConstantMethod.load(templateId));

extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumTest.java

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.junit.jupiter.api.extension.RegisterExtension;
1010

1111
import io.quarkus.qute.Template;
12+
import io.quarkus.qute.TemplateEnum;
1213
import io.quarkus.qute.i18n.Message;
1314
import io.quarkus.qute.i18n.MessageBundle;
1415
import io.quarkus.test.QuarkusUnitTest;
@@ -18,24 +19,47 @@ public class MessageBundleEnumTest {
1819
@RegisterExtension
1920
static final QuarkusUnitTest config = new QuarkusUnitTest()
2021
.withApplicationRoot((jar) -> jar
21-
.addClasses(Messages.class, MyEnum.class)
22+
.addClasses(Messages.class, MyEnum.class, UnderscoredEnum.class, AnotherUnderscoredEnum.class)
2223
.addAsResource("messages/enu.properties")
2324
.addAsResource("messages/enu_cs.properties")
2425
.addAsResource(new StringAsset(
2526
"{enu:myEnum(MyEnum:ON)}::{enu:myEnum(MyEnum:OFF)}::{enu:myEnum(MyEnum:UNDEFINED)}::"
2627
+ "{enu:shortEnum(MyEnum:ON)}::{enu:shortEnum(MyEnum:OFF)}::{enu:shortEnum(MyEnum:UNDEFINED)}::"
2728
+ "{enu:foo(MyEnum:ON)}::{enu:foo(MyEnum:OFF)}::{enu:foo(MyEnum:UNDEFINED)}::"
2829
+ "{enu:locFileOverride(MyEnum:ON)}::{enu:locFileOverride(MyEnum:OFF)}::{enu:locFileOverride(MyEnum:UNDEFINED)}"),
29-
"templates/foo.html"));
30+
"templates/foo.html")
31+
.addAsResource(new StringAsset(
32+
"{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)}"),
33+
"templates/bar.html"));
3034

3135
@Inject
3236
Template foo;
3337

38+
@Inject
39+
Template bar;
40+
3441
@Test
3542
public void testMessages() {
3643
assertEquals("On::Off::Undefined::1::0::U::+::-::_::on::off::undefined", foo.render());
3744
assertEquals("Zapnuto::Vypnuto::Nedefinováno::1::0::N::+::-::_::zap::vyp::nedef",
3845
foo.instance().setLocale("cs").render());
46+
assertEquals("A/B::Foo/Bar/Baz::NEXT::NEXT::NEXT", bar.render());
47+
}
48+
49+
@TemplateEnum
50+
public enum UnderscoredEnum {
51+
A_B,
52+
FOO_BAR_BAZ
53+
}
54+
55+
@TemplateEnum
56+
public enum AnotherUnderscoredEnum {
57+
NEXT_B
58+
}
59+
60+
@TemplateEnum
61+
public enum UncommonEnum {
62+
NEXT$B
3963
}
4064

4165
@MessageBundle(value = "enu", locale = "en")
@@ -69,6 +93,22 @@ public interface Messages {
6993
@Message
7094
String locFileOverride(MyEnum myEnum);
7195

96+
// maps to underscored_$A_B, underscored_$FOO_BAR_BAZ
97+
@Message
98+
String underscored(UnderscoredEnum val);
99+
100+
// maps to underscored_foo_$NEXT_B
101+
@Message
102+
String underscored_foo(AnotherUnderscoredEnum val);
103+
104+
// maps to underscored$foo_$NEXT_B
105+
@Message
106+
String underscored$foo(AnotherUnderscoredEnum val);
107+
108+
// maps to uncommon_$NEXT$B
109+
@Message
110+
String uncommon(UncommonEnum val);
111+
72112
}
73113

74114
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package io.quarkus.qute.deployment.i18n;
2+
3+
import static org.junit.jupiter.api.Assertions.fail;
4+
5+
import org.junit.jupiter.api.Test;
6+
import org.junit.jupiter.api.extension.RegisterExtension;
7+
8+
import io.quarkus.qute.TemplateEnum;
9+
import io.quarkus.qute.deployment.MessageBundleException;
10+
import io.quarkus.qute.i18n.Message;
11+
import io.quarkus.qute.i18n.MessageBundle;
12+
import io.quarkus.test.QuarkusUnitTest;
13+
14+
public class MessageBundleInvalidEnumConstantTest {
15+
16+
@RegisterExtension
17+
static final QuarkusUnitTest config = new QuarkusUnitTest()
18+
.withApplicationRoot(root -> root
19+
.addClasses(Messages.class, UnderscoredEnum.class)
20+
.addAsResource("messages/enu_invalid.properties"))
21+
.setExpectedException(MessageBundleException.class, true);
22+
23+
@Test
24+
public void testMessages() {
25+
fail();
26+
}
27+
28+
@TemplateEnum
29+
public enum UnderscoredEnum {
30+
31+
A_B,
32+
33+
}
34+
35+
@MessageBundle(value = "enu_invalid")
36+
public interface Messages {
37+
38+
@Message
39+
String underscored(UnderscoredEnum constants);
40+
41+
}
42+
43+
}

extensions/qute/deployment/src/test/resources/messages/enu.properties

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,13 @@ locFileOverride={#when myEnum}\
1010
{#is ON}on\
1111
{#is OFF}off\
1212
{#else}undefined\
13-
{/when}
13+
{/when}
14+
15+
underscored_$A_B=A/B
16+
underscored_$FOO_BAR_BAZ=Foo/Bar/Baz
17+
18+
underscored_foo_$NEXT_B=NEXT
19+
20+
underscored$foo_$NEXT_B=NEXT
21+
22+
uncommon_$NEXT$B=NEXT

extensions/qute/deployment/src/test/resources/messages/enu_cs.properties

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,13 @@ locFileOverride={#when myEnum}\
1010
{#is ON}zap\
1111
{#is OFF}vyp\
1212
{#else}nedef\
13-
{/when}
13+
{/when}
14+
15+
underscored_$A_B=A/B
16+
underscored_$FOO_BAR_BAZ=Foo/Bar/Baz
17+
18+
underscored_foo_$NEXT_B=NEXT
19+
20+
underscored$foo_$NEXT_B=NEXT
21+
22+
uncommon_$NEXT$B=NEXT
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
underscored_$A_B=A/B
2+
underscored_$FOO_BAR_BAZ=Foo/Bar/Baz

extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
* There is a convenient way to localize enums.
2828
* <p>
2929
* If there is a message bundle method that accepts a single parameter of an enum type and has no message template defined then
30-
* it receives a generated template:
30+
* it receives a generated template like:
3131
*
3232
* <pre>
3333
* {#when enumParamName}
@@ -37,14 +37,20 @@
3737
* </pre>
3838
*
3939
* Furthermore, a special message method is generated for each enum constant. Finally, each localized file must contain keys and
40-
* values for all constant message keys:
40+
* values for all enum constants.
4141
*
4242
* <pre>
4343
* methodName_CONSTANT1=Value 1
4444
* methodName_CONSTANT2=Value 2
4545
* </pre>
4646
*
47-
* In a template, an enum constant can be localized with a message bundle method {@code msg:methodName(enumConstant)}.
47+
* By default, the message key consists of the method name followed by the {@code _} separator and the constant name. If any
48+
* constant name of a particular enum contains the {@code _} or the {@code $} character then the {@code _$} separator must be
49+
* used for all message keys for this enum instead. For example, {@code methodName_$CONSTANT_1=Value 1} or
50+
* {@code methodName_$CONSTANT$1=Value 1}.
51+
* </p>
52+
* In a template, the localized message for an enum constant can be obtained with a message bundle method like
53+
* {@code msg:methodName(enumConstant)}.
4854
*
4955
* @see MessageBundle
5056
*/

0 commit comments

Comments
 (0)