Skip to content

Commit

Permalink
Merge pull request #31299 from mkouba/qute-message-bundle-defaulted-name
Browse files Browse the repository at this point in the history
Qute type-safe messages - change the default bundle name strategy
  • Loading branch information
mkouba authored Feb 21, 2023
2 parents e0afb63 + 709b87b commit 37343bb
Show file tree
Hide file tree
Showing 18 changed files with 189 additions and 45 deletions.
49 changes: 37 additions & 12 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2459,7 +2459,25 @@ In the development mode, all files located in `src/main/resources/templates` are
The basic idea is that every message is potentially a very simple template.
In order to prevent type errors a message is defined as an annotated method of a *message bundle interface*.
Quarkus generates the *message bundle implementation* at build time.
Subsequently, the bundles can be used at runtime:

.Message Bundle Interface Example
[source,java]
----
import io.quarkus.qute.i18n.Message;
import io.quarkus.qute.i18n.MessageBundle;
@MessageBundle <1>
public interface AppMessages {
@Message("Hello {name}!") <2>
String hello_name(String name); <3>
}
----
<1> Denotes a message bundle interface. The bundle name is defaulted to `msg` and is used as a namespace in templates expressions, e.g. `{msg:hello_name}`.
<2> Each method must be annotated with `@Message`. The value is a qute template. If no value is provided, then a corresponding value from a localized file is taken. If no such file exists an exception is thrown and the build fails.
<3> The method parameters can be used in the template.

The message bundles can be used at runtime:

1. Directly in your code via `io.quarkus.qute.i18n.MessageBundles#get()`; e.g. `MessageBundles.get(AppMessages.class).hello_name("Lucie")`
2. Injected in your beans via `@Inject`; e.g. `@Inject AppMessages`
Expand All @@ -2475,26 +2493,33 @@ Subsequently, the bundles can be used at runtime:
<3> `Lucie` is the parameter of the message bundle interface method.
<4> It is also possible to obtain a localized message for a key resolved at runtime using a reserved key `message`. The validation is skipped in this case though.

.Message Bundle Interface Example

==== Default Bundle Name

The bundle name is defaulted unless it's specified with `@MessageBundle#value()`.
For a top-level class the `msg` value is used by default.
For a nested class the name starts with `msg` followed by an underscore, followed by the simple names of all enclosing classes in the hierarchy (top-level class goes first) seperated by underscores.

For example, the name of the following message bundle will be defaulted to `msg_Index`:

