diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index dea89afaf0357..8b31a7c50f139 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -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 <> - 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()`, diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java index 3509d439e5821..56809719f7b0a 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java @@ -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. *

- * 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 { @@ -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; } @@ -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; + } + } 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 56e4529393b9b..3b45e64f29e94 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 @@ -38,6 +38,7 @@ import org.jboss.jandex.ClassInfo; import org.jboss.jandex.ClassInfo.NestingType; import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; import org.jboss.jandex.Type; @@ -85,6 +86,8 @@ import io.quarkus.qute.Namespaces; import io.quarkus.qute.Resolver; import io.quarkus.qute.SectionHelperFactory; +import io.quarkus.qute.TemplateException; +import io.quarkus.qute.TemplateInstance; import io.quarkus.qute.deployment.QuteProcessor.JavaMemberLookupConfig; import io.quarkus.qute.deployment.QuteProcessor.MatchResult; import io.quarkus.qute.deployment.TemplatesAnalysisBuildItem.TemplateAnalysis; @@ -266,7 +269,7 @@ List processBundles(BeanArchiveIndexBuildItem beanArchiv // Generate implementations // name -> impl class Map generatedImplementations = generateImplementations(bundles, generatedClasses, - messageTemplateMethods); + messageTemplateMethods, index); // Register synthetic beans for (MessageBundleBuildItem bundle : bundles) { @@ -393,8 +396,11 @@ void validateMessageBundleMethods(TemplatesAnalysisBuildItem templatesAnalysis, if (messageBundleMethod != null) { // All top-level expressions without a namespace should be mapped to a param Set usedParamNames = new HashSet<>(); - Set paramNames = IntStream.range(0, messageBundleMethod.getMethod().parametersCount()) - .mapToObj(idx -> getParameterName(messageBundleMethod.getMethod(), idx)).collect(Collectors.toSet()); + Set paramNames = messageBundleMethod.hasMethod() + ? IntStream.range(0, messageBundleMethod.getMethod().parametersCount()) + .mapToObj(idx -> getParameterName(messageBundleMethod.getMethod(), idx)) + .collect(Collectors.toSet()) + : Set.of(); for (Expression expression : analysis.expressions) { validateExpression(incorrectExpressions, messageBundleMethod, expression, paramNames, usedParamNames, globals); @@ -431,9 +437,8 @@ private void validateExpression(BuildProducer inco // Expression has no type info or type info that does not match a method parameter // expressions that have incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(), - name + " is not a parameter of the message bundle method " - + messageBundleMethod.getMethod().declaringClass().name() + "#" - + messageBundleMethod.getMethod().name() + "()", + name + " is not a parameter of the message bundle method: " + + messageBundleMethod.getPathForAnalysis(), expression.getOrigin())); } else { usedParamNames.add(name); @@ -568,6 +573,10 @@ public String apply(String id) { MethodInfo method = methods.get(methodPart.getName()); if (method == null) { + if (methods.containsKey(methodPart.getName())) { + // Skip validation - enum constant key + continue; + } if (!methodPart.isVirtualMethod() || methodPart.asVirtualMethod().getParameters().isEmpty()) { // The method template may contain no expressions method = defaultBundleInterface.method(methodPart.getName()); @@ -690,7 +699,8 @@ void generateExamplePropertiesFiles(List messageBu private Map generateImplementations(List bundles, BuildProducer generatedClasses, - BuildProducer messageTemplateMethods) throws IOException { + BuildProducer messageTemplateMethods, + IndexView index) throws IOException { Map generatedTypes = new HashMap<>(); @@ -701,29 +711,33 @@ private Map generateImplementations(List // take message templates not specified by Message#value from corresponding localized file Map defaultKeyToMap = getLocalizedFileKeyToTemplate(bundle, bundleInterface, - bundle.getDefaultLocale(), bundleInterface.methods(), null); + bundle.getDefaultLocale(), bundleInterface.methods(), null, index); MergeClassInfoWrapper bundleInterfaceWrapper = new MergeClassInfoWrapper(bundleInterface, null, null); + // Generate implementation for the default bundle interface String bundleImpl = generateImplementation(bundle, null, null, bundleInterfaceWrapper, - defaultClassOutput, messageTemplateMethods, defaultKeyToMap, null); + defaultClassOutput, messageTemplateMethods, defaultKeyToMap, null, index); generatedTypes.put(bundleInterface.name().toString(), bundleImpl); + + // Generate imeplementation for each localized interface for (Entry entry : bundle.getLocalizedInterfaces().entrySet()) { ClassInfo localizedInterface = entry.getValue(); // take message templates not specified by Message#value from corresponding localized file Map keyToMap = getLocalizedFileKeyToTemplate(bundle, bundleInterface, entry.getKey(), - localizedInterface.methods(), localizedInterface); + localizedInterface.methods(), localizedInterface, index); MergeClassInfoWrapper localizedInterfaceWrapper = new MergeClassInfoWrapper(localizedInterface, bundleInterface, keyToMap); generatedTypes.put(entry.getValue().name().toString(), generateImplementation(bundle, bundleInterface, bundleImpl, localizedInterfaceWrapper, - defaultClassOutput, messageTemplateMethods, keyToMap, null)); + defaultClassOutput, messageTemplateMethods, keyToMap, null, index)); } + // Generate implementation for each localized file for (Entry entry : bundle.getLocalizedFiles().entrySet()) { Path localizedFile = entry.getValue(); - var keyToTemplate = parseKeyToTemplateFromLocalizedFile(bundleInterface, localizedFile); + var keyToTemplate = parseKeyToTemplateFromLocalizedFile(bundleInterface, localizedFile, index); String locale = entry.getKey(); ClassOutput localeAwareGizmoAdaptor = new GeneratedClassGizmoAdaptor(generatedClasses, @@ -739,19 +753,19 @@ public String apply(String className) { })); generatedTypes.put(localizedFile.toString(), generateImplementation(bundle, bundleInterface, bundleImpl, new SimpleClassInfoWrapper(bundleInterface), - localeAwareGizmoAdaptor, messageTemplateMethods, keyToTemplate, locale)); + localeAwareGizmoAdaptor, messageTemplateMethods, keyToTemplate, locale, index)); } } return generatedTypes; } private Map getLocalizedFileKeyToTemplate(MessageBundleBuildItem bundle, - ClassInfo bundleInterface, String locale, List methods, ClassInfo localizedInterface) + ClassInfo bundleInterface, String locale, List methods, ClassInfo localizedInterface, IndexView index) throws IOException { Path localizedFile = bundle.getMergeCandidates().get(locale); if (localizedFile != null) { - Map keyToTemplate = parseKeyToTemplateFromLocalizedFile(bundleInterface, localizedFile); + Map keyToTemplate = parseKeyToTemplateFromLocalizedFile(bundleInterface, localizedFile, index); if (!keyToTemplate.isEmpty()) { // keep message templates if value wasn't provided by Message#value @@ -785,7 +799,7 @@ private Map getLocalizedFileKeyToTemplate(MessageBundleBuildItem } private Map parseKeyToTemplateFromLocalizedFile(ClassInfo bundleInterface, - Path localizedFile) throws IOException { + Path localizedFile, IndexView index) throws IOException { Map keyToTemplate = new HashMap<>(); for (ListIterator it = Files.readAllLines(localizedFile).listIterator(); it.hasNext();) { String line = it.next(); @@ -804,7 +818,7 @@ private Map parseKeyToTemplateFromLocalizedFile(ClassInfo bundle "Missing key/value separator\n\t- file: " + localizedFile + "\n\t- line " + it.previousIndex()); } String key = line.substring(0, eqIdx).strip(); - if (!hasMessageBundleMethod(bundleInterface, key)) { + if (!hasMessageBundleMethod(bundleInterface, key) && !isEnumConstantMessageKey(key, index, bundleInterface)) { throw new MessageBundleException( "Message bundle method " + key + "() not found on: " + bundleInterface + "\n\t- file: " + localizedFile + "\n\t- line " + it.previousIndex()); @@ -822,6 +836,42 @@ private Map parseKeyToTemplateFromLocalizedFile(ClassInfo bundle return keyToTemplate; } + /** + * + * @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("_"); + if (lastIdx != -1 && lastIdx != key.length()) { + String methodName = key.substring(0, lastIdx); + String constant = key.substring(lastIdx + 1, key.length()); + MethodInfo method = messageBundleMethod(bundleInterface, methodName); + if (method != null && method.parametersCount() == 1) { + Type paramType = method.parameterType(0); + if (paramType.kind() == org.jboss.jandex.Type.Kind.CLASS) { + ClassInfo maybeEnum = index.getClassByName(paramType.name()); + if (maybeEnum != null && maybeEnum.isEnum()) { + if (maybeEnum.fields().stream() + .filter(FieldInfo::isEnumConstant) + .map(FieldInfo::name) + .anyMatch(constant::equals)) { + return true; + } + throw new MessageBundleException( + String.format("%s is not an enum constant of %: %s", constant, maybeEnum, key)); + } + } + } + } + return false; + } + private void constructLine(StringBuilder builder, Iterator it) { if (it.hasNext()) { String nextLine = adaptLine(it.next()); @@ -839,19 +889,22 @@ private String adaptLine(String line) { } private boolean hasMessageBundleMethod(ClassInfo bundleInterface, String name) { + return messageBundleMethod(bundleInterface, name) != null; + } + + private MethodInfo messageBundleMethod(ClassInfo bundleInterface, String name) { for (MethodInfo method : bundleInterface.methods()) { if (method.name().equals(name)) { - return true; + return method; } } - return false; + return null; } private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo defaultBundleInterface, - String defaultBundleImpl, - ClassInfoWrapper bundleInterfaceWrapper, ClassOutput classOutput, + String defaultBundleImpl, ClassInfoWrapper bundleInterfaceWrapper, ClassOutput classOutput, BuildProducer messageTemplateMethods, - Map messageTemplates, String locale) { + Map messageTemplates, String locale, IndexView index) { ClassInfo bundleInterface = bundleInterfaceWrapper.getClassInfo(); LOG.debugf("Generate bundle implementation for %s", bundleInterface); @@ -884,7 +937,7 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d ClassCreator bundleCreator = builder.build(); // key -> method - Map keyMap = new LinkedHashMap<>(); + Map keyMap = new LinkedHashMap<>(); List methods = new ArrayList<>(bundleInterfaceWrapper.methods()); // Sort methods methods.sort(Comparator.comparing(MethodInfo::name).thenComparing(Comparator.comparing(MethodInfo::toString))); @@ -927,7 +980,7 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d if (keyMap.containsKey(key)) { throw new MessageBundleException(String.format("Duplicate key [%s] found on %s", key, bundleInterface)); } - keyMap.put(key, method); + keyMap.put(key, new SimpleMessageMethod(method)); String messageTemplate = messageTemplates.get(method.name()); if (messageTemplate == null) { @@ -940,6 +993,50 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d method.parameterTypes().toArray(new Type[] {}))).annotation(Names.MESSAGE)); } + // 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} + // ... + // {/when} + // Furthermore, a special message method is generated for each enum constant + if (messageTemplate == null && method.parametersCount() == 1) { + Type paramType = method.parameterType(0); + if (paramType.kind() == org.jboss.jandex.Type.Kind.CLASS) { + ClassInfo maybeEnum = index.getClassByName(paramType.name()); + if (maybeEnum != null && maybeEnum.isEnum()) { + StringBuilder generatedMessageTemplate = new StringBuilder("{#when ") + .append(getParameterName(method, 0)) + .append("}"); + Set enumConstants = maybeEnum.fields().stream().filter(FieldInfo::isEnumConstant) + .map(FieldInfo::name).collect(Collectors.toSet()); + for (String enumConstant : enumConstants) { + // org_acme_MyEnum_CONSTANT1 + String enumConstantKey = toEnumConstantKey(method.name(), enumConstant); + String enumConstantTemplate = messageTemplates.get(enumConstantKey); + if (enumConstantTemplate == null) { + throw new TemplateException( + String.format("Enum constant message not found in bundle [%s] for key: %s", + bundleName + (locale != null ? "_" + locale : ""), enumConstantKey)); + } + generatedMessageTemplate.append("{#is ") + .append(enumConstant) + .append("}{") + .append(bundle.getName()) + .append(":") + .append(enumConstantKey) + .append("}"); + generateEnumConstantMessageMethod(bundleCreator, bundleName, locale, bundleInterface, + defaultBundleInterface, enumConstantKey, keyMap, enumConstantTemplate, + messageTemplateMethods); + } + generatedMessageTemplate.append("{/when}"); + messageTemplate = generatedMessageTemplate.toString(); + } + } + } + if (messageTemplate == null) { throw new MessageBundleException( String.format("Message template for key [%s] is missing for default locale [%s]", key, @@ -948,6 +1045,7 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d String templateId = null; if (messageTemplate.contains("}")) { + // Qute is needed - at least one expression/section found if (defaultBundleInterface != null) { if (locale == null) { AnnotationInstance localizedAnnotation = bundleInterface.declaredAnnotation(Names.LOCALIZED); @@ -975,6 +1073,12 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d // Create a template instance ResultHandle templateInstance = bundleMethod .invokeInterfaceMethod(io.quarkus.qute.deployment.Descriptors.TEMPLATE_INSTANCE, template); + if (locale != null) { + bundleMethod.invokeInterfaceMethod( + MethodDescriptor.ofMethod(TemplateInstance.class, "setLocale", TemplateInstance.class, + String.class), + templateInstance, bundleMethod.load(locale)); + } List paramTypes = method.parameterTypes(); if (!paramTypes.isEmpty()) { // Set data @@ -1002,6 +1106,62 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d return generatedName.replace('/', '.'); } + private String toEnumConstantKey(String methodName, String enumConstant) { + return methodName + "_" + enumConstant; + } + + private void generateEnumConstantMessageMethod(ClassCreator bundleCreator, String bundleName, String locale, + ClassInfo bundleInterface, ClassInfo defaultBundleInterface, String enumConstantKey, + Map keyMap, String messageTemplate, + BuildProducer messageTemplateMethods) { + String templateId = null; + if (messageTemplate.contains("}")) { + if (defaultBundleInterface != null) { + if (locale == null) { + AnnotationInstance localizedAnnotation = bundleInterface + .declaredAnnotation(Names.LOCALIZED); + locale = localizedAnnotation.value().asString(); + } + templateId = bundleName + "_" + locale + "_" + enumConstantKey; + } else { + templateId = bundleName + "_" + enumConstantKey; + } + } + + MessageBundleMethodBuildItem messageBundleMethod = new MessageBundleMethodBuildItem(bundleName, enumConstantKey, + templateId, null, messageTemplate, + defaultBundleInterface == null); + messageTemplateMethods.produce(messageBundleMethod); + + MethodCreator enumConstantMethod = bundleCreator.getMethodCreator(enumConstantKey, + String.class); + + if (!messageBundleMethod.isValidatable()) { + // No expression/tag - no need to use qute + enumConstantMethod.returnValue(enumConstantMethod.load(messageTemplate)); + } else { + // Obtain the template, e.g. msg_org_acme_MyEnum_CONSTANT1 + ResultHandle template = enumConstantMethod.invokeStaticMethod( + io.quarkus.qute.deployment.Descriptors.BUNDLES_GET_TEMPLATE, + enumConstantMethod.load(templateId)); + // Create a template instance + ResultHandle templateInstance = enumConstantMethod + .invokeInterfaceMethod(io.quarkus.qute.deployment.Descriptors.TEMPLATE_INSTANCE, template); + if (locale != null) { + enumConstantMethod.invokeInterfaceMethod( + MethodDescriptor.ofMethod(TemplateInstance.class, "setLocale", TemplateInstance.class, + String.class), + templateInstance, enumConstantMethod.load(locale)); + } + // Render the template + enumConstantMethod.returnValue(enumConstantMethod.invokeInterfaceMethod( + io.quarkus.qute.deployment.Descriptors.TEMPLATE_INSTANCE_RENDER, templateInstance)); + } + + keyMap.put(enumConstantKey, + new EnumConstantMessageMethod(enumConstantMethod.getMethodDescriptor())); + } + /** * @return {@link Message#value()} if value was provided */ @@ -1035,7 +1195,7 @@ static String getParameterName(MethodInfo method, int position) { return name; } - private void implementResolve(String defaultBundleImpl, ClassCreator bundleCreator, Map keyMap) { + private void implementResolve(String defaultBundleImpl, ClassCreator bundleCreator, Map keyMap) { MethodCreator resolve = bundleCreator.getMethodCreator("resolve", CompletionStage.class, EvalContext.class); String resolveMethodPrefix = bundleCreator.getClassName().contains("/") ? bundleCreator.getClassName().substring(bundleCreator.getClassName().lastIndexOf('/') + 1) @@ -1106,7 +1266,7 @@ private void implementResolve(String defaultBundleImpl, ClassCreator bundleCreat int resolveIndex = 0; MethodCreator resolveGroup = null; - for (Entry entry : keyMap.entrySet()) { + for (Entry entry : keyMap.entrySet()) { if (resolveGroup == null || groupIndex++ >= groupLimit) { groupIndex = 0; String resolveMethodName = resolveMethodPrefix + "_resolve_" + resolveIndex++; @@ -1147,16 +1307,18 @@ private void implementResolve(String defaultBundleImpl, ClassCreator bundleCreat } } - private void addMessageMethod(MethodCreator resolve, String key, MethodInfo method, ResultHandle name, + private void addMessageMethod(MethodCreator resolve, String key, MessageMethod method, ResultHandle name, ResultHandle evaluatedParams, ResultHandle ret, String bundleClass) { List methodParams = method.parameterTypes(); BytecodeCreator matched = resolve.ifTrue(Gizmo.equals(resolve, resolve.load(key), name)) .trueBranch(); - if (method.parameterTypes().isEmpty()) { + if (methodParams.isEmpty()) { matched.invokeVirtualMethod(Descriptors.COMPLETABLE_FUTURE_COMPLETE, ret, - matched.invokeInterfaceMethod(method, matched.getThis())); + method.isMessageBundleInterfaceMethod() + ? matched.invokeInterfaceMethod(method.descriptor(), matched.getThis()) + : matched.invokeVirtualMethod(method.descriptor(), matched.getThis())); matched.returnValue(ret); } else { // The CompletionStage upon which we invoke whenComplete() @@ -1200,7 +1362,9 @@ private void addMessageMethod(MethodCreator resolve, String key, MethodInfo meth exception.getCaughtException()); tryCatch.assign(invokeRet, - tryCatch.invokeInterfaceMethod(MethodDescriptor.of(method), whenThis, paramsHandle)); + method.isMessageBundleInterfaceMethod() + ? tryCatch.invokeInterfaceMethod(method.descriptor(), whenThis, paramsHandle) + : tryCatch.invokeVirtualMethod(method.descriptor(), whenThis, paramsHandle)); tryCatch.invokeVirtualMethod(Descriptors.COMPLETABLE_FUTURE_COMPLETE, whenRet, invokeRet); // CompletableFuture.completeExceptionally(Throwable) @@ -1424,4 +1588,61 @@ public final MethodInfo method(String name, Type... parameters) { return classInfo.method(name, parameters); } } + + interface MessageMethod { + + List parameterTypes(); + + MethodDescriptor descriptor(); + + default boolean isMessageBundleInterfaceMethod() { + return true; + } + + } + + static class SimpleMessageMethod implements MessageMethod { + + final MethodInfo method; + + SimpleMessageMethod(MethodInfo method) { + this.method = method; + } + + @Override + public List parameterTypes() { + return method.parameterTypes(); + } + + @Override + public MethodDescriptor descriptor() { + return MethodDescriptor.of(method); + } + + } + + static class EnumConstantMessageMethod implements MessageMethod { + + final MethodDescriptor descriptor; + + EnumConstantMessageMethod(MethodDescriptor descriptor) { + this.descriptor = descriptor; + } + + @Override + public List parameterTypes() { + return List.of(); + } + + @Override + public MethodDescriptor descriptor() { + return descriptor; + } + + @Override + public boolean isMessageBundleInterfaceMethod() { + return false; + } + + } } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index 5a57f0d95f270..7353f30506eaa 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -714,10 +714,12 @@ public void beforeParsing(ParserHelper parserHelper) { MessageBundleMethodBuildItem messageBundleMethod = messageBundleMethodsMap.get(templateId); if (messageBundleMethod != null) { MethodInfo method = messageBundleMethod.getMethod(); - for (ListIterator 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 it = method.parameterTypes().listIterator(); it.hasNext();) { + Type paramType = it.next(); + String name = MessageBundleProcessor.getParameterName(method, it.previousIndex()); + parserHelper.addParameter(name, getCheckedTemplateParameterTypeName(paramType)); + } } } } @@ -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())); } 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 new file mode 100644 index 0000000000000..8ac3a9e739810 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumTest.java @@ -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); + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLogicalLineTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLogicalLineTest.java index 89c944458e999..fcc4f14a9c414 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLogicalLineTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLogicalLineTest.java @@ -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; @@ -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)}"), @@ -58,11 +57,4 @@ public interface Messages { } - @TemplateEnum - public enum MyEnum { - ON, - OFF, - UNDEFINED - } - } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java index 6acf6738cb8ed..c9349a722dd84 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java @@ -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!", diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MyEnum.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MyEnum.java new file mode 100644 index 0000000000000..7e26e81d95345 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MyEnum.java @@ -0,0 +1,10 @@ +package io.quarkus.qute.deployment.i18n; + +import io.quarkus.qute.TemplateEnum; + +@TemplateEnum +public enum MyEnum { + ON, + OFF, + UNDEFINED +} \ No newline at end of file diff --git a/extensions/qute/deployment/src/test/resources/messages/enu.properties b/extensions/qute/deployment/src/test/resources/messages/enu.properties new file mode 100644 index 0000000000000..072f933eb0881 --- /dev/null +++ b/extensions/qute/deployment/src/test/resources/messages/enu.properties @@ -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} \ 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 new file mode 100644 index 0000000000000..e3f5c0a2ae6de --- /dev/null +++ b/extensions/qute/deployment/src/test/resources/messages/enu_cs.properties @@ -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} \ 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 8f4c68664af85..93c5fbe6b1327 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 @@ -14,7 +14,8 @@ * {@link MessageBundle#defaultKey()}. *

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

* 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. @@ -22,6 +23,30 @@ * All message bundle methods must return {@link String}. If a message bundle method does not return string then the build * fails. * + *

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 then + * it + * receives a generated template: + * + *

+ * {#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: + * + *
+ * 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)}. + * * @see MessageBundle */ @Retention(RUNTIME) @@ -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. + *

+ * There is a convenient way to localize enums. See the javadoc of {@link Message}. * * @return the message template */