tessl install tessl/maven-io-quarkus--quarkus-qute@3.30.0Offer templating support for web, email, etc in a build time, type-safe way
Qute message bundles provide a type-safe, compile-time validated internationalization (i18n) system for Quarkus applications. Message bundles allow you to define localized messages as Java interfaces, access them from templates and Java code, and manage translations through properties files or programmatic interfaces.
Message bundles integrate seamlessly with Qute's template engine and leverage CDI for dependency injection and locale resolution. All message templates are validated at build time, ensuring that parameter references are correct and that required translations are provided.
The @MessageBundle annotation marks an interface as a message bundle. Each method on the interface represents a localized message that can be accessed from templates or injected into CDI beans.
@Retention(RUNTIME)
@Target(TYPE)
public @interface MessageBundle {
String DEFAULT_LOCALE = "<<default locale>>";
String DEFAULT_NAME = "msg";
String DEFAULTED_NAME = "<<defaulted name>>";
/**
* The name used as a namespace in template expressions and as part of
* localized file names. Default is DEFAULTED_NAME.
*/
String value() default DEFAULTED_NAME;
/**
* The default key strategy for message methods.
* One of: ELEMENT_NAME, HYPHENATED_ELEMENT_NAME, UNDERSCORED_ELEMENT_NAME
*/
String defaultKey() default Message.ELEMENT_NAME;
/**
* The language tag (IETF) of the default locale.
* Default is DEFAULT_LOCALE (uses quarkus.default-locale config property).
*/
String locale() default DEFAULT_LOCALE;
}The bundle name determines the namespace used in templates and the prefix for properties files:
"msg", accessed as {msg:methodName} with properties file msg_locale.propertiesvalue() attribute// Default name "msg"
@MessageBundle
interface Messages {
@Message("Hello!")
String hello();
}
// Template usage: {msg:hello}
// Properties file: msg_en.properties
// Custom name
@MessageBundle("app")
interface AppMessages {
@Message("Welcome!")
String welcome();
}
// Template usage: {app:welcome}
// Properties file: app_de.properties
// Nested bundle - name is "Controller_index"
class Controller {
@MessageBundle
interface index {
@Message("Hello {name}!")
String hello(String name);
}
}
// Template usage: {Controller_index:hello('World')}
// Properties file: Controller_index_fr.propertiesThe defaultKey() attribute sets the strategy for generating message keys from method names:
// ELEMENT_NAME (default) - use method name as-is
@MessageBundle
interface Messages {
@Message("Hello!")
String hello_world(); // key: "hello_world"
}
// HYPHENATED_ELEMENT_NAME - convert camelCase to kebab-case
@MessageBundle(defaultKey = Message.HYPHENATED_ELEMENT_NAME)
interface AlphaMessages {
@Message("Hello!")
String helloWorld(); // key: "hello-world"
}
// UNDERSCORED_ELEMENT_NAME - convert camelCase to snake_case
@MessageBundle(defaultKey = Message.UNDERSCORED_ELEMENT_NAME)
interface BetaMessages {
@Message("Hello!")
String helloWorld(); // key: "hello_world"
}The locale() attribute defines the default locale for the bundle:
// Use application default locale (from quarkus.default-locale)
@MessageBundle
interface Messages { }
// Explicit locale for English bundle
@MessageBundle(locale = "en")
interface EnglishMessages { }
// Explicit locale for German bundle
@MessageBundle(locale = "de")
interface GermanMessages { }The @Message annotation defines a message template on a bundle method. All bundle methods must return String.
@Retention(RUNTIME)
@Target(METHOD)
public @interface Message {
String DEFAULT_NAME = "<<default>>";
String ELEMENT_NAME = "<<element name>>";
String DEFAULT_VALUE = "<<default value>>";
String HYPHENATED_ELEMENT_NAME = "<<hyphenated element name>>";
String UNDERSCORED_ELEMENT_NAME = "<<underscored element name>>";
/**
* The key of the message. Can be an explicit string or a strategy constant.
*/
String key() default DEFAULT_NAME;
/**
* The message template. Has priority over localized file values.
*/
String value() default DEFAULT_VALUE;
/**
* The default template used if value() and localized file are not provided.
*/
String defaultValue() default DEFAULT_VALUE;
}Message templates use Qute template syntax and can reference method parameters:
@MessageBundle
interface Messages {
// Simple message
@Message("Hello world!")
String hello();
// Message with parameter
@Message("Hello {name}!")
String hello_name(String name);
// Message with multiple parameters
@Message("Hello {name} {surname}!")
String hello_fullname(String name, String surname);
// Message with object property access
@Message("Item name: {item.name}, age: {item.age}")
String itemDetail(Item item);
// Message with expressions and sections
@Message("{#if count eq 1}is one file{#else}are {count} files{/if}")
String fileCount(int count);
// Message referencing other bundle methods
@Message("There {msg:fileStrings(numberOfFiles)} on {disk}.")
String files(int numberOfFiles, String disk);
@Message("{#when numberOfFiles}"
+ "{#is 0}are no files"
+ "{#is 1}is one file"
+ "{#else}are {numberOfFiles} files"
+ "{/when}")
String fileStrings(int numberOfFiles);
}{msg:hello}
{msg:hello_name('Jachym')}
{msg:hello_fullname('John', 'Doe')}
{msg:itemDetail(myItem)}
{msg:files(100, 'C')}Message keys can be specified explicitly or derived from method names:
@MessageBundle
interface Messages {
// Key derived from method name: "hello"
@Message("Hello!")
String hello();
// Explicit key
@Message(key = "greeting", value = "Hello!")
String hello_message();
// Key with dots (accessed via bracket notation in templates)
@Message(key = "dot.test", value = "Dot test!")
String dotTest();
// Use bundle's defaultKey strategy
@Message(key = Message.ELEMENT_NAME, value = "Hi!")
String greet();
// Convert to hyphenated
@Message(key = Message.HYPHENATED_ELEMENT_NAME, value = "Bye!")
String sayGoodbye(); // key: "say-goodbye"
// Convert to underscored
@Message(key = Message.UNDERSCORED_ELEMENT_NAME, value = "Welcome!")
String welcomeUser(); // key: "welcome_user"
}Template usage with special keys:
{msg:hello}
{msg:greeting}
{msg:['dot.test']}Message values are resolved in the following order:
If none are provided, the build fails.
@MessageBundle
interface Messages {
// Value from annotation (highest priority)
@Message("Hello from annotation!")
String hello();
// Value from localized file only
@Message
String goodbye(); // Must exist in messages/msg.properties
// Fallback to defaultValue if not in file
@Message(defaultValue = "Default greeting")
String greet();
// Annotation value overrides file
@Message("Always this value")
String override(); // File value ignored even if present
}The @Localized annotation is a CDI qualifier that marks a localized variant of a message bundle interface. It enables injection of locale-specific bundle implementations.
@Qualifier
@Retention(RUNTIME)
@Target({ TYPE, METHOD, FIELD, PARAMETER })
public @interface Localized {
/**
* The locale language tag string (IETF).
*/
String value();
/**
* Programmatic literal for use with CDI APIs.
*/
public static final class Literal extends AnnotationLiteral<Localized>
implements Localized {
/**
* Factory method to create a Localized literal.
* @param value the locale language tag
* @return new Literal instance
*/
public static Literal of(String value);
/**
* Constructor to create a Localized literal.
* @param value the locale language tag
*/
public Literal(String value);
/**
* @return the locale language tag
*/
String value();
}
}import io.quarkus.qute.i18n.Localized;
import io.quarkus.qute.i18n.MessageBundle;
import jakarta.inject.Inject;
@MessageBundle
interface Messages {
@Message("Hello!")
String hello();
@Message("Hello {name}!")
String hello_name(String name);
}
public class MyService {
// Default locale bundle
@Inject
Messages messages;
// Czech locale bundle
@Inject
@Localized("cs")
Messages czechMessages;
// German locale bundle
@Inject
@Localized("de")
Messages germanMessages;
public void greet() {
System.out.println(messages.hello()); // Hello!
System.out.println(czechMessages.hello()); // Ahoj!
System.out.println(germanMessages.hello()); // Hallo!
}
}Create explicit interfaces for localized bundles by extending the base bundle and annotating with @Localized:
@MessageBundle
interface Messages {
@Message("Hello!")
String hello();
@Message("Goodbye!")
String goodbye();
}
// English localized interface
@Localized("en")
interface EnglishMessages extends Messages {
@Message("Hello from England!")
@Override
String hello();
}
// Czech localized interface
@Localized("cs")
interface CzechMessages extends Messages {
@Message("Ahoj!")
@Override
String hello();
// Can leave goodbye() to use default or file-based value
}
// German localized interface (uses only file-based values)
@Localized("de")
interface GermanMessages extends Messages {
// All values from msg_de.properties file
}The @MessageParam annotation binds a method parameter to a named template parameter, allowing parameter names to differ from template variable names.
@Retention(RUNTIME)
@Target(PARAMETER)
public @interface MessageParam {
/**
* The name of the parameter in the template.
* Default is Message.ELEMENT_NAME (use actual parameter name).
*/
String value() default Message.ELEMENT_NAME;
}@MessageBundle
interface Messages {
// Without @MessageParam - parameter name must match template variable
@Message("Hello {name}!")
String hello(String name);
// With @MessageParam - parameter name can differ from template variable
@Message("Hello {name}!")
String greet(@MessageParam("name") String firstName);
// Multiple mapped parameters
@Message("User {username} has {points} points")
String userInfo(
@MessageParam("username") String login,
@MessageParam("points") int score
);
// Mix of mapped and unmapped parameters
@Message("Hello {name}{suffix}")
String hello_custom(@MessageParam("name") String foo, String suffix);
}Usage:
Messages msg = MessageBundles.get(Messages.class);
msg.hello("World"); // Hello World!
msg.greet("Alice"); // Hello Alice!
msg.userInfo("bob", 100); // User bob has 100 points
msg.hello_custom("there", "!"); // Hello there!The MessageBundles class provides static utility methods for programmatic access to message bundles.
public final class MessageBundles {
public static final String ATTRIBUTE_LOCALE = TemplateInstance.LOCALE;
public static final String DEFAULT_LOCALE = "<<default>>";
/**
* Get the default locale bundle implementation.
*/
public static <T> T get(Class<T> bundleInterface);
/**
* Get a localized bundle implementation.
*/
public static <T> T get(Class<T> bundleInterface, Localized localized);
/**
* Get a message template by ID.
*/
public static Template getTemplate(String id);
}import io.quarkus.qute.i18n.MessageBundles;
import io.quarkus.qute.i18n.Localized;
// Get default locale bundle
Messages msg = MessageBundles.get(Messages.class);
String greeting = msg.hello_name("Alice"); // Hello Alice!
// Get specific locale bundle
Messages czechMsg = MessageBundles.get(Messages.class, Localized.Literal.of("cs"));
String czechGreeting = czechMsg.hello_name("Alice"); // Ahoj Alice!
Messages germanMsg = MessageBundles.get(Messages.class, Localized.Literal.of("de"));
String germanGreeting = germanMsg.hello_name("Alice"); // Hallo Alice!
// Get language-only locale (falls back to language if exact match not found)
Messages deMsg = MessageBundles.get(Messages.class, Localized.Literal.of("de"));
// Matches "de" locale bundles, or "de-AT", "de-CH", etc.// Throws IllegalArgumentException if not an interface
MessageBundles.get(String.class);
// Throws IllegalArgumentException if not annotated with @MessageBundle or @Localized
MessageBundles.get(Engine.class);
// Throws IllegalStateException if locale not available
MessageBundles.get(Messages.class, Localized.Literal.of("hu"));Message bundles can be supplemented or defined entirely through properties files. Files must be placed in the messages/ directory on the classpath.
messages/<bundle-name>_<locale>.propertiesExamples:
messages/msg_en.properties - English messages for "msg" bundlemessages/msg_de.properties - German messages for "msg" bundlemessages/msg_cs.properties - Czech messages for "msg" bundlemessages/app_fr.properties - French messages for "app" bundlemessages/Controller_index_es.properties - Spanish messages for nested bundle# Simple message
hello=Hello world!
# Message with parameters (using Qute syntax)
hello_name=Hello {name}!
# Message with multiple parameters
hello_fullname=Hello {name} {surname}!
# Message with expressions
itemDetail=Item name: {item.name}, age: {item.age}
# Multi-line message with backslash continuation
longMessage=This is a very long message \
that spans multiple lines \
in the properties file.
# Message with Qute sections
fileCount={#if count eq 1}is one file{#else}are {count} files{/if}
# Message with when section
fileStrings={#when numberOfFiles}\
{#is 0}are no files\
{#is 1}is one file\
{#else}are {numberOfFiles} files\
{/when}
# Keys with dots (special access required)
dot.test=Dot test!messages/msg_en.properties:
hello=Hello world!
hello_name=Hello {name}!
goodbye=Goodbye!
farewell=See you later!messages/msg_de.properties:
hello=Hallo Welt!
hello_name=Hallo {name}!
goodbye=Auf Wiedersehen!
farewell=Bis später!messages/msg_cs.properties:
hello=Ahoj světe!
hello_name=Ahoj {name}!
goodbye=Sbohem!
farewell=Nashledanou!Properties files are merged with annotation values. Annotation @Message(value) always takes precedence:
@MessageBundle
interface Messages {
// Annotation value always wins (file value ignored)
@Message("Hello world from annotation!")
String hello();
// File value used
@Message
String goodbye();
// File can override if annotation has no value
@Message(key = "farewell")
String farewell();
// Default value used if not in file
@Message(defaultValue = "Default greeting!")
String greet();
}messages/msg_en.properties:
# This value is IGNORED because annotation has value()
hello=This will be ignored
# This value is USED
goodbye=Goodbye from file!
# This value OVERRIDES default
farewell=Farewell from file!
# greet() falls back to defaultValue since not in fileMessage bundles provide convenient enum localization support. When a message method accepts a single enum parameter and has no template defined, a generated template is created automatically.
@TemplateEnum
enum Status {
ON,
OFF,
UNDEFINED
}
@MessageBundle
interface Messages {
// Automatically generates:
// @Message("{#when status}"
// + "{#is ON}{msg:status_ON}"
// + "{#is OFF}{msg:status_OFF}"
// + "{#is UNDEFINED}{msg:status_UNDEFINED}"
// + "{/when}")
@Message
String status(Status status);
}This requires corresponding entries in properties files for each enum constant.
Standard enums use methodName_CONSTANT pattern:
# messages/msg_en.properties
status_ON=On
status_OFF=Off
status_UNDEFINED=Undefined# messages/msg_de.properties
status_ON=An
status_OFF=Aus
status_UNDEFINED=UndefiniertIf any constant contains _ or $, all keys must use the _$ separator:
@TemplateEnum
enum UnderscoredEnum {
A_B,
FOO_BAR_BAZ
}
@MessageBundle
interface Messages {
@Message
String underscored(UnderscoredEnum val);
}Properties file with _$ separator:
# messages/msg_en.properties
underscored_$A_B=A/B
underscored_$FOO_BAR_BAZ=Foo/Bar/Baz@TemplateEnum
enum AnotherEnum {
NEXT_B
}
@TemplateEnum
enum UncommonEnum {
NEXT$B
}
@MessageBundle
interface Messages {
// Keys: underscored_foo_$NEXT_B
@Message
String underscored_foo(AnotherEnum val);
// Keys: underscored$foo_$NEXT_B
@Message
String underscored$foo(AnotherEnum val);
// Keys: uncommon_$NEXT$B
@Message
String uncommon(UncommonEnum val);
}Properties:
underscored_foo_$NEXT_B=Next B value
underscored$foo_$NEXT_B=Next B alternative
uncommon_$NEXT$B=Uncommon valueYou can override the automatic enum template generation:
@MessageBundle
interface Messages {
// Custom template instead of automatic generation
@Message("{#when status}"
+ "{#is ON}+"
+ "{#is OFF}-"
+ "{#else}_"
+ "{/when}")
String customStatus(Status status);
}Properties files can also override enum templates:
# Custom template in properties file
locFileOverride={#when status}\
{#is ON}on\
{#is OFF}off\
{#else}undefined\
{/when}@MessageBundle
interface Messages {
// Template from localized file
@Message
String locFileOverride(Status status);
}The default locale for message bundles is configured via the quarkus.default-locale property:
# application.properties
quarkus.default-locale=csBundles using MessageBundle.DEFAULT_LOCALE (or omitting the locale attribute) will use this default:
// Uses quarkus.default-locale
@MessageBundle
interface Messages { }
// Explicitly uses default
@MessageBundle(locale = MessageBundle.DEFAULT_LOCALE)
interface OtherMessages { }
// Explicitly sets locale (overrides default)
@MessageBundle(locale = "en")
interface EnglishMessages { }Templates resolve message bundle locale in the following order:
MessageBundles.ATTRIBUTE_LOCALE or TemplateInstance.LOCALETemplateInstance.setLocale()Template template = engine.getTemplate("mytemplate");
// Use default locale
String result1 = template.render();
// Set locale via attribute
String result2 = template.instance()
.setAttribute(MessageBundles.ATTRIBUTE_LOCALE, Locale.forLanguageTag("cs"))
.render();
// Set locale via setLocale
String result3 = template.instance()
.setLocale(Locale.GERMAN)
.render();When resolving localized bundles, the system tries:
@MessageBundle
interface Messages {
@Message("Hello!")
String hello();
}
@Localized("en")
interface EnMessages extends Messages {
@Override
@Message("Hello from England!")
String hello();
}
@Localized("de")
interface DeMessages extends Messages {
@Override
@Message("Hallo!")
String hello();
}
// Usage
Messages msg = MessageBundles.get(Messages.class, Localized.Literal.of("de-AT"));
// Matches "de" interface since no exact "de-AT" exists
// Returns: "Hallo!"
Messages msg2 = MessageBundles.get(Messages.class, Localized.Literal.of("fr"));
// No match for "fr", falls back to default bundle
// Returns: "Hello!"In web applications, locale is typically determined from HTTP headers:
@Path("/greet")
public class GreetingResource {
@Inject
Messages messages;
@Inject
Template greeting;
@GET
@Produces(MediaType.TEXT_HTML)
public TemplateInstance get(@Context HttpHeaders headers) {
Locale locale = headers.getAcceptableLanguages().get(0);
return greeting.instance()
.setLocale(locale);
}
}Template greeting.html:
<h1>{msg:hello}</h1>
<p>{msg:welcome_name(user.name)}</p>Message bundles are accessible in Qute templates via namespace syntax.
{bundleName:methodName}
{bundleName:methodName(arg1)}
{bundleName:methodName(arg1, arg2)}@MessageBundle // Default name "msg"
interface Messages {
@Message("Hello world!")
String hello();
@Message("Hello {name}!")
String hello_name(String name);
}Template:
<h1>{msg:hello}</h1>
<p>{msg:hello_name('Alice')}</p>
<p>{msg:hello_name(user.name)}</p>Output:
<h1>Hello world!</h1>
<p>Hello Alice!</p>
<p>Hello John!</p>@MessageBundle("app")
interface AppMessages {
@Message("Welcome!")
String welcome();
}
@MessageBundle("errors")
interface ErrorMessages {
@Message("Error: {message}")
String error(String message);
}Template:
<h1>{app:welcome}</h1>
<div class="error">{errors:error('File not found')}</div>Access messages dynamically using the special message() method:
@MessageBundle
interface Messages {
@Message("Hello!")
String hello();
@Message("Hello {name}!")
String hello_name(String name);
@Message("Hello {name} {surname}!")
String hello_fullname(String name, String surname);
}Template with dynamic keys:
{! Access message by key string !}
{msg:message('hello')}
{! Dynamic key with parameters !}
{msg:message('hello_name', 'Alice')}
{! Dynamic key from variable !}
{msg:message(messageKey, param1, param2)}Usage:
template.data("messageKey", "hello_fullname")
.data("param1", "John")
.data("param2", "Doe")
.render();
// Output: Hello John Doe!Messages with dots in keys require bracket notation:
@MessageBundle
interface Messages {
@Message(key = "dot.test", value = "Dot test!")
String dotTest();
@Message(key = "validation.email", value = "Invalid email")
String validationEmail();
}Template:
{msg:['dot.test']}
{msg:['validation.email']}Messages can reference other messages from the same bundle:
@MessageBundle
interface Messages {
@Message("There {msg:fileStrings(count)} on {disk}.")
String files(int count, String disk);
@Message("{#when count}"
+ "{#is 0}are no files"
+ "{#is 1}is one file"
+ "{#else}are {count} files"
+ "{/when}")
String fileStrings(int count);
}Template:
{msg:files(0, 'C')} {! There are no files on C. !}
{msg:files(1, 'D')} {! There is one file on D. !}
{msg:files(100, 'E')} {! There are 100 files on E. !}Template template = engine.getTemplate("mytemplate");
// Default locale
template.render();
// Czech locale via attribute
template.instance()
.setAttribute(MessageBundles.ATTRIBUTE_LOCALE, Locale.forLanguageTag("cs"))
.render();
// German locale via setLocale
template.instance()
.setLocale(Locale.GERMAN)
.render();The same template renders different messages based on locale:
Template foo.html:
<h1>{msg:hello}</h1>
<p>{msg:hello_name('Jachym')}</p>Output with default locale (en):
<h1>Hello world!</h1>
<p>Hello Jachym!</p>Output with Czech locale (cs):
<h1>Ahoj světe!</h1>
<p>Ahoj Jachym!</p>Output with German locale (de):
<h1>Hallo Welt!</h1>
<p>Hallo Jachym!</p>import io.quarkus.qute.Qute;
// Simple message
String msg1 = Qute.fmt("{msg:hello}").render();
// Message with parameters
String msg2 = Qute.fmt("{msg:files(0,'C')}").render();
// With explicit locale
String msg3 = Qute.fmt("{msg:hello}")
.attribute("locale", Locale.GERMAN)
.render();Message bundles are automatically registered as CDI beans and can be injected into application components.
import io.quarkus.qute.i18n.MessageBundle;
import io.quarkus.qute.i18n.Localized;
import jakarta.inject.Inject;
@MessageBundle
interface Messages {
@Message("Hello!")
String hello();
@Message("Hello {name}!")
String hello_name(String name);
}
public class MyService {
@Inject
Messages messages;
public String greet(String name) {
return messages.hello_name(name);
}
}@MessageBundle
interface Messages {
@Message("Hello!")
String hello();
}
@Localized("cs")
interface CzechMessages extends Messages {
@Override
@Message("Ahoj!")
String hello();
}
@Localized("de")
interface GermanMessages extends Messages {
// Uses file-based values
}
public class GreetingService {
@Inject
Messages defaultMessages;
@Inject
@Localized("cs")
Messages czechMessages;
@Inject
@Localized("de")
Messages germanMessages;
public void greetInAllLanguages() {
System.out.println(defaultMessages.hello()); // Hello!
System.out.println(czechMessages.hello()); // Ahoj!
System.out.println(germanMessages.hello()); // Hallo!
}
}public class MyService {
// Field injection
@Inject
Messages messages;
@Inject
@Localized("cs")
Messages czechMessages;
}
public class OtherService {
private final Messages messages;
private final Messages czechMessages;
// Constructor injection
@Inject
public OtherService(
Messages messages,
@Localized("cs") Messages czechMessages) {
this.messages = messages;
this.czechMessages = czechMessages;
}
}import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
import io.quarkus.qute.i18n.Localized;
@ApplicationScoped
public class MessageProducer {
@Produces
@Localized("custom")
public Messages customMessages() {
// Custom logic to create localized bundle
return MessageBundles.get(Messages.class, Localized.Literal.of("cs"));
}
}
// Inject the produced bundle
@Inject
@Localized("custom")
Messages customMessages;import jakarta.enterprise.context.RequestScoped;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Inject;
import jakarta.ws.rs.core.HttpHeaders;
import java.util.Locale;
@RequestScoped
public class LocaleProducer {
@Inject
HttpHeaders headers;
@Produces
@RequestScoped
public Locale currentLocale() {
return headers.getAcceptableLanguages().get(0);
}
}
@RequestScoped
public class GreetingService {
@Inject
Messages messages;
@Inject
Locale currentLocale;
public String greet(String name) {
// Get bundle for request locale
Messages localizedMsg = MessageBundles.get(
Messages.class,
Localized.Literal.of(currentLocale.toLanguageTag())
);
return localizedMsg.hello_name(name);
}
}import io.quarkus.qute.Template;
import jakarta.inject.Inject;
public class GreetingResource {
@Inject
Template greeting; // templates/greeting.html
@Inject
Messages messages;
@GET
public TemplateInstance get() {
return greeting.data("customMessage", messages.hello());
}
}Template greeting.html:
<h1>{customMessage}</h1>
<p>{msg:hello_name('World')}</p>@MessageBundle
public interface AppMessages {
@Message("Hello world!")
String hello();
@Message("Hello {name}!")
String hello_name(String name);
@Message("Goodbye {name}, see you later!")
String goodbye(String name);
}Properties file messages/msg_de.properties:
hello=Hallo Welt!
hello_name=Hallo {name}!
goodbye=Auf Wiedersehen {name}, bis später!Template index.html:
<!DOCTYPE html>
<html>
<head>
<title>{msg:hello}</title>
</head>
<body>
<h1>{msg:hello}</h1>
<p>{msg:hello_name(user.name)}</p>
<p>{msg:goodbye(user.name)}</p>
</body>
</html>Java usage:
@Inject
AppMessages messages;
@Inject
@Localized("de")
AppMessages germanMessages;
public void greet() {
System.out.println(messages.hello()); // Hello world!
System.out.println(germanMessages.hello()); // Hallo Welt!
}@TemplateEnum
public enum OrderStatus {
PENDING,
PROCESSING,
SHIPPED,
DELIVERED,
CANCELLED
}
@MessageBundle
public interface OrderMessages {
@Message
String orderStatus(OrderStatus status);
@Message("Order #{orderId} is {msg:orderStatus(status)}")
String orderInfo(String orderId, OrderStatus status);
}Properties file messages/msg_en.properties:
orderStatus_PENDING=Pending
orderStatus_PROCESSING=Processing
orderStatus_SHIPPED=Shipped
orderStatus_DELIVERED=Delivered
orderStatus_CANCELLED=CancelledProperties file messages/msg_de.properties:
orderStatus_PENDING=Ausstehend
orderStatus_PROCESSING=In Bearbeitung
orderStatus_SHIPPED=Versandt
orderStatus_DELIVERED=Geliefert
orderStatus_CANCELLED=StorniertTemplate:
<div class="order">
<p>Status: {msg:orderStatus(order.status)}</p>
<p>{msg:orderInfo(order.id, order.status)}</p>
</div>@MessageBundle("app")
public interface AppMessages {
@Message("Welcome to our application!")
String welcome();
}
@MessageBundle("errors")
public interface ErrorMessages {
@Message("Error: {message}")
String error(String message);
@Message("Field '{field}' is required")
String required(String field);
@Message("Invalid {field}: {reason}")
String invalid(String field, String reason);
}
@MessageBundle("validation")
public interface ValidationMessages {
@Message("Email must be valid")
String emailInvalid();
@Message("Password must be at least {min} characters")
String passwordTooShort(int min);
}Template:
<h1>{app:welcome}</h1>
{#if errors}
<div class="errors">
{#for error in errors}
<p>{errors:error(error.message)}</p>
{/for}
</div>
{/if}
{#if validationErrors}
<ul>
<li>{validation:emailInvalid}</li>
<li>{validation:passwordTooShort(8)}</li>
</ul>
{/if}@MessageBundle(locale = "en")
public interface Messages {
@Message("Hello!")
String hello();
@Message("Welcome!")
String welcome();
@Message("Goodbye!")
String goodbye();
}
@Localized("cs")
public interface CzechMessages extends Messages {
@Override
@Message("Ahoj!")
String hello();
@Override
@Message("Vítejte!")
String welcome();
// goodbye() uses file-based value
}
@Localized("de")
public interface GermanMessages extends Messages {
@Override
@Message("Hallo!")
String hello();
// welcome() and goodbye() use file-based values
}Properties file messages/msg_cs.properties:
goodbye=Sbohem!Properties file messages/msg_de.properties:
welcome=Willkommen!
goodbye=Auf Wiedersehen!Usage:
@Inject Messages messages;
@Inject @Localized("cs") Messages czechMessages;
@Inject @Localized("de") Messages germanMessages;
messages.hello(); // Hello!
czechMessages.hello(); // Ahoj!
germanMessages.hello(); // Hallo!
messages.goodbye(); // Goodbye!
czechMessages.goodbye(); // Sbohem!
germanMessages.goodbye(); // Auf Wiedersehen!@MessageBundle
public interface NotificationMessages {
@Message("{#if count == 0}No new messages"
+ "{#else if count == 1}You have 1 new message"
+ "{#else}You have {count} new messages"
+ "{/if}")
String messageCount(int count);
@Message("{#when priority}"
+ "{#is HIGH}<span class=\"high\">{text}</span>"
+ "{#is MEDIUM}<span class=\"medium\">{text}</span>"
+ "{#is LOW}<span class=\"low\">{text}</span>"
+ "{/when}")
String priorityMessage(Priority priority, String text);
@Message("Posted {#if days == 0}today"
+ "{#else if days == 1}yesterday"
+ "{#else}{days} days ago"
+ "{/if}")
String timeAgo(int days);
}Template:
<div class="notifications">
<h2>{msg:messageCount(notifications.size)}</h2>
{#for notification in notifications}
<div class="notification">
{msg:priorityMessage(notification.priority, notification.text)}
<small>{msg:timeAgo(notification.daysAgo)}</small>
</div>
{/for}
</div>When a translation is missing, Qute's behavior depends on configuration:
// If @Message has value(), it's always used (highest priority)
@Message("Default English text")
String greeting();
// If @Message has no value(), file must provide it
@Message
String farewell(); // Build fails if not in properties file
// If @Message has defaultValue(), it's used as fallback
@Message(defaultValue = "Fallback text")
String optional(); // Uses defaultValue if not in file# Escape special characters in properties files
message.with.equals=Value with \\= sign
message.with.colon=Value with \\: colon
message.with.newline=Line 1\\nLine 2
message.with.unicode=Unicode: \\u00A9 copyright
# Multi-line messages
long.message=This is a very long message \\
that continues on the next line \\
and can span multiple lines.@MessageBundle
interface Messages {
@Message("Hello!")
String hello();
@Message("Goodbye!")
String goodbye();
}
// Select message dynamically
public String getGreeting(boolean isArriving) {
Messages msg = MessageBundles.get(Messages.class);
return isArriving ? msg.hello() : msg.goodbye();
}
// Or use message() method with dynamic key
template.data("messageKey", isArriving ? "hello" : "goodbye");
// Template: {msg:message(messageKey)}// Request locale: de-AT (German/Austria)
// Available bundles: en (default), de (German)
// Resolution: Uses "de" bundle (language match)
Messages msg = MessageBundles.get(Messages.class,
Localized.Literal.of("de-AT"));
// Falls back to "de" since exact "de-AT" not available
// Request locale: fr (French)
// Available bundles: en (default), de (German)
// Resolution: Uses default "en" bundle (no match)
Messages frMsg = MessageBundles.get(Messages.class,
Localized.Literal.of("fr"));
// Falls back to default since no French bundle@TemplateEnum
enum Status {
IN_PROGRESS, // Contains underscore
READY_TO_SHIP, // Multiple underscores
COMPLETED
}
@MessageBundle
interface Messages {
@Message
String status(Status status);
}Properties file (note the _$ separator):
# Use _$ separator when enum has underscores
status_$IN_PROGRESS=In Progress
status_$READY_TO_SHIP=Ready to Ship
status_$COMPLETED=Completed// Base bundle
@MessageBundle
interface BaseMessages {
@Message("Common message")
String common();
@Message("Base greeting")
String greeting();
}
// Extended bundle
@MessageBundle("extended")
interface ExtendedMessages extends BaseMessages {
@Override
@Message("Extended greeting") // Override
String greeting();
// Inherits common() from base
@Message("Extra message")
String extra(); // New message
}@Inject
Messages messages; // Cached by CDI
public String greet(String name) {
return messages.hello_name(name); // Fast
}// Bad: Complex logic in message
@Message("{#if count > 100}Many{#else if count > 10}Some{#else}Few{/if}")
String complexity(int count);
// Good: Logic in Java, simple message
public String getCountMessage(int count) {
if (count > 100) return messages.many();
if (count > 10) return messages.some();
return messages.few();
}// Bad: Creates new bundle each time
public String greet(Locale locale) {
Messages msg = MessageBundles.get(Messages.class,
Localized.Literal.of(locale.toLanguageTag()));
return msg.hello();
}
// Good: Inject localized bundles
@Inject Messages defaultMsg;
@Inject @Localized("de") Messages germanMsg;
@Inject @Localized("fr") Messages frenchMsg;
public String greet(Locale locale) {
return switch(locale.getLanguage()) {
case "de" -> germanMsg.hello();
case "fr" -> frenchMsg.hello();
default -> defaultMsg.hello();
};
}