The library Lib-I18N
allows a developer to bind a key-value pair of a .properties
file to a StringBinding. This makes it very easy to change the language during
runtime in a JavaFX application.
Lib-I18N is written in JavaFX, Maven and NetBeans.
Image: Demo integration from Lib-I18N
The demo shows how easy an application becomes multilingual in four steps 😄 .
Plz see the section Demo
for more informations.
- Features
- Conventions
- Examples
- Demo
- JavaDoc
- Download
- Requirements
- Installation
- Contribution
- License
- Autor
- Contact
In this sub-section all main features from the library 'Lib-I18N' are listed:
- The library
Lib-I18N
allows a developer to bind a key with its associated value of a.properties
file to a StringBinding. - With the builder I18NResourceBundleBuilder the developer can configure the
ResourceBundle which contains the
key - value
pairs which will then be bind to aactual
Locale. - The builder I18NBindingBuilder let the developer create a StringBinding. The
StringBinding can created with a function from type Callable<String> or
with a .properties
key
and optionalarguments
. - To load a .properties
key
with optionalarguments
from the initialized ResourceBundle through the I18NResourceBundleBuilder the developer can use the builder I18NMessageBuilder.
This sub-section list all general features from the library 'Lib-I18N':
- The library
Lib-I18N
isopen source
and licensed under General Public License 3.0. - The library is written in
Java
and JavaFX. - The library is programmed with the IDE NetBeans as a Maven library.
- The library can easily integrated into a foreign project over Maven Central.
- Due to the connection of the project with Travis CI, a build is automatically executed at each commit.
- By integrating various "badges" from "img.shield.io", interested readers can easily find out about the "build" state, the current version and the license used for this library.
- All functionalities from the classes in the
core
andinternal
packages are tested withUnittests
. - Every
parameter
in all functionalities will be verified against minimal conditions with the internal validator DefaultI18NValidator. For example aString
can't beNULL
orEMPTY
. - The documentation from the library is available with an extended
ReadMe
and well-described JavaDoc comments.
In this chapter, the interested developer can find out all about the conventions
from the library Lib-I18N
:
If a ResourceBundle with the defined 'baseBundleName' can't be found a spezific MissingResourceException will be thrown.
public final class DefaultI18NResourceBundle implements I18NResourceBundle {
...
private ResourceBundle getResourceBundle() {
ResourceBundle bundle = null;
try {
bundle = ResourceBundle.getBundle(this.getBaseBundleName(), this.getActualLocale());
} catch (MissingResourceException mre) {
LoggerFacade.getDefault().error(this.getClass(),
String.format("Can't access the ResourceBundle[path=%s, actual-locale=%s]",
this.getBaseBundleName(), this.getActualLocale().getDisplayLanguage()),
mre);
}
return bundle;
}
...
}
If a key
can't be found in the defined ResourceBundle then
- the String pattern '<key>' will returned and
- the following 'warning' message will be logged:
"Can't find key(%s) in resourcebundle. Return: %s"
2019-04-19 22:36:34,708 WARN Can't find key(not.existing.key.in.existing.resourcebundle) in resourcebundle. Return: <not.existing.key.in.existing.resourcebundle> [DefaultI18NResourceBundle]
java.util.MissingResourceException: Can't find resource for bundle java.util.PropertyResourceBundle, key not.existing.key.in.existing.resourcebundle
at java.util.ResourceBundle.getObject(ResourceBundle.java:450) ~[?:1.8.0_201]
at java.util.ResourceBundle.getString(ResourceBundle.java:407) ~[?:1.8.0_201]
at com.github.naoghuman.lib.i18n.internal.DefaultI18NResourceBundle.getMessage(DefaultI18NResourceBundle.java:96) [classes/:?]
at com.github.naoghuman.lib.i18n.internal.DefaultI18NResourceBundleTest.getMessageWithResourceBundleThrowsMissingResourceException(DefaultI18NResourceBundleTest.java:110) [test-classes/:?]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_201]
...
public final class DefaultI18NResourceBundle implements I18NResourceBundle {
private static final String PATTERN_KEY_NAME = "<{0}>"; // NO18N
...
@Override
public String getMessage(final String key, final Object... arguments) {
DefaultI18NValidator.requireNonNullAndNotEmpty(key);
DefaultI18NValidator.requireNonNullAndNotEmpty(arguments);
DefaultI18NValidator.requireResourceBundleExist(this.getBaseBundleName(), this.getActualLocale());
final ResourceBundle bundle = getResourceBundle();
String value = MessageFormat.format(PATTERN_KEY_NAME, key);
if (bundle != null) {
try {
value = MessageFormat.format(bundle.getString(key), arguments);
} catch (MissingResourceException mre) {
LoggerFacade.getDefault().warn(this.getClass(),
String.format("Can't find key(%s) in resourcebundle. Return: %s", key, value),
mre);
}
}
return value;
}
...
}
Supported Locales
- Defines all supported Locales in the momentary session.
- This array should reflectes all your defined languages .properties files.
public final class I18NResourceBundleBuilder {
...
private static final class I18NResourceBundleBuilderImpl implements
FirstStep, ForthStep, LastStep, SecondStep, ThirdStep
{
...
@Override
public ThirdStep supportedLocales(final Locale... locales) {
LoggerFacade.getDefault().debug(this.getClass(), "I18NResourceBundleBuilderImpl.supportedLocales(Locale...)"); // NOI18N
final List<Locale> list = Arrays.asList(locales);
DefaultI18NValidator.requireNonNullAndNotEmpty(list);
final ObservableList<Locale> observableList = FXCollections.observableArrayList();
observableList.addAll(list);
properties.put(ATTR__SUPPORTED_LOCALES, new SimpleObjectProperty(observableList));
return this;
}
@Override
public ThirdStep supportedLocales(final ObservableList<Locale> locales) {
LoggerFacade.getDefault().debug(this.getClass(), "I18NResourceBundleBuilderImpl.supportedLocales(ObservableList<Locale>)"); // NOI18N
DefaultI18NValidator.requireNonNullAndNotEmpty(locales);
properties.put(ATTR__SUPPORTED_LOCALES, new SimpleObjectProperty(locales));
return this;
}
...
}
}
public final class I18NFacade implements I18NBinding, I18NResourceBundle {
...
@Override
public ObservableList<Locale> getSupportedLocales() {
return i18NResourceBundle.getSupportedLocales();
}
@Override
public void setSupportedLocales(final ObservableList<Locale> locales) {
i18NResourceBundle.setSupportedLocales(locales);
}
@Override
public void setSupportedLocales(final Locale... locales) {
i18NResourceBundle.setSupportedLocales(locales);
}
...
}
public final class DefaultI18NResourceBundle implements I18NResourceBundle {
...
@Override
public ObservableList<Locale> getSupportedLocales() {
DefaultI18NValidator.requireNonNull(supportedLocales);
return supportedLocales;
}
@Override
public void setSupportedLocales(final ObservableList<Locale> locales) {
DefaultI18NValidator.requireNonNullAndNotEmpty(locales);
supportedLocales.clear();
supportedLocales.addAll(locales);
}
@Override
public void setSupportedLocales(final Locale... locales) {
final List<Locale> list = Arrays.asList(locales);
DefaultI18NValidator.requireNonNullAndNotEmpty(list);
supportedLocales.clear();
supportedLocales.addAll(list);
}
...
}
Default Locale
- If the supported Locales doesn't contained the default Locale then the
Locale#ENGLISH
instead will be return.
public final class DefaultI18NResourceBundle implements I18NResourceBundle {
...
@Override
public void setDefaultLocale(final Locale locale) {
DefaultI18NValidator.requireNonNull(locale);
defaultLocale = this.getSupportedLocales().contains(locale) ? locale : Locale.ENGLISH;
}
...
}
Actual Locale
- If the supported Locales doesn't contained the actual Locale then the
default
Locale instead will be return.
public final class DefaultI18NResourceBundle implements I18NResourceBundle {
...
@Override
public void setActualLocale(final Locale locale) {
DefaultI18NValidator.requireNonNull(locale);
actualLocaleProperty.set(this.getSupportedLocales().contains(locale) ? locale : defaultLocale);
}
...
}
- Every
functionality
in the builders and in the default implementations will checked against minimal preconditions with the validator DefaultI18NValidator. Getters
andSetters
functionalities are only checked if they are initial only instantiate and not declarated.- For example a String will be validate if it's not NULL and not EMPTY.
public final class I18NResourceBundleBuilder {
...
private static final class I18NResourceBundleBuilderImpl implements
FirstStep, ForthStep, LastStep, SecondStep, ThirdStep
{
...
@Override
public SecondStep baseBundleName(final String baseBundleName) {
LoggerFacade.getDefault().debug(this.getClass(), "I18NResourceBundleBuilderImpl.baseBundleName(String)"); // NOI18N
DefaultI18NValidator.requireNonNullAndNotEmpty(baseBundleName);
properties.put(ATTR__BASE_BUNDLE_NAME, new SimpleStringProperty(baseBundleName));
return this;
}
...
}
}
public final class DefaultI18NResourceBundle implements I18NResourceBundle {
...
@Override
public String getBaseBundleName() {
DefaultI18NValidator.requireNonNullAndNotEmpty(baseBundleName);
return baseBundleName;
}
@Override
public void setBaseBundleName(final String baseBundleName) {
DefaultI18NValidator.requireNonNullAndNotEmpty(baseBundleName);
this.baseBundleName = baseBundleName;
}
...
}
public final class DefaultI18NBinding implements I18NBinding {
...
@Override
public StringBinding createStringBinding(final String key, Object... arguments) {
DefaultI18NValidator.requireNonNullAndNotEmpty(key);
DefaultI18NValidator.requireNonNullAndNotEmpty(arguments);
return Bindings.createStringBinding(() -> I18NFacade.getDefault().getMessage(key, arguments), I18NFacade.getDefault().actualLocaleProperty());
}
...
}
With the builder I18NResourceBundleBuilder the developer can configure the
ResourceBundle which contains the key - value
terms which will then be bind
to a Locale. That means switching the actual
Locale update all binded textes
with the specific values from the corresponding language .properties
file.
/**
* 1) Starts the configuration process.
* 2) Defines the path and name from the .properties file.
* 3) Sets all supported Locales with an [].
* 4) Sets all supported Locales with an ObservableList.
* 5) Sets the default Locale.
* 6) Sets the actual Locale.
* 7) Completes the configuration process.
*/
I18NResourceBundleBuilder.configure() // 1
.baseBundleName(String) // 2
.supportedLocales(Locale...) // 3
.supportedLocales(ObservableList<Locale>) // 4
.defaultLocale(Locale) // 5
.actualLocale(Locale) // 6
.build(); // 7
@Test
public void lastStepWithSupportedLocalesAsArray() {
String resourcbundle = "com.github.naoghuman.lib.i18n.internal.resourcebundle";
I18NResourceBundleBuilder.configure()
.baseBundleName(resourcbundle)
.supportedLocales(Locale.ITALIAN, Locale.JAPANESE)
.defaultLocale(Locale.ITALIAN)
.actualLocale(Locale.JAPANESE)
.build();
assertEquals(resourcbundle, I18NFacade.getDefault().getBaseBundleName());
assertEquals(Locale.ITALIAN, I18NFacade.getDefault().getDefaultLocale());
assertEquals(Locale.JAPANESE, I18NFacade.getDefault().getActualLocale());
assertEquals(2, I18NFacade.getDefault().getSupportedLocales().size());
}
@Test
public void lastStepWithSupportedLocalesAsObservableList() {
String resourcbundle = "com.github.naoghuman.lib.i18n.internal.resourcebundle";
final ObservableList<Locale> locales = FXCollections.observableArrayList();
locales.addAll(Locale.ITALIAN, Locale.JAPANESE, Locale.FRENCH);
I18NResourceBundleBuilder.configure()
.baseBundleName(resourcbundle)
.supportedLocales(locales)
.defaultLocale(Locale.ITALIAN)
.actualLocale(Locale.JAPANESE)
.build();
assertEquals(resourcbundle, I18NFacade.getDefault().getBaseBundleName());
assertEquals(Locale.ITALIAN, I18NFacade.getDefault().getDefaultLocale());
assertEquals(Locale.JAPANESE,I18NFacade.getDefault().getActualLocale());
assertEquals(3, I18NFacade.getDefault().getSupportedLocales().size());
}
The builder I18NBindingBuilder let the developer create a StringBinding. The
StringBinding can created with a function from type Callable<String> or
with a .properties key
and optional arguments
.
/**
* 1) Starts the binding process.
* 2) Use the given function to create a StringBinding.
* 3) Completes the binding process and returns the StringBinding.
*/
I18NBindingBuilder.bind() // 1
.callable(Callable<String>) // 2
.build(); // 3
/**
* 1) Starts the binding process.
* 2) Defines the key which value will be bind to the StringBinding.
* 3) Optional arguments for the value from the given key.
* 4) Completes the binding process and returns the StringBinding.
*/
I18NBindingBuilder.bind() // 1
.key(String) // 2
.arguments(Object... args) // 3
.build(); // 4
@Test
public void lastStepCallable() {
I18NResourceBundleBuilder.configure()
.baseBundleName("com.github.naoghuman.lib.i18n.internal.resourcebundle")
.supportedLocales(Locale.ENGLISH, Locale.GERMAN)
.defaultLocale(Locale.ENGLISH)
.actualLocale(Locale.GERMAN)
.build();
Optional<StringBinding> result = I18NBindingBuilder.bind()
.callable(() -> I18NMessageBuilder.message()
.key("resourcebundle.title")
.build()
)
.build();
assertTrue(result.isPresent());
assertEquals("RB: Test Titel", result.get().get());
I18NFacade.getDefault().setActualLocale(Locale.ENGLISH); // Here the magic happen :)
assertEquals("RB: Test title", result.get().get());
}
@Test
public void lastStepKeyWithoutArguments() {
I18NResourceBundleBuilder.configure()
.baseBundleName("com.github.naoghuman.lib.i18n.internal.resourcebundle")
.supportedLocales(Locale.ENGLISH, Locale.GERMAN)
.defaultLocale(Locale.ENGLISH)
.actualLocale(Locale.GERMAN)
.build();
Optional<StringBinding> result = I18NBindingBuilder.bind()
.key("resourcebundle.title")
.build();
assertTrue(result.isPresent());
assertEquals("RB: Test Titel", result.get().get());
I18NFacade.getDefault().setActualLocale(Locale.ENGLISH); // Here the magic happen :)
assertEquals("RB: Test title", result.get().get());
}
@Test
public void lastStepKeyWithArguments() {
I18NResourceBundleBuilder.configure()
.baseBundleName("com.github.naoghuman.lib.i18n.internal.resourcebundle")
.supportedLocales(Locale.ENGLISH, Locale.GERMAN)
.defaultLocale(Locale.ENGLISH)
.actualLocale(Locale.GERMAN)
.build();
Optional<StringBinding> result = I18NBindingBuilder.bind()
.key("resourcebundle.label.with.parameter")
.arguments(123)
.build();
assertTrue(result.isPresent());
assertEquals("RB: Text mit Parameter: 123", result.get().get());
I18NFacade.getDefault().setActualLocale(Locale.ENGLISH); // Here the magic happen :)
assertEquals("RB: Text with parameter: 123", result.get().get());
}
To load a .properties key
with optional arguments
from the initialized ResourceBundle
through the I18NResourceBundleBuilder the developer can use the builder I18NMessageBuilder.
/**
* 1) Starts the message process.
* 2) Defines the key which value will be loaded.
* 3) Optional arguments for the value from the given key.
* 4) Completes the message process and returns a String.
*/
I18NMessageBuilder.message() // 1
.key(String) // 2
.arguments(Object...) // 3
.build(); // 4
@Test
public void lastStepWithoutArguments() {
I18NResourceBundleBuilder.configure()
.baseBundleName("com.github.naoghuman.lib.i18n.internal.resourcebundle")
.supportedLocales(Locale.ENGLISH, Locale.GERMAN)
.defaultLocale(Locale.ENGLISH)
.actualLocale(Locale.GERMAN)
.build();
String result = I18NMessageBuilder.message()
.key("resourcebundle.title")
.build();
assertEquals("RB: Test Titel", result);
I18NFacade.getDefault().setActualLocale(Locale.ENGLISH); // Here the magic happen :)
result = I18NMessageBuilder.message()
.key("resourcebundle.title")
.build();
assertEquals("RB: Test title", result);
}
@Test
public void lastStepWithArguments() {
I18NResourceBundleBuilder.configure()
.baseBundleName("com.github.naoghuman.lib.i18n.internal.resourcebundle")
.supportedLocales(Locale.ENGLISH, Locale.GERMAN)
.defaultLocale(Locale.ENGLISH)
.actualLocale(Locale.GERMAN)
.build();
String result = I18NMessageBuilder.message()
.key("resourcebundle.label.with.parameter")
.arguments(2)
.build();
assertEquals("RB: Text mit Parameter: 2", result);
I18NFacade.getDefault().setActualLocale(Locale.ENGLISH); // Here the magic happen :)
result = I18NMessageBuilder.message()
.key("resourcebundle.label.with.parameter")
.arguments(123)
.build();
assertEquals("RB: Text with parameter: 123", result);
}
The demo applications shows how to integrate the library Lib-I18N
in four simple steps.
Hint:
To run the demo local the library Lib-FMXL v0.3.0-PRERELEASE
is needed which isn't
momentary available in Maven Central
. The library can be downloaded manually here:
https://github.com/Naoghuman/lib-fxml
First inject the library 'Lib-I18N' into your project.
Then create for every supported language a .properties file.
<dependencies>
<dependency>
<groupId>com.github.naoghuman</groupId>
<artifactId>lib-i18n</artifactId>
<version>0.8.0</version>
</dependency>
</dependencies>
Next register the ResourceBundle where supportedLocales
are corresponding to every existing .properties file and actualLocale
is the language which will shown first.
public final class DemoI18NStart extends Application {
@Override
public void init() throws Exception {
I18NResourceBundleBuilder.configure()
.baseBundleName("com.github.naoghuman.lib.i18n.demo_i18n") // NOI18N
.supportedLocales(Locale.ENGLISH, Locale.FRENCH, Locale.GERMAN, Locale.ITALIAN)
.defaultLocale(Locale.ENGLISH)
.actualLocale(Locale.ENGLISH)
.build();
}
}
In the third step the text components will be bind to the depending key from the ResourceBundle.
public final class DemoI18NController extends FXMLController implements Initializable {
@FXML private Button bFrench;
@FXML private Button bGerman;
@FXML private Button bItalian;
@FXML private Button bEnglish;
@FXML private Label lLanguages;
@FXML private Text tAbout;
@FXML private Text tFrom;
@FXML private Text tHello;
@FXML private Text tLand;
@Override
public void initialize(final URL location, final ResourceBundle resources) {
// Menu
this.bind(lLanguages.textProperty(), "demo.i18n.languages"); // NOI18N
this.bind(bFrench.textProperty(), "demo.i18n.language.french"); // NOI18N
this.bind(bGerman.textProperty(), "demo.i18n.language.german"); // NOI18N
this.bind(bItalian.textProperty(), "demo.i18n.language.italian"); // NOI18N
this.bind(bEnglish.textProperty(), "demo.i18n.language.english"); // NOI18N
// Message
this.bind(tHello.textProperty(), "demo.i18n.greetings"); // NOI18N
this.bind(tFrom.textProperty(), "demo.i18n.from"); // NOI18N
this.bind(tLand.textProperty(), "demo.i18n.land"); // NOI18N
this.bind(tAbout.textProperty(), "demo.i18n.about"); // NOI18N
}
private void bind(final StringProperty stringProperty, final String key) {
final Optional<StringBinding> optionalStringBinding = I18NBindingBuilder.bind().key(key).build();
optionalStringBinding.ifPresent(stringBinding -> {
stringProperty.bind(stringBinding);
});
}
}
And in the last step the user will change the language in the runing application which leads to a change from the actualLocale
which performs then the language update in the gui.
public final class DemoI18NController extends FXMLController implements Initializable {
public void onActionSwitchToLanguageFrench() {
if (I18NFacade.getDefault().getActualLocale().equals(Locale.FRENCH)) {
LoggerFacade.getDefault().debug(this.getClass(), "Shows already the Locale.FRENCH - do nothing."); // NOI18N
return;
}
I18NFacade.getDefault().setActualLocale(Locale.FRENCH);
}
public void onActionSwitchToLanguageGerman() {
if (I18NFacade.getDefault().getActualLocale().equals(Locale.GERMAN)) {
LoggerFacade.getDefault().debug(this.getClass(), "Shows already the Locale.GERMAN - do nothing."); // NOI18N
return;
}
I18NFacade.getDefault().setActualLocale(Locale.GERMAN);
}
...
}
The JavaDoc from the library Lib-I18N
can be explored here: JavaDoc Lib-I18N
Image: JavaDoc Lib-I18N v0.7.2
Current version
is 0.8.0
. Main points in this release are:
- Implement a demo which shows how easy an application becomes multilingual in four steps 😄 .
- Extend the ReadMe with a new chapter 'Demo'.
- Fix some minor bugs (test files are copied into the .jar file).
Maven coordinates
In context from a Maven project you can use following maven coordinates:
<dependencies>
<dependency>
<groupId>com.github.naoghuman</groupId>
<artifactId>lib-i18n</artifactId>
<version>0.8.0</version>
</dependency>
<!-- optional -->
<dependency>
<groupId>com.github.naoghuman</groupId>
<artifactId>lib-logger</artifactId>
<version>0.6.0</version>
</dependency>
</dependencies>
Download:
- Release v0.8.0 (04.28.2019 / MM.dd.yyyy)
An overview about all existings releases can be found here:
- Overview from all releases in
Lib-I18N
.
- On your system you need JRE 8 or JDK 8 installed.
- The library lib-i18n-0.8.0.jar.
In the library are following libraries registered as dependencies:
- The library lib-logger-0.6.0.jar.
- Included in
Lib-Logger
is the library log4j-api-2.10.0.jar. - Included is
Lib-Logger
is the library log4j-core-2.10.0.jar.
- Included in
- If not installed download the JRE 8 or the JDK 8.
- Optional: To work better with FXML files in a JavaFX application
download the JavaFX Scene Builder supported by
Gluon
.
- Optional: To work better with FXML files in a JavaFX application
download the JavaFX Scene Builder supported by
- Choose your preferred IDE (e.g. NetBeans, Eclipse or IntelliJ IDEA) for development.
- Download or clone Lib-Logger.
- Open the projects in your IDE and run them.
- If you find a
Bug
I will be glad if you could report an Issue. - If you want to contribute to the project plz fork the project and do a Pull Request.
The project Lib-I18n
is licensed under General Public License 3.0.
The project Lib-I18n
is maintained by me, Peter Rogge. See Contact.
You can reach me under [email protected].