or run

tessl search
Log in

Version

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
mavenpkg:maven/io.quarkus/quarkus-qute@3.30.x

docs

checked-templates.mdconfiguration.mdengine-advanced.mdindex.mdmessage-bundles.mdtemplate-basics.mdtemplate-extensions.mdtemplate-syntax.mdutilities.mdvalue-resolvers.md
tile.json

tessl/maven-io-quarkus--quarkus-qute

tessl install tessl/maven-io-quarkus--quarkus-qute@3.30.0

Offer templating support for web, email, etc in a build time, type-safe way

message-bundles.mddocs/

Qute Message Bundles (i18n)

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.

Capabilities

@MessageBundle Annotation

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;
}

Name Resolution

The bundle name determines the namespace used in templates and the prefix for properties files:

  • Default name: Top-level interfaces default to "msg", accessed as {msg:methodName} with properties file msg_locale.properties
  • Nested classes: Name consists of all enclosing class simple names separated by underscores
  • Custom name: Explicitly set via value() 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.properties

Default Key Strategy

The 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"
}

Locale Specification

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 { }

@Message Annotation

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

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);
}

Template Usage

{msg:hello}
{msg:hello_name('Jachym')}
{msg:hello_fullname('John', 'Doe')}
{msg:itemDetail(myItem)}
{msg:files(100, 'C')}

Message Keys

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']}

Value Resolution Priority

Message values are resolved in the following order:

  1. @Message value() - Highest priority
  2. Localized file entry - Medium priority
  3. @Message defaultValue() - Lowest priority

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
}

@Localized Annotation

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();
    }
}

CDI Injection with Localized Bundles

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!
    }
}

Localized Bundle Interfaces

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
}

@MessageParam Annotation

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;
}

Parameter Binding

@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!

MessageBundles Class

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);
}

Programmatic Bundle Access

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.

Error Handling

// 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"));

Localized File Formats

Message bundles can be supplemented or defined entirely through properties files. Files must be placed in the messages/ directory on the classpath.

File Naming Convention

messages/<bundle-name>_<locale>.properties

Examples:

  • messages/msg_en.properties - English messages for "msg" bundle
  • messages/msg_de.properties - German messages for "msg" bundle
  • messages/msg_cs.properties - Czech messages for "msg" bundle
  • messages/app_fr.properties - French messages for "app" bundle
  • messages/Controller_index_es.properties - Spanish messages for nested bundle

Properties File Format

# 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!

Example: English Bundle

messages/msg_en.properties:

hello=Hello world!
hello_name=Hello {name}!
goodbye=Goodbye!
farewell=See you later!

Example: German Bundle

messages/msg_de.properties:

hello=Hallo Welt!
hello_name=Hallo {name}!
goodbye=Auf Wiedersehen!
farewell=Bis später!

Example: Czech Bundle

messages/msg_cs.properties:

hello=Ahoj světe!
hello_name=Ahoj {name}!
goodbye=Sbohem!
farewell=Nashledanou!

Merging with Annotations

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 file

Enum Localization

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

Automatic Enum Template Generation

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

Enum Key Naming

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=Undefiniert

Enum Constants with Underscores or Dollar Signs

If 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

More Complex Enum Key Examples

@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 value

Custom Enum Templates

You 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 File Override

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);
}

Default Locales and Locale Resolution

Default Locale Configuration

The default locale for message bundles is configured via the quarkus.default-locale property:

# application.properties
quarkus.default-locale=cs

Bundles 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 { }

Locale Resolution in Templates

Templates resolve message bundle locale in the following order:

  1. Explicit locale attribute - Set via MessageBundles.ATTRIBUTE_LOCALE or TemplateInstance.LOCALE
  2. Selected variant locale - Set via TemplateInstance.setLocale()
  3. Default bundle - Falls back to default locale bundle
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();

Locale Matching Strategy

When resolving localized bundles, the system tries:

  1. Exact match - Language tag exactly matches (e.g., "de-DE")
  2. Language match - Language portion matches (e.g., "de" matches "de-DE", "de-AT", etc.)
  3. Default bundle - Falls back to default locale bundle
@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!"

Locale from Request Context

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>

Integration with Templates

Message bundles are accessible in Qute templates via namespace syntax.

Template Namespace Syntax

{bundleName:methodName}
{bundleName:methodName(arg1)}
{bundleName:methodName(arg1, arg2)}

Basic Usage

@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>

Custom Bundle Names

@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>

Dynamic Message Keys

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!

Keys with Dots

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']}

Nested Message References

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. !}

Locale Control in Templates

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>

Using Qute.fmt() with Message Bundles

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();

CDI Integration

Message bundles are automatically registered as CDI beans and can be injected into application components.

Basic Injection

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);
    }
}

Injecting Localized Bundles

@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!
    }
}

Field vs Constructor Injection

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;
    }
}

Producer Methods

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;

Request-Scoped Locale Resolution

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);
    }
}

Template Injection with Message Bundles

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>

Complete Examples

Example 1: Basic Message Bundle

@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!
}

Example 2: Enum Localization

@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=Cancelled

Properties file messages/msg_de.properties:

orderStatus_PENDING=Ausstehend
orderStatus_PROCESSING=In Bearbeitung
orderStatus_SHIPPED=Versandt
orderStatus_DELIVERED=Geliefert
orderStatus_CANCELLED=Storniert

Template:

<div class="order">
    <p>Status: {msg:orderStatus(order.status)}</p>
    <p>{msg:orderInfo(order.id, order.status)}</p>
</div>

Example 3: Multiple Bundle Names

@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}

Example 4: Localized Interfaces with Inheritance

@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!

Example 5: Complex Template with Sections

@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>

Edge Cases and Best Practices

Missing Translations

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

Handling Special Characters in Messages

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

Dynamic Message Selection

@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)}

Fallback Locale Strategy

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

Enum with Complex Names

@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

Message Bundle Inheritance

// 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
}

Performance Tips

  1. Cache bundle references: Inject once, reuse many times
@Inject
Messages messages;  // Cached by CDI

public String greet(String name) {
    return messages.hello_name(name);  // Fast
}
  1. Avoid complex logic in message templates: Move to Java code
// 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();
}
  1. Use appropriate locale resolution: Don't create bundles repeatedly
// 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();
    };
}