How to implement a JavaFX UI where the language can be changed dynamically

When you are trying to make a multilingual JavaFX application, you have the possibility to give a ResourceBundle to the FXMLLoader when creating the UI from an FXML file. But this fixes the language of the UI when creating it. When you want to change it later, you have to replace your UI with a new loaded version where a different ResourceBundle was passed on loading.

In this post I show how to create a UI programmatically, where the language of the UI can be changed dynamically during runtime without rebuilding the UI.

The sample application consists of two classes, I18N and I18NApplication and of two language resources. The used language resource files for english and german are:

message_en.properties:

label.numSwitches=Number of language switches: {0}
window.title=Dynamic language change
button.english=English
button.german=German
button.english.tooltip=changes the language to english
button.german.tooltip=changes the language to german

and message_de.properties:

label.numSwitches=Anzahl Sprachwechsel: {0}
window.title=Dynamischer Sprachwechsel
button.english=Englisch
button.german=Deutsch
button.english.tooltip=ändert die Sprache in Englisch
button.german.tooltip=ändert die Sprache in Deutsch

The I18N class

This class is implemented as a utitlity class consisting static methods, I could have implemented some singleton pattern as well. I will show the different methods and implementation details one by one and finally show the complete code for the class:

class properties

/**
 * I18N utility class..
 *
 * @author P.J. Meisch (pj.meisch@sothawo.com).
 */
public final class I18N {

    /** the current selected Locale. */
    private static final ObjectProperty<Locale> locale;

    static {
        locale = new SimpleObjectProperty<>(getDefaultLocale());
        locale.addListener((observable, oldValue, newValue) -> Locale.setDefault(newValue));
    }
}

The class has one static field which is a JavaFX ObjectProperty that is wrapping a Java Locale object. We will bind to this property later. It is initialized with the default locale and every time it is changed, the Java JVM’s default locale is set as well.

helper methods to initialize

/**
 * get the supported Locales.
 *
 * @return List of Locale objects.
 */
public static List<Locale> getSupportedLocales() {
    return new ArrayList<>(Arrays.asList(Locale.ENGLISH, Locale.GERMAN));
}

/**
 * get the default locale. This is the systems default if contained in the supported locales, english otherwise.
 *
 * @return
 */
public static Locale getDefaultLocale() {
    Locale sysDefault = Locale.getDefault();
    return getSupportedLocales().contains(sysDefault) ? sysDefault : Locale.ENGLISH;
}

the getSupportedLocale method returns a list of Locales that are supported by the program; getDefaultLocale reads the system default and checks if it is in the supported locales, if yes, it is returned, otherwise the english locale is used.

JavaFX property methods

public static Locale getLocale() {
    return locale.get();
}

public static void setLocale(Locale locale) {
    localeProperty().set(locale);
    Locale.setDefault(locale);
}

public static ObjectProperty<Locale> localeProperty() {
    return locale;
}

These methods are just the standard property accessors for a JavaFX property.

get a formatted localized String

/**
 * gets the string with the given key from the resource bundle for the current locale and uses it as first argument
 * to MessageFormat.format, passing in the optional args and returning the result.
 *
 * @param key
 *         message key
 * @param args
 *         optional arguments for the message
 * @return localized formatted string
 */
public static String get(final String key, final Object... args) {
    ResourceBundle bundle = ResourceBundle.getBundle("messages", getLocale());
    return MessageFormat.format(bundle.getString(key), args);
}

This method is used to retrieve a message from the ResourceBundle associated with the currently selected locale and to format it with the MessageFormat.format method as it may contain parameters.

String-Binding methods

/**
 * creates a String binding to a localized String for the given message bundle key
 *
 * @param key
 *         key
 * @return String binding
 */
public static StringBinding createStringBinding(final String key, Object... args) {
    return Bindings.createStringBinding(() -> get(key, args), locale);
}

/**
 * creates a String Binding to a localized String that is computed by calling the given func
 *
 * @param func
 *         function called on every change
 * @return StringBinding
 */
public static StringBinding createStringBinding(Callable<String> func) {
    return Bindings.createStringBinding(func, locale);
}

These methods are the core of the implementation. The first one creates a StringBinding that will change whenever the locale property is changed and will change the string bindings value to the value when get is called with the message key and the arguments passed in. Note that the arguments are fixed when the StringBinding is created as the lambda function is created at that time.

For the case that there are some values to evaluate for the String creation there is the second method which accepts a Callable which is executed whenever the locale property is changed and the String binding must be recalculated.

Create language aware UI Components

/**
 * creates a bound Label whose value is computed on language change.
 *
 * @param func
 *         the function to compute the value
 * @return Label
 */
public static Label labelForValue(Callable<String> func) {
    Label label = new Label();
    label.textProperty().bind(createStringBinding(func));
    return label;
}

/**
 * creates a bound Button for the given resourcebundle key
 *
 * @param key
 *         ResourceBundle key
 * @param args
 *         optional arguments for the message
 * @return Button
 */
public static Button buttonForKey(final String key, final Object... args) {
    Button button = new Button();
    button.textProperty().bind(createStringBinding(key, args));
    return button;
}

/**
 * creates a bound Tooltip for the given resourcebundle key
 *
 * @param key
 *         ResourceBundle key
 * @param args
 *         optional arguments for the message
 * @return Label
 */
public static Tooltip tooltipForKey(final String key, final Object... args) {
    Tooltip tooltip = new Tooltip();
    tooltip.textProperty().bind(createStringBinding(key, args));
    return tooltip;
}

This methods will be used the create UI components. For a Label I chose to use the callable version, and for Button and Tooltip objects the ones with key and args parameters. And that is all that is contained in the I18N class.

The I18N Application class