[source,java]
----
import io.quarkus.qute.i18n.Message;
import io.quarkus.qute.i18n.MessageBundle;
class Index {
@MessageBundle <1>
public interface AppMessages {
@MessageBundle
interface Bundle {
@Message("Hello {name}!") <2>
String hello_name(String name); <3>
@Message("Hello {name}!")
String hello(String name);
}
}
----
<1> Denotes a message bundle interface. The bundle name is defaulted to `msg` and is used as a namespace in templates expressions, e.g. `{msg:hello_name}`.
<2> Each method must be annotated with `@Message`. The value is a qute template. If no value is provided, then a corresponding value from a localized file is taken. If no such file exists an exception is thrown and the build fails.
<3> The method parameters can be used in the template.

NOTE: The bundle name is also used as a part of the name of a localized file, e.g. `msg_Index` in the `msg_Index_de.properties`.

==== Bundle Name and Message Keys

Keys are used directly in templates.
Message keys are used directly in templates.
The bundle name is used as a namespace in template expressions.
The `@MessageBundle` can be used to define the default strategy used to generate message keys from method names.
However, the `@Message` can override this strategy and even define a custom key.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.jboss.jandex.AnnotationTarget.Kind;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.ClassInfo.NestingType;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;
Expand Down Expand Up @@ -102,7 +103,7 @@

public class MessageBundleProcessor {

private static final Logger LOGGER = Logger.getLogger(MessageBundleProcessor.class);
private static final Logger LOG = Logger.getLogger(MessageBundleProcessor.class);

private static final String SUFFIX = "_Bundle";
private static final String BUNDLE_DEFAULT_KEY = "defaultKey";
Expand Down Expand Up @@ -136,7 +137,31 @@ List<MessageBundleBuildItem> processBundles(BeanArchiveIndexBuildItem beanArchiv
ClassInfo bundleClass = bundleAnnotation.target().asClass();
if (Modifier.isInterface(bundleClass.flags())) {
AnnotationValue nameValue = bundleAnnotation.value();
String name = nameValue != null ? nameValue.asString() : MessageBundle.DEFAULT_NAME;
String name = nameValue != null ? nameValue.asString() : MessageBundle.DEFAULTED_NAME;
if (name.equals(MessageBundle.DEFAULTED_NAME)) {
if (bundleClass.nestingType() == NestingType.TOP_LEVEL) {
name = MessageBundle.DEFAULT_NAME;
} else {
// The name starts with the DEFAULT_NAME followed by an underscore, followed by simple names of all
// declaring classes in the hierarchy seperated by underscores
List<String> enclosingNames = new ArrayList<>();
DotName enclosingName = bundleClass.enclosingClass();
while (enclosingName != null) {
ClassInfo enclosingClass = index.getClassByName(enclosingName);
if (enclosingClass != null) {
enclosingNames.add(DotNames.simpleName(enclosingClass));
enclosingName = enclosingClass.nestingType() == NestingType.TOP_LEVEL ? null
: enclosingClass.enclosingClass();
}
}
enclosingNames.add(MessageBundle.DEFAULT_NAME);
// Class Bar declares nested class Foo and bundle Baz is declared as nested interface of Foo
// [Foo, Bar, msg] -> [msg, Bar, Foo]
Collections.reverse(enclosingNames);
name = String.join("_", enclosingNames);
}
LOG.debugf("Message bundle %s: name defaulted to %s", bundleClass, name);
}
if (!Namespaces.isValidNamespace(name)) {
throw new MessageBundleException(
String.format(
Expand Down Expand Up @@ -185,7 +210,10 @@ List<MessageBundleBuildItem> processBundles(BeanArchiveIndexBuildItem beanArchiv
String fileName = messageFile.getFileName().toString();
if (fileName.startsWith(name)) {
// msg_en.txt -> en
String locale = fileName.substring(fileName.indexOf('_') + 1, fileName.indexOf('.'));
// msg_Views_Index_cs.properties -> cs
// msg_Views_Index_cs-CZ.properties -> cs-CZ
// msg_Views_Index_cs_CZ.properties -> cs_CZ
String locale = fileName.substring(name.length() + 1, fileName.indexOf('.'));
// Support resource bundle naming convention
locale = locale.replace('_', '-');
ClassInfo localizedInterface = localeToInterface.get(locale);
Expand Down Expand Up @@ -338,7 +366,7 @@ void validateMessageBundleMethods(TemplatesAnalysisBuildItem templatesAnalysis,
// Log a warning if a parameter is not used in the template
for (String paramName : paramNames) {
if (!usedParamNames.contains(paramName)) {
LOGGER.warnf("Unused parameter found [%s] in the message template of: %s", paramName,
LOG.warnf("Unused parameter found [%s] in the message template of: %s", paramName,
messageBundleMethod.getMethod().declaringClass().name() + "#"
+ messageBundleMethod.getMethod().name() + "()");
}
Expand Down Expand Up @@ -432,12 +460,12 @@ public String apply(String id) {
HierarchyIndexer hierarchyIndexer = new HierarchyIndexer(index);

// bundle name -> (key -> method)
Map<String, Map<String, MethodInfo>> bundleMethodsMap = new HashMap<>();
Map<String, Map<String, MethodInfo>> bundleToMethods = new HashMap<>();
for (MessageBundleMethodBuildItem messageBundleMethod : messageBundleMethods) {
Map<String, MethodInfo> bundleMethods = bundleMethodsMap.get(messageBundleMethod.getBundleName());
Map<String, MethodInfo> bundleMethods = bundleToMethods.get(messageBundleMethod.getBundleName());
if (bundleMethods == null) {
bundleMethods = new HashMap<>();
bundleMethodsMap.put(messageBundleMethod.getBundleName(), bundleMethods);
bundleToMethods.put(messageBundleMethod.getBundleName(), bundleMethods);
}
bundleMethods.put(messageBundleMethod.getKey(), messageBundleMethod.getMethod());
}
Expand All @@ -447,7 +475,7 @@ public String apply(String id) {
bundlesMap.put(messageBundle.getName(), messageBundle.getDefaultBundleInterface());
}

for (Entry<String, Map<String, MethodInfo>> bundleEntry : bundleMethodsMap.entrySet()) {
for (Entry<String, Map<String, MethodInfo>> bundleEntry : bundleToMethods.entrySet()) {

Map<TemplateAnalysis, Set<Expression>> expressions = QuteProcessor.collectNamespaceExpressions(analysis,
bundleEntry.getKey());
Expand Down Expand Up @@ -778,12 +806,11 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d
Map<String, String> messageTemplates, String locale) {

ClassInfo bundleInterface = bundleInterfaceWrapper.getClassInfo();
LOGGER.debugf("Generate bundle implementation for %s", bundleInterface);
LOG.debugf("Generate bundle implementation for %s", bundleInterface);
AnnotationInstance bundleAnnotation = defaultBundleInterface != null
? defaultBundleInterface.declaredAnnotation(Names.BUNDLE)
: bundleInterface.declaredAnnotation(Names.BUNDLE);
AnnotationValue nameValue = bundleAnnotation.value();
String bundleName = nameValue != null ? nameValue.asString() : MessageBundle.DEFAULT_NAME;
String bundleName = bundle.getName();
AnnotationValue defaultKeyValue = bundleAnnotation.value(BUNDLE_DEFAULT_KEY);

String baseName;
Expand Down Expand Up @@ -820,7 +847,7 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d
String.format("A message bundle method must return java.lang.String: %s#%s",
bundleInterface, method.name()));
}
LOGGER.debugf("Found message bundle method %s on %s", method, bundleInterface);
LOG.debugf("Found message bundle method %s on %s", method, bundleInterface);

MethodCreator bundleMethod = bundleCreator.getMethodCreator(MethodDescriptor.of(method));

Expand All @@ -838,7 +865,7 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d
}

if (messageAnnotation == null) {
LOGGER.debugf("@Message not declared on %s#%s - using the default key/value", bundleInterface, method);
LOG.debugf("@Message not declared on %s#%s - using the default key/value", bundleInterface, method);
messageAnnotation = AnnotationInstance.builder(Names.MESSAGE).value(Message.DEFAULT_VALUE)
.add("name", Message.DEFAULT_NAME).build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.qute.deployment.i18n;

import static io.quarkus.qute.i18n.MessageBundle.DEFAULT_NAME;
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.jboss.shrinkwrap.api.asset.StringAsset;
Expand Down Expand Up @@ -59,7 +60,7 @@ public void testBothDefaultAndLocalizedFromFile() {
assertEquals("Abschied", deMessages.farewell());
}

@MessageBundle
@MessageBundle(DEFAULT_NAME)
public interface Messages {

@Message("Ahoj svete!")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.qute.deployment.i18n;

import static io.quarkus.qute.i18n.MessageBundle.DEFAULT_NAME;
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.jboss.shrinkwrap.api.asset.StringAsset;
Expand Down Expand Up @@ -38,7 +39,7 @@ public void testInterfaceIsMerged() {
assertEquals("Hello world!", messages.helloWorld());
}

@MessageBundle
@MessageBundle(DEFAULT_NAME)
public interface Messages {

@Message("Hello world!")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.qute.deployment.i18n;

import static io.quarkus.qute.i18n.MessageBundle.DEFAULT_NAME;
import static org.junit.jupiter.api.Assertions.assertEquals;

import jakarta.inject.Inject;
Expand Down Expand Up @@ -42,7 +43,7 @@ public void testLocalizedFile() {
assertEquals("Ahoj!", foo.instance().setAttribute("locale", "cs-CZ").render());
}

@MessageBundle
@MessageBundle(DEFAULT_NAME)
public interface Messages1 {

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

import static io.quarkus.qute.i18n.MessageBundle.DEFAULT_NAME;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.Locale;
Expand Down Expand Up @@ -37,7 +38,7 @@ public void testResolvers() {
assertEquals("Hello world!", foo.instance().setAttribute("locale", Locale.ENGLISH).render());
}

@MessageBundle
@MessageBundle(DEFAULT_NAME)
public interface Messages {

@Message("Ahoj světe!")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.quarkus.qute.deployment.i18n;

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

import java.util.Locale;

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

import io.quarkus.qute.i18n.MessageBundles;
import io.quarkus.test.QuarkusUnitTest;

public class MessageBundleDefaultedNameTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(Views.class)
.addAsResource(new StringAsset(
"{msg_Views_Index:hello(name)}"),
"templates/Index/index.html")
.addAsResource(new StringAsset("hello=Ahoj {name}!"), "messages/msg_Views_Index_cs.properties"));

@Test
public void testBundle() {
assertEquals("Hello world!",
Views.Index.Templates.index("world").render());
assertEquals("Ahoj svete!", Views.Index.Templates.index("svete")
.setAttribute(MessageBundles.ATTRIBUTE_LOCALE, Locale.forLanguageTag("cs")).render());
}

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

import static io.quarkus.qute.i18n.MessageBundle.DEFAULT_NAME;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

Expand Down Expand Up @@ -49,7 +50,7 @@ public void testValidation() {
fail();
}

@MessageBundle
@MessageBundle(DEFAULT_NAME)
public interface WrongBundle {

// item has no "foo" property, "bar" and "baf" are not parameters, string has no "baz" property
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class MessageBundleLocaleTest {
.withApplicationRoot((jar) -> jar
.addClasses(Messages.class)
.addAsResource(new StringAsset(
"{msg:helloWorld}"),
"{msg_MessageBundleLocaleTest:helloWorld}"),
"templates/foo.html"));

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

import static io.quarkus.qute.i18n.MessageBundle.DEFAULT_NAME;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.Locale;
Expand Down Expand Up @@ -38,7 +39,7 @@ public void testResolvers() {
foo.instance().setAttribute(MessageBundles.ATTRIBUTE_LOCALE, Locale.forLanguageTag("cs")).render());
}

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

@Message("Hello {name}!")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.qute.deployment.i18n;

import static io.quarkus.qute.i18n.MessageBundle.DEFAULT_NAME;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

Expand Down Expand Up @@ -46,7 +47,7 @@ public void testValidation() {
fail();
}

@MessageBundle
@MessageBundle(value = DEFAULT_NAME)
public interface MyBundle {

@Message("Hello {item.name}")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.qute.deployment.i18n;

import static io.quarkus.qute.i18n.MessageBundle.DEFAULT_NAME;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

Expand Down Expand Up @@ -44,7 +45,7 @@ public void testValidation() {
fail();
}

@MessageBundle
@MessageBundle(DEFAULT_NAME)
public interface Hellos {

@Message("Hello {foo}")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.quarkus.qute.deployment.i18n;

import io.quarkus.qute.CheckedTemplate;
import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.i18n.Message;
import io.quarkus.qute.i18n.MessageBundle;

public class Views {

public static class Index {

@CheckedTemplate
static class Templates {

static native TemplateInstance index(String name);

}

@MessageBundle
public interface Messages {

@Message("Hello {name}!")
String hello(String name);
}
}

}
Loading

0 comments on commit 37343bb

Please sign in to comment.