From b9278d50148097d3c3adb0fc635907cd0aaa6d1f Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Tue, 3 Dec 2024 08:24:51 +0100 Subject: [PATCH] Qute message bundles: fix localization of enums - support constants with underscores - fixes #44866 (cherry picked from commit b322dbf358dd3f7356063f1e37d1ad4913c78044) --- docs/src/main/asciidoc/qute-reference.adoc | 21 ++++++--- .../deployment/MessageBundleProcessor.java | 45 ++++++++++++++----- .../i18n/MessageBundleEnumTest.java | 44 +++++++++++++++++- .../MessageBundleInvalidEnumConstantTest.java | 43 ++++++++++++++++++ .../test/resources/messages/enu.properties | 11 ++++- .../test/resources/messages/enu_cs.properties | 11 ++++- .../resources/messages/enu_invalid.properties | 2 + .../java/io/quarkus/qute/i18n/Message.java | 12 +++-- 8 files changed, 167 insertions(+), 22 deletions(-) create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleInvalidEnumConstantTest.java create mode 100644 extensions/qute/deployment/src/test/resources/messages/enu_invalid.properties diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 4b62a037bd432..162103b1c31cc 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -2994,10 +2994,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} @@ -3006,7 +3005,8 @@ 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] ---- @@ -3014,7 +3014,18 @@ 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 <> - a convenient annotation to access enum constants in a template. diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java index 5842bbd715abc..a9e659b47b068 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java @@ -865,16 +865,20 @@ private Map 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); @@ -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) { @@ -1036,9 +1041,12 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d .append("}"); Set 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( @@ -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); @@ -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 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, @@ -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)); diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumTest.java index 8ac3a9e739810..c0b4eb2e481e0 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumTest.java @@ -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; @@ -18,7 +19,7 @@ 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( @@ -26,16 +27,39 @@ public class MessageBundleEnumTest { + "{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") @@ -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); + } } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleInvalidEnumConstantTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleInvalidEnumConstantTest.java new file mode 100644 index 0000000000000..cd771a4929dfb --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleInvalidEnumConstantTest.java @@ -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); + + } + +} diff --git a/extensions/qute/deployment/src/test/resources/messages/enu.properties b/extensions/qute/deployment/src/test/resources/messages/enu.properties index 072f933eb0881..ac9aa52409737 100644 --- a/extensions/qute/deployment/src/test/resources/messages/enu.properties +++ b/extensions/qute/deployment/src/test/resources/messages/enu.properties @@ -10,4 +10,13 @@ locFileOverride={#when myEnum}\ {#is ON}on\ {#is OFF}off\ {#else}undefined\ - {/when} \ No newline at end of file + {/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 \ No newline at end of file diff --git a/extensions/qute/deployment/src/test/resources/messages/enu_cs.properties b/extensions/qute/deployment/src/test/resources/messages/enu_cs.properties index e3f5c0a2ae6de..8f15c483934fc 100644 --- a/extensions/qute/deployment/src/test/resources/messages/enu_cs.properties +++ b/extensions/qute/deployment/src/test/resources/messages/enu_cs.properties @@ -10,4 +10,13 @@ locFileOverride={#when myEnum}\ {#is ON}zap\ {#is OFF}vyp\ {#else}nedef\ - {/when} \ No newline at end of file + {/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 \ No newline at end of file diff --git a/extensions/qute/deployment/src/test/resources/messages/enu_invalid.properties b/extensions/qute/deployment/src/test/resources/messages/enu_invalid.properties new file mode 100644 index 0000000000000..79e8a9324eb4d --- /dev/null +++ b/extensions/qute/deployment/src/test/resources/messages/enu_invalid.properties @@ -0,0 +1,2 @@ +underscored_$A_B=A/B +underscored_$FOO_BAR_BAZ=Foo/Bar/Baz \ No newline at end of file diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java index b8b8a43ae5955..6f858af9e6a91 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java @@ -27,7 +27,7 @@ * 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 then - * it receives a generated template: + * it receives a generated template like: * *

  * {#when enumParamName}
@@ -37,14 +37,20 @@
  * 
* * 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. * *
  * methodName_CONSTANT1=Value 1
  * methodName_CONSTANT2=Value 2
  * 
* - * 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}. + *

+ * 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 */