Skip to content

Commit 709b87b

Browse files
committed
Qute type-safe messages - change the default bundle name strategy
- use an approach similar to type-safe templates - the default bundle name of a nested class starts with "msg" and includes the simple names of all declaring classes in the hierarchy
1 parent b331513 commit 709b87b

18 files changed

+189
-45
lines changed

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

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2459,7 +2459,25 @@ In the development mode, all files located in `src/main/resources/templates` are
24592459
The basic idea is that every message is potentially a very simple template.
24602460
In order to prevent type errors a message is defined as an annotated method of a *message bundle interface*.
24612461
Quarkus generates the *message bundle implementation* at build time.
2462-
Subsequently, the bundles can be used at runtime:
2462+
2463+
.Message Bundle Interface Example
2464+
[source,java]
2465+
----
2466+
import io.quarkus.qute.i18n.Message;
2467+
import io.quarkus.qute.i18n.MessageBundle;
2468+
2469+
@MessageBundle <1>
2470+
public interface AppMessages {
2471+
2472+
@Message("Hello {name}!") <2>
2473+
String hello_name(String name); <3>
2474+
}
2475+
----
2476+
<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}`.
2477+
<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.
2478+
<3> The method parameters can be used in the template.
2479+
2480+
The message bundles can be used at runtime:
24632481

24642482
1. Directly in your code via `io.quarkus.qute.i18n.MessageBundles#get()`; e.g. `MessageBundles.get(AppMessages.class).hello_name("Lucie")`
24652483
2. Injected in your beans via `@Inject`; e.g. `@Inject AppMessages`
@@ -2475,26 +2493,33 @@ Subsequently, the bundles can be used at runtime:
24752493
<3> `Lucie` is the parameter of the message bundle interface method.
24762494
<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.
24772495

2478-
.Message Bundle Interface Example
2496+
2497+
==== Default Bundle Name
2498+
2499+
The bundle name is defaulted unless it's specified with `@MessageBundle#value()`.
2500+
For a top-level class the `msg` value is used by default.
2501+
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.
2502+
2503+
For example, the name of the following message bundle will be defaulted to `msg_Index`:
2504+
24792505
[source,java]
24802506
----
2481-
import io.quarkus.qute.i18n.Message;
2482-
import io.quarkus.qute.i18n.MessageBundle;
2507+
class Index {
24832508
2484-
@MessageBundle <1>
2485-
public interface AppMessages {
2509+
@MessageBundle
2510+
interface Bundle {
24862511
2487-
@Message("Hello {name}!") <2>
2488-
String hello_name(String name); <3>
2512+
@Message("Hello {name}!")
2513+
String hello(String name);
2514+
}
24892515
}
24902516
----
2491-
<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}`.
2492-
<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.
2493-
<3> The method parameters can be used in the template.
2517+
2518+
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`.
24942519

24952520
==== Bundle Name and Message Keys
24962521

2497-
Keys are used directly in templates.
2522+
Message keys are used directly in templates.
24982523
The bundle name is used as a namespace in template expressions.
24992524
The `@MessageBundle` can be used to define the default strategy used to generate message keys from method names.
25002525
However, the `@Message` can override this strategy and even define a custom key.

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

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.jboss.jandex.AnnotationTarget.Kind;
3737
import org.jboss.jandex.AnnotationValue;
3838
import org.jboss.jandex.ClassInfo;
39+
import org.jboss.jandex.ClassInfo.NestingType;
3940
import org.jboss.jandex.DotName;
4041
import org.jboss.jandex.IndexView;
4142
import org.jboss.jandex.MethodInfo;
@@ -102,7 +103,7 @@
102103

103104
public class MessageBundleProcessor {
104105

105-
private static final Logger LOGGER = Logger.getLogger(MessageBundleProcessor.class);
106+
private static final Logger LOG = Logger.getLogger(MessageBundleProcessor.class);
106107

107108
private static final String SUFFIX = "_Bundle";
108109
private static final String BUNDLE_DEFAULT_KEY = "defaultKey";
@@ -136,7 +137,31 @@ List<MessageBundleBuildItem> processBundles(BeanArchiveIndexBuildItem beanArchiv
136137
ClassInfo bundleClass = bundleAnnotation.target().asClass();
137138
if (Modifier.isInterface(bundleClass.flags())) {
138139
AnnotationValue nameValue = bundleAnnotation.value();
139-
String name = nameValue != null ? nameValue.asString() : MessageBundle.DEFAULT_NAME;
140+
String name = nameValue != null ? nameValue.asString() : MessageBundle.DEFAULTED_NAME;
141+
if (name.equals(MessageBundle.DEFAULTED_NAME)) {
142+
if (bundleClass.nestingType() == NestingType.TOP_LEVEL) {
143+
name = MessageBundle.DEFAULT_NAME;
144+
} else {
145+
// The name starts with the DEFAULT_NAME followed by an underscore, followed by simple names of all
146+
// declaring classes in the hierarchy seperated by underscores
147+
List<String> enclosingNames = new ArrayList<>();
148+
DotName enclosingName = bundleClass.enclosingClass();
149+
while (enclosingName != null) {
150+
ClassInfo enclosingClass = index.getClassByName(enclosingName);
151+
if (enclosingClass != null) {
152+
enclosingNames.add(DotNames.simpleName(enclosingClass));
153+
enclosingName = enclosingClass.nestingType() == NestingType.TOP_LEVEL ? null
154+
: enclosingClass.enclosingClass();
155+
}
156+
}
157+
enclosingNames.add(MessageBundle.DEFAULT_NAME);
158+
// Class Bar declares nested class Foo and bundle Baz is declared as nested interface of Foo
159+
// [Foo, Bar, msg] -> [msg, Bar, Foo]
160+
Collections.reverse(enclosingNames);
161+
name = String.join("_", enclosingNames);
162+
}
163+
LOG.debugf("Message bundle %s: name defaulted to %s", bundleClass, name);
164+
}
140165
if (!Namespaces.isValidNamespace(name)) {
141166
throw new MessageBundleException(
142167
String.format(
@@ -185,7 +210,10 @@ List<MessageBundleBuildItem> processBundles(BeanArchiveIndexBuildItem beanArchiv
185210
String fileName = messageFile.getFileName().toString();
186211
if (fileName.startsWith(name)) {
187212
// msg_en.txt -> en
188-
String locale = fileName.substring(fileName.indexOf('_') + 1, fileName.indexOf('.'));
213+
// msg_Views_Index_cs.properties -> cs
214+
// msg_Views_Index_cs-CZ.properties -> cs-CZ
215+
// msg_Views_Index_cs_CZ.properties -> cs_CZ
216+
String locale = fileName.substring(name.length() + 1, fileName.indexOf('.'));
189217
// Support resource bundle naming convention
190218
locale = locale.replace('_', '-');
191219
ClassInfo localizedInterface = localeToInterface.get(locale);
@@ -338,7 +366,7 @@ void validateMessageBundleMethods(TemplatesAnalysisBuildItem templatesAnalysis,
338366
// Log a warning if a parameter is not used in the template
339367
for (String paramName : paramNames) {
340368
if (!usedParamNames.contains(paramName)) {
341-
LOGGER.warnf("Unused parameter found [%s] in the message template of: %s", paramName,
369+
LOG.warnf("Unused parameter found [%s] in the message template of: %s", paramName,
342370
messageBundleMethod.getMethod().declaringClass().name() + "#"
343371
+ messageBundleMethod.getMethod().name() + "()");
344372
}
@@ -432,12 +460,12 @@ public String apply(String id) {
432460
HierarchyIndexer hierarchyIndexer = new HierarchyIndexer(index);
433461

434462
// bundle name -> (key -> method)
435-
Map<String, Map<String, MethodInfo>> bundleMethodsMap = new HashMap<>();
463+
Map<String, Map<String, MethodInfo>> bundleToMethods = new HashMap<>();
436464
for (MessageBundleMethodBuildItem messageBundleMethod : messageBundleMethods) {
437-
Map<String, MethodInfo> bundleMethods = bundleMethodsMap.get(messageBundleMethod.getBundleName());
465+
Map<String, MethodInfo> bundleMethods = bundleToMethods.get(messageBundleMethod.getBundleName());
438466
if (bundleMethods == null) {
439467
bundleMethods = new HashMap<>();
440-
bundleMethodsMap.put(messageBundleMethod.getBundleName(), bundleMethods);
468+
bundleToMethods.put(messageBundleMethod.getBundleName(), bundleMethods);
441469
}
442470
bundleMethods.put(messageBundleMethod.getKey(), messageBundleMethod.getMethod());
443471
}
@@ -447,7 +475,7 @@ public String apply(String id) {
447475
bundlesMap.put(messageBundle.getName(), messageBundle.getDefaultBundleInterface());
448476
}
449477

450-
for (Entry<String, Map<String, MethodInfo>> bundleEntry : bundleMethodsMap.entrySet()) {
478+
for (Entry<String, Map<String, MethodInfo>> bundleEntry : bundleToMethods.entrySet()) {
451479

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

780808
ClassInfo bundleInterface = bundleInterfaceWrapper.getClassInfo();
781-
LOGGER.debugf("Generate bundle implementation for %s", bundleInterface);
809+
LOG.debugf("Generate bundle implementation for %s", bundleInterface);
782810
AnnotationInstance bundleAnnotation = defaultBundleInterface != null
783811
? defaultBundleInterface.declaredAnnotation(Names.BUNDLE)
784812
: bundleInterface.declaredAnnotation(Names.BUNDLE);
785-
AnnotationValue nameValue = bundleAnnotation.value();
786-
String bundleName = nameValue != null ? nameValue.asString() : MessageBundle.DEFAULT_NAME;
813+
String bundleName = bundle.getName();
787814
AnnotationValue defaultKeyValue = bundleAnnotation.value(BUNDLE_DEFAULT_KEY);
788815

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

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

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

840867
if (messageAnnotation == null) {
841-
LOGGER.debugf("@Message not declared on %s#%s - using the default key/value", bundleInterface, method);
868+
LOG.debugf("@Message not declared on %s#%s - using the default key/value", bundleInterface, method);
842869
messageAnnotation = AnnotationInstance.builder(Names.MESSAGE).value(Message.DEFAULT_VALUE)
843870
.add("name", Message.DEFAULT_NAME).build();
844871
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.quarkus.qute.deployment.i18n;
22

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

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

62-
@MessageBundle
63+
@MessageBundle(DEFAULT_NAME)
6364
public interface Messages {
6465

6566
@Message("Ahoj svete!")

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.quarkus.qute.deployment.i18n;
22

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

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

41-
@MessageBundle
42+
@MessageBundle(DEFAULT_NAME)
4243
public interface Messages {
4344

4445
@Message("Hello world!")

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.quarkus.qute.deployment.i18n;
22

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

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

45-
@MessageBundle
46+
@MessageBundle(DEFAULT_NAME)
4647
public interface Messages1 {
4748

4849
@Message

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.quarkus.qute.deployment.i18n;
22

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

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

40-
@MessageBundle
41+
@MessageBundle(DEFAULT_NAME)
4142
public interface Messages {
4243

4344
@Message("Ahoj světe!")
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package io.quarkus.qute.deployment.i18n;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import java.util.Locale;
6+
7+
import org.jboss.shrinkwrap.api.asset.StringAsset;
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.api.extension.RegisterExtension;
10+
11+
import io.quarkus.qute.i18n.MessageBundles;
12+
import io.quarkus.test.QuarkusUnitTest;
13+
14+
public class MessageBundleDefaultedNameTest {
15+
16+
@RegisterExtension
17+
static final QuarkusUnitTest config = new QuarkusUnitTest()
18+
.withApplicationRoot((jar) -> jar
19+
.addClasses(Views.class)
20+
.addAsResource(new StringAsset(
21+
"{msg_Views_Index:hello(name)}"),
22+
"templates/Index/index.html")
23+
.addAsResource(new StringAsset("hello=Ahoj {name}!"), "messages/msg_Views_Index_cs.properties"));
24+
25+
@Test
26+
public void testBundle() {
27+
assertEquals("Hello world!",
28+
Views.Index.Templates.index("world").render());
29+
assertEquals("Ahoj svete!", Views.Index.Templates.index("svete")
30+
.setAttribute(MessageBundles.ATTRIBUTE_LOCALE, Locale.forLanguageTag("cs")).render());
31+
}
32+
33+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.quarkus.qute.deployment.i18n;
22

3+
import static io.quarkus.qute.i18n.MessageBundle.DEFAULT_NAME;
34
import static org.junit.jupiter.api.Assertions.assertTrue;
45
import static org.junit.jupiter.api.Assertions.fail;
56

@@ -49,7 +50,7 @@ public void testValidation() {
4950
fail();
5051
}
5152

52-
@MessageBundle
53+
@MessageBundle(DEFAULT_NAME)
5354
public interface WrongBundle {
5455

5556
// item has no "foo" property, "bar" and "baf" are not parameters, string has no "baz" property

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public class MessageBundleLocaleTest {
2323
.withApplicationRoot((jar) -> jar
2424
.addClasses(Messages.class)
2525
.addAsResource(new StringAsset(
26-
"{msg:helloWorld}"),
26+
"{msg_MessageBundleLocaleTest:helloWorld}"),
2727
"templates/foo.html"));
2828

2929
@Inject

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.quarkus.qute.deployment.i18n;
22

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

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

41-
@MessageBundle(locale = "en")
42+
@MessageBundle(value = DEFAULT_NAME, locale = "en")
4243
public interface Messages {
4344

4445
@Message("Hello {name}!")

0 commit comments

Comments
 (0)