A modern, fast java implementation of the amazing project fluent ☕
I started this project due to the need for a lightweight localization framework for a project called JDA-Commands. Existing solutions for the java ecosystem were either integrated into large web frameworks like Spring or Micronaut or massive like ICU.
So, after doing a bit of research I stumbled across project fluent and was amazed by its expressiveness and simplicity. And that's how this library was born.
Fluava is published on maven central.
gradle.build.kts
implementation("dev.goldmensch:fluava:VERSION")Important! I'm not going to explain how fluent works in general. This guide is primary about how this library implements the fluent functionality. Knowledge about the project's syntax and concepts are required.
To start using this library, you have to create an instance of the Fluava class by
calling Fluava#create(Locale) or Fluava#builder(Locale).
The Fluava class serves as the main entry point to the library, providing several
helpful methods to create and load resource and bundles.
Resources are in general a collection of fluent files belonging together based on the locale and basename.
The fluents file content are structured according to the Guide here.
They can be loaded from raw strings by calling Fluava#of(String, Locale) or from disk by calling
Fluava#of(Path, Locale). In that case the resulting Resource of course only consists of this one file.
Bundles are a collection of multiple resources with the same basename but different locales.
They can be loaded by calling Fluava#loadBundle(String) and the basename as the parameter.
The load process of Bundles are currently limited to the classpath and very similar to how java's ResourceBundles work but with a more flexible directory structure. The classpath will be searched lazily for a fluent file given a specific locale according to following order:
- BASE_LANGUAGE_COUNTRY_VARIANT.ftl
- BASE/LANGUAGE_COUNTRY_VARIANT.ftl
- BASE_LANGUAGE_COUNTRY.ftl
- BASE/LANGUAGE_COUNTRY.ftl
- BASE_LANGUAGE.ftl
- BASE/LANGUAGE.ftl
If a key isn't found for a specific locale in the whole bundle, then the fallback locale will be queried for this key.
Messages are the smallest entity of this system. They represent the direct localization message
associated with a key. The in code representation is the Message class and provide
methods for getting the messages content and attributes. That class also supports replacing
fluent placeables with their real world value.
You can also access a message's attributes. You can either call Message#interpolated(Map<String, Object>)
or directly refer to the attribute in the message key: Resource.message("my-message.attribute") or
Bundle.apply("my-message.attribute", Map.of()).
// create Fluava instance with englisch as the fallback locale
Fluava fluava = Fluava.create(Locale.ENGLISH);
// load a bundle with the basename "app"
Bundle app = fluent.loadBundle("app");
// just some user to demonstrate
User user = getUser();
// get the message with key "greet" for the users locale and the variable "name" set to the user's name.
String msg = app.apply(user.locale(), "greet", Map.of("name", user.name()));
// display the localized message to the user
user.display(msg);Project fluent introduces so-called "function" to format certain variable values. These functions can take multiple positional and named arguments and return either a number or a text.
Fluava introduces a subtype of functions called "implicit functions". They represent the
default formatters for certain objects (based on their class) and can be implemented
with the interface Function.Implicit.
If in a fluent messages a variables is referenced without an enclosing function. The implicit function for this variables will be searched and called.
Often, there are implicit functions that support classes that are modeling data very similar to those used in your application. Just think about a custom "Date" class you receive from the database. Now to include this "Date" class in an i18n message you normally would need to manually convert this to some built-in supported one or write a custom function for it. But for your luck, Fluava supports Proteus, which is a universal java type adapting library.
Fluava will try to adapt your value to some supported class, for which an implicit function
exists using the global proteus mapper Proteus#global().
That comes in handy if you have your own types that can be easily converted to some supported ones.
For the example above, you just have to register a proteus mapper that converts
your custom Data to for example java.time.LocalDate and fluava will just
use the default implicit datetime function with all it's options supported.
For many types this way is way more comfortable than implemented a dedicated function
each time.
Project fluent also introduces the term "partial function", which means that
the developer (caller of Message#apply) preset some named arguments (options)
with values. To do that in Fluava you have to wrap your variables value in a Partial
record:
User user = getUser();
String msg = app.apply(user.locale(), "current-time", Map.of("time",
new Partial(user.getTime(), Map.of("timeZone", "CET"))));It's important to note that such predefined options only apply to the variables implicit function. In this example this would be the builtin "datetime" function.
With the partial function in the above example, the named argument timeZone
is set to the value CET by default. This can be overridden if this exact
function is called in the fluent message with this option set there.