The following show the code to start the application, the UI just has two buttons with tooltips and a label showing how often the language was switched:

/**
 * Copyright (c) 2016 sothawo
 *
 * http://www.sothawo.com
 */

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

import java.util.Locale;

/**
 * Sample application showing dynamic language switching,
 *
 * @author P.J. Meisch (pj.meisch@sothawo.com).
 */
public class I18nApplication extends Application {

    /** number of language switches. */
    private Integer numSwitches = 0;

    @Override
    public void start(Stage primaryStage) throws Exception {

        primaryStage.titleProperty().bind(I18N.createStringBinding("window.title"));

        // create content
        BorderPane content = new BorderPane();

        // at the top two buttons
        HBox hbox = new HBox();
        hbox.setPadding(new Insets(5, 5, 5, 5));
        hbox.setSpacing(5);

        Button buttonEnglish = I18N.buttonForKey("button.english");
        buttonEnglish.setTooltip(I18N.tooltipForKey("button.english.tooltip"));
        buttonEnglish.setOnAction((evt) -> switchLanguage(Locale.ENGLISH));
        hbox.getChildren().add(buttonEnglish);

        Button buttonGerman = I18N.buttonForKey("button.german");
        buttonGerman.setTooltip(I18N.tooltipForKey("button.german.tooltip"));
        buttonGerman.setOnAction((evt) -> switchLanguage(Locale.GERMAN));
        hbox.getChildren().add(buttonGerman);

        content.setTop(hbox);

        // a label to display the number of changes, recalculating the text on every change
        final Label label = I18N.labelForValue(() -> I18N.get("label.numSwitches", numSwitches));
        content.setBottom(label);

        primaryStage.setScene(new Scene(content, 400, 200));
        primaryStage.show();
    }

    /**
     * sets the given Locale in the I18N class and keeps count of the number of switches.
     *
     * @param locale
     *         the new local to set
     */
    private void switchLanguage(Locale locale) {
        numSwitches++;
        I18N.setLocale(locale);
    }
}

The buttons and tooltips are initialized with key values, here I use no arguments. For the Label I choose the setup with the Callable so the the value for the label can be calculated dynamically. On button clicks, the locale property of the I18N class is set which triggers the update of the UI.

Here is a video showing the dynamic change and the complete code for the I1N class:
Your browser does not support the video tag.

/**
 * Copyright (c) 2016 sothawo
 *
 * http://www.sothawo.com
 */

import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.concurrent.Callable;

/**
 * I18N utility class..
 *
 * @author P.J. Meisch (pj.meisch@sothawo.com).
 */
public final class I18N {

    /** the current selected Locale. */
    private static final ObjectProperty<Locale> locale;

    static {
        locale = new SimpleObjectProperty<>(getDefaultLocale());
        locale.addListener((observable, oldValue, newValue) -> Locale.setDefault(newValue));
    }

    /**
     * get the supported Locales.
     *
     * @return List of Locale objects.
     */
    public static List<Locale> getSupportedLocales() {
        return new ArrayList<>(Arrays.asList(Locale.ENGLISH, Locale.GERMAN));
    }

    /**
     * get the default locale. This is the systems default if contained in the supported locales, english otherwise.
     *
     * @return
     */
    public static Locale getDefaultLocale() {
        Locale sysDefault = Locale.getDefault();
        return getSupportedLocales().contains(sysDefault) ? sysDefault : Locale.ENGLISH;
    }

    public static Locale getLocale() {
        return locale.get();
    }

    public static void setLocale(Locale locale) {
        localeProperty().set(locale);
        Locale.setDefault(locale);
    }

    public static ObjectProperty<Locale> localeProperty() {
        return locale;
    }

    /**
     * gets the string with the given key from the resource bundle for the current locale and uses it as first argument
     * to MessageFormat.format, passing in the optional args and returning the result.
     *
     * @param key
     *         message key
     * @param args
     *         optional arguments for the message
     * @return localized formatted string
     */
    public static String get(final String key, final Object... args) {
        ResourceBundle bundle = ResourceBundle.getBundle("messages", getLocale());
        return MessageFormat.format(bundle.getString(key), args);
    }

    /**
     * creates a String binding to a localized String for the given message bundle key
     *
     * @param key
     *         key
     * @return String binding
     */
    public static StringBinding createStringBinding(final String key, Object... args) {
        return Bindings.createStringBinding(() -> get(key, args), locale);
    }

    /**
     * creates a String Binding to a localized String that is computed by calling the given func
     *
     * @param func
     *         function called on every change
     * @return StringBinding
     */
    public static StringBinding createStringBinding(Callable<String> func) {
        return Bindings.createStringBinding(func, locale);
    }

    /**
     * creates a bound Label whose value is computed on language change.
     *
     * @param func
     *         the function to compute the value
     * @return Label
     */
    public static Label labelForValue(Callable<String> func) {
        Label label = new Label();
        label.textProperty().bind(createStringBinding(func));
        return label;
    }

    /**
     * creates a bound Button for the given resourcebundle key
     *
     * @param key
     *         ResourceBundle key
     * @param args
     *         optional arguments for the message
     * @return Button
     */
    public static Button buttonForKey(final String key, final Object... args) {
        Button button = new Button();
        button.textProperty().bind(createStringBinding(key, args));
        return button;
    }

    /**
     * creates a bound Tooltip for the given resourcebundle key
     *
     * @param key
     *         ResourceBundle key
     * @param args
     *         optional arguments for the message
     * @return Label
     */
    public static Tooltip tooltipForKey(final String key, final Object... args) {
        Tooltip tooltip = new Tooltip();
        tooltip.textProperty().bind(createStringBinding(key, args));
        return tooltip;
    }

}

Hope you enjoyed this artice, comments are welcome!