Skip to main content

Fluent API

The library provides a fluent API to load and format messages that is built on top of Java standard libraries. In addition to simplifying the overall setup, it brings strong typing and compile time checks to the mix when used with the annotation processor.

Use L10nMessages.builder() to create instances. Ideally, use the annotation processor to make the initialization simpler using the generated enum. Customize "failure handling" to add logging or to change default behaviors. Consider using caching when performance is sensitive. The builder can also be fully configured manually if the annotation processor is not used.

L10nMessages instance provides only one function format() with different overloads for ease of use. format() takes care of loading the localized message from the resource bundle for the given locale. When required, it formats the message using the arguments passed to the function and the base arguments.

Create an L10nMessages instance

With the annotation processor

Create the L10nMessages instance from the generated enum using the builder(). It will automatically load the resource bundle baseName and the type of MessageFormatAdapterProvider that were defined in the @L10nProperties annotation.

The first argument of format() function will now be one of the enum values. This provides the strong typing of the key and ensures that messages are present at build time.


@L10nProperties(baseName = "com.pinterest.l10nmessages.example.Messages")
class Example {

{
L10nMessages<Messages> m = L10nMessages.builder(Messages.class).build();
System.out.println(m.format(welcome));
// Welcome!
}
}

Without the annotation processor

Without the enum generated by the annotation processor, the resource bundle baseName must be provided manually with resourceBundleName(). The L10nMessages instance is manually typed. String is the most common type and is equivalent to the default Java support with a plain resource bundle.

class Example {

{
L10nMessages<String> m =
L10nMessages.builder(String.class)
.resourceBundleName("com.pinterest.l10nmessages.example.Messages").build();
System.out.println(m.format("welcome"));
}
}

But the type can be any type that implements toString(), for example a user defined enum:

class Example {

enum MyEnum {
welcome,
welcome_user
}

{
L10nMessages<MyEnum> m =
L10nMessages.builder(MyEnum.class)
.resourceBundleName("com.pinterest.l10nmessages.example.Messages").build();
System.out.println(m.format(MyEnum.welcome));
}
}

Specify a locale

The builder is created with the system locale: Locale.getDefault() as default locale. The locale can then be overridden using the locale(). Changing the system locale after the builder has been created has no impact on the creation of L10nMessages instance (since it was set when the builder was created).

note

It is not possible to change the locale of the L10nMessasges instance later, so it should be done on on the builder.

To specify the locale:

class Example {

{
L10nMessages<Messages> m = L10nMessages.builder(Messages.class)
.locale(Locale.forLanguageTag("fr-FR")).build();

System.out.println(m.format(welcome));
// Bienvenue!
}
}

Formatting

Formatting capabilities depends on which MessageFormat implementation is used, see more details here. This section covers the basic usages of format().

format()

The first argument of format() is the key of the message to load and optionally format. When using the enum, the key is typed accordingly.

formatUntyped can be used to look up the message using an untyped String as the key.

L10nMessages always use named arguments (even when working with plain JDK and with messages that have numbered arguments). All arguments are provided as map entries. The format() function has overloads to pass map entries or a full map.

src/resources/java/com/pinterest/l10nmessages/example/Messages.properties
welcome_user=Welcome {username}!
welcome_user_numbered=Welcome {0}!
class Example {

{
L10nMessages<Messages> m = L10nMessages.builder(Messages.class).build();

// format with map entries
System.out.println(m.format(welcome_user, "username", "Bob"));

// format with a map of arguments
System.out.println(m.format(welcome_user, ImmutableMap.of("username", "Bob")));

// Numbered arguments are referenced by name as any other arguments
System.out.println(m.format(welcome_user_numbered, "0", "Bob"));

// It is possible to load a message with an untyped key
System.out.println(m.formatUntyped("welcome_user", ImmutableMap.of("username", "Bob")));
}
}

Check ICU4J for advance formatting.

Argument names typing

Use enumType = WITH_ARGUMENT_BUILDERS to generate additional "argument builders" / FormatContext in the enum. These provide strong typing on the argument names yet keeping a compact syntax.

Use the function Message.welcome_user() to obtain the builder and then call the argument setter username("Mary").


@L10nProperties(
baseName = "com.pinterest.l10nmessages.example.Messages",
enumType = EnumType.WITH_ARGUMENT_BUILDERS)
public class Example {

{
// Format using an "argument builder" / FormatContext
m.format(welcome_user().username("Mary"));
}
}
note

The enum value: Message.welcome_user references the key, while the method: Message.welcome_user() creates an argument builder / FormatContext.

Both style of format() functions can be used at the same time for a same message.

No builder will be generated for messages with no arguments. Only the key in the enum will be generated.

Base arguments

If multiple messages have the same arguments, having to provide them through the format() every single time can be cumbersome. Instead, "base" arguments can be defined on the L10nMessages instance. Arguments provided to the format() override the base arguments.

class Example {

{
L10nMessages<Messages> m = L10nMessages.builder(Messages.class)
.baseArguments(ImmutableMap.of("username", "Mary")).build();

System.out.println(m.format(welcome_user));
// Welcome Mary!

System.out.println(m.format(bye_user));
// Bye Mary...

System.out.println(m.format(bye_user, "username", "Bob"));
// Bye Bob...
}
}

Failure handling

In plain Java, following cases will throw an exception and, most likely cause a hard failure at the application level:

  • trying to get a resource bundle with no matching properties file
  • looking up for a message using a missing key in a resource bundle
  • trying to create a MessageFormat with an invalid message
  • formatting a message with invalid arguments

L10nMessages will instead, by default, gracefully fail by showing the broken message or the missing key. Use OnFormatErrors.RETHROW to propagate exceptions if wanted.

On the other hand, in plain Java, missing arguments when formatting a message are not causing an exception, and the rendered string just contains the original placeholder untouched.

L10nMessages keeps that behavior as default to be consistent with other formatting issues. It is possible to raise an exception instead using OnMissingArguments.THROW if hard failure are wanted.

Note that when using the annotation processor's default configuration with properties backed resource bundle, the invalid messages will be identified at compile time. Missing keys should be avoided by using the enum.

Customize formatting error handling

The behavior can be customized by providing a customized OnFormatError implementation. This example adds logging to the default behavior that fallbacks to the key or the message.

class Example {

{
L10nMessages<Messages> m = L10nMessages.builder(Messages.class)
.messageFormatAdapterProvider(JDK)
.onFormatError(
(throwable, baseName, key, message) -> {
logger.severe(
String.format(
"Can't format message for baseName: `%1$s`, key: `%2$s` and message: `%3$s`",
baseName,
key,
message));
return OnFormatErrors.MESSAGE_FALLBACK.apply(throwable, baseName, key, message);
}).build();

String format = m.formatUntyped("missing_key", ImmutableMap.of());
// SEVERE: Can't format message for baseName: `com.pinterest.l10nmessages.example.Messages`, key: `missing_key` and message: `null`
System.out.println(format);
// missing_key

String invalidMessage = m.format(welcome, ImmutableMap.of("username", "Mary"));
// SEVERE: Can't format message for baseName: `com.pinterest.l10nmessages.example.Messages`, key: `welcome` and message: `Welcome {username}!`
System.out.println(invalidMessage);
// Welcome {username}!
}
}

Customize missing arguments handling

The behavior can be customized by providing a customized onMissingArgument implementation. This example adds logging to the default behavior. It is possible to replace the argument by returning an Optional with the replacement value.

class Example {

{
L10nMessages<Messages> m =
L10nMessages.builder(Messages.class)
.onMissingArgument((baseName, key, argumentName) -> {
logger.severe(
String.format("Argument: `%3$s` missing for baseName: `%1$s` and key: `%2$s`",
baseName, key, argumentName));
return OnMissingArguments.NOOP.apply(baseName, key, argumentName);
}).build();
System.out.println(m.format(connected_to, "username", "Mary"));
// SEVERE: Argument: `username2` missing for baseName: `com.pinterest.l10nmessages.example.Messages` and key: `connected_to`
// Mary is connected to {username2}
}
}

ICU4J, JDK or JDK with named arguments

By default, the fluent API (as the annotation processor) will use ICU4J if it is available. If not, it will use the JDK extended with named arguments support.

When initialized from the enum, L10nMessages will use the same messageFormatAdapterProvider that was used by the annotation processor.

To provide a custom implementation:

class Example {

{
L10nMessages<Messages> m =
L10nMessages.builder(Messages.class)
.messageFormatAdapterProvider(
(message, locale) -> {
logger.info("Using named argument with JDK");
return JDK_NAMED_ARGS.get(message, locale);
}).build();

System.out.println(m.format(welcome_user, "username", "Mary"));
// INFO: Using named argument with JDK...
// Welcome Mary!
}
}

Named vs. Numbered arguments

Plain JDK MessageFormat only support numbered arguments.

src/resources/java/com/pinterest/l10nmessages/example/Messages.properties
welcome_user=Welcome {0}!

ICU4J on ther other hand supports both numbered arguments and named arguments

src/resources/java/com/pinterest/l10nmessages/example/MessagesNamed.properties
welcome_user=Welcome {username}!

Named arguments with JDK MessageFormat

L10nMessages provides a lightweight extension to support named arguments on top JDK MessageFormats.

If you can't or don't wish to integrate with ICU4J because of its size or dependencies but still want named arguments, this is an interesting trade off.

By default, the library will use JDK with named argument if ICU4J is not used. To use plain JDK usually set the messageFormatAdapterProviders at the annotation level to get the proper message format validation.


@L10nProperties(
baseName = "com.pinterest.l10nmessages.example.Messages",
messageFormatAdapterProviders = MessageFormatAdapterProviders.JDK)
class Example {

{
L10nMessages<MessagesNamed> m = L10nMessages.builder(Messages.class).build();
System.out.println(m.format(MessagesNamed.welcome_user, "0", "Mary"));
}
}

Or just set it up on the builder

class Example {

{
L10nMessages<String> m = L10nMessages.<String>builder()
.resourceBundleName("com.pinterest.l10nmessages.example.Messages")
.messageFormatAdapterProvider(MessageFormatAdapterProviders.JDK)
.build();
System.out.println(m.format("welcome_user", "0", "Mary"));
}
}
Also works as an independent formatter Just looking for message formatting with named

arguments in Java?

The formatter can be used directly and since the library is lightweight and has no dependencies that can be a good option.

class Example {

{
MessageFormatAdapter mf =
MessageFormatAdapterProviders.JDK_NAMED_ARGS.get("Welcome {username}", Locale.ROOT);
System.out.println(mf.format(ImmutableMap.of("username", "Mary")));
}
}