JavaFX: Change application language on the run
Asked Answered
L

3

14

I am making JavaFX desktop application with core components described in FXML and I would like to offer user the option to change the language. However I have not find any direct way how to change the language once the component has been loaded from the FXML.

The question is is there any standard way how to deal with switching the language in JavaFX.

Livvi answered 8/9, 2015 at 18:39 Comment(0)
I
22

You can do something like this. As in your answer, you would either want to implement this as a singleton, or use a DI framework to inject a single instance wherever you need it:

public class ObservableResourceFactory {

    private ObjectProperty<ResourceBundle> resources = new SimpleObjectProperty<>();
    public ObjectProperty<ResourceBundle> resourcesProperty() {
        return resources ;
    }
    public final ResourceBundle getResources() {
        return resourcesProperty().get();
    }
    public final void setResources(ResourceBundle resources) {
        resourcesProperty().set(resources);
    }

    public StringBinding getStringBinding(String key) {
        return new StringBinding() {
            { bind(resourcesProperty()); }
            @Override
            public String computeValue() {
                return getResources().getString(key);
            }
        };
    }
}

Now you can do things like:

ObservableResourceFactory resourceFactory = .... ;

resourceBundle.setResources(...);

Label greetingLabel = new Label();
greetingLabel.textProperty().bind(resourceFactory.getStringBinding("greeting"));

And any time you update the resource with

resourceFactory.setResources(...);

will cause the label to update its text.

Here's an SSCCE (with apologies for the extremely ugly way of forcing a ResourceBundle into a single runnable class...)

import java.util.ListResourceBundle;
import java.util.Locale;
import java.util.ResourceBundle;

import javafx.application.Application;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class ResourceBundleBindingExample extends Application {

    private static final String RESOURCE_NAME = Resources.class.getTypeName() ;

    private static final ObservableResourceFactory RESOURCE_FACTORY = new ObservableResourceFactory();

    static {
        RESOURCE_FACTORY.setResources(ResourceBundle.getBundle(RESOURCE_NAME));
    }

    @Override
    public void start(Stage primaryStage) {
        ComboBox<Locale> languageSelect = new ComboBox<>();
        languageSelect.getItems().addAll(Locale.ENGLISH, Locale.FRENCH);
        languageSelect.setValue(Locale.ENGLISH);
        languageSelect.setCellFactory(lv -> new LocaleCell());
        languageSelect.setButtonCell(new LocaleCell());

        languageSelect.valueProperty().addListener((obs, oldValue, newValue) -> {
            if (newValue != null) {
                RESOURCE_FACTORY.setResources(ResourceBundle.getBundle(RESOURCE_NAME, newValue));
            }
        });

        Label label = new Label();
        label.textProperty().bind(RESOURCE_FACTORY.getStringBinding("greeting"));

        BorderPane root = new BorderPane(null, languageSelect, null, label, null);
        root.setPadding(new Insets(10));
        Scene scene = new Scene(root, 400, 400);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static class LocaleCell extends ListCell<Locale> {
        @Override
        public void updateItem(Locale locale, boolean empty) {
            super.updateItem(locale, empty);
            if (empty) {
                setText(null);
            } else {
                setText(locale.getDisplayLanguage(locale));
            }
        }
    }

    public static class ObservableResourceFactory {

        private ObjectProperty<ResourceBundle> resources = new SimpleObjectProperty<>();
        public ObjectProperty<ResourceBundle> resourcesProperty() {
            return resources ;
        }
        public final ResourceBundle getResources() {
            return resourcesProperty().get();
        }
        public final void setResources(ResourceBundle resources) {
            resourcesProperty().set(resources);
        }

        public StringBinding getStringBinding(String key) {
            return new StringBinding() {
                { bind(resourcesProperty()); }
                @Override
                public String computeValue() {
                    return getResources().getString(key);
                }
            };
        }

    }

    public static class Resources extends ListResourceBundle {

        @Override
        protected Object[][] getContents() {
            return new Object[][] {
                    {"greeting", "Hello"}
            };
        }

    }

    public static class Resources_fr extends ListResourceBundle {

        @Override
        protected Object[][] getContents() {
            return new Object[][] {
                    {"greeting", "Bonjour"}
            };
        }

    }

    public static void main(String[] args) {
        launch(args);
    }
}
Industrious answered 8/9, 2015 at 23:37 Comment(2)
Works perfectly. Makes the use of internationalizations strings in FXML a bit useless thou but I consider it as very very small price (all labels and strings can be easily set in initialization().Livvi
just beware: this might introduce memory leaks (particularly if used in cells). While the listener in the binding is weak - sounds safe, doesn't it - the cleanup only happens if the observed property actually changes. See bugs.openjdk.java.net/browse/JDK-8095375 which is closed as wontfix.Chimney
I
3

Like @Chiggiddi, i also like the internationalized string approach. Binding each label on the java side is too tedious. So i came up with a mixed approach with binding and expression binding on fxml side. I share this solution here today because i didn't find any like this on all stackoverflow's question i've visited. I hope it will be of some help to someone.

First create an observable map of map populated from ResourceBundle's keys like below:

public class LocaleManager extends SimpleMapProperty<String, Object> {

    private String bundleName = "language"; // a file language.properties must be present at the root of your classpath
    
    public LocaleManager() {
        super(FXCollections.observableHashMap());
        reload();
    }

    public void changeLocale(Locale newLocale) {
        Locale.setDefault(newLocale);
        reload();
    }

    private void reload() {
        ResourceBundle bundle = ResourceBundle.getBundle(bundleName);
        Enumeration<String> keys = bundle.getKeys();
        while (keys.hasMoreElements()) {
            String key = keys.nextElement();
            String value = bundle.getString(key);
            
            String[] parts = key.split("\\.");
            
            MapProperty<String, Object> map = this;
            
            for (int i=0;i < parts.length; i++) {
                String part = parts[i];
                if (i == parts.length - 1) {
                    map.put(part, value);
                } else {
                    if (!map.containsKey(part)) {
                        map.put(part, new SimpleMapProperty<>(FXCollections.observableHashMap()));
                    }
                    map = (MapProperty<String, Object>)map.get(part);
                }
            }
        }
    }
    
    public StringBinding bind(String key) {
        String[] parts = key.split("\\.");
        
        MapProperty<String, Object> map = this;
        
        for (int i=0;i < parts.length; i++) {
            String part = parts[i];
            if (i == parts.length - 1) {
                return Bindings.valueAt(map, part).asString();
            } else {
                if (!map.containsKey(part)) {
                    map.put(part, new SimpleMapProperty<>(FXCollections.observableHashMap()));
                }
                map = (MapProperty<String, Object>)map.get(part);
            }
        }
        throw new NullPointerException("Unknown key : " + key);
    }
}

Now you need to create a base class for your views exposing the LocaleManager as a property with getter and setter included:

public class BaseView {

    private LocaleManager lang;
    
    public BaseView() {
        lang = new LocaleManager();
    }
    
    public LocaleManager langProperty() {
        return lang;
    }

    public ObservableMap<String, Object> getLang() {
        return lang.get();
    }

    public void setLang(MapProperty<String, Object> resource) {
        this.lang.set(resource);
    }

}

Now, if your view extends BaseView

public MyView extends BaseView {}

Any expression in your fxml like ${controller.lang.my.resource.key} will be binded to the same key in your ResourceBundle

Binding on java side can still be done using:

someField.textProperty().bind(langProperty().bind(BUNDLE_KEY));

Now to change the language on the fly just use:

langProperty().changeLocale(newLocale);

Remember to turn LocaleManager as a singleton in your app if you want to change the language for all your app.

On SceneBuilder side binding expression for string fields aren't supported yet. But the following pull request may help if accepted in the future:

Pull request

EDIT:

To answer @Novy comment, the binding key contains 2 parts. The first part "controller.lang" give you access to the lang property of the controller. It is the same as writing this.getLang() in your controller class

So if in your property file you have the following properties:

item1 = "somestring"
item1.item2 = "someotherstring"
item1.item2.item3 = "someotherotherstring"

then

${controller.lang.item1} == "somestring"
${controller.lang.item1.item2} == "someotherstring"
${controller.lang.item1.item2.item3} == "someotherotherstring"

${controller.lang.item1.item2} can be translated to the following java code in your controller class

((Map<String, Object)this.getLang().get("item1")).get("item2").toString()

or with the implicit casting provided by javafx

this.getLang().get("item1").get("item2")
Inunction answered 20/7, 2020 at 9:16 Comment(4)
I really your solution but I have a problem trying to implement it. Can you give an example of your properties file content. may be mind has a problem in it.Tolmach
Hmm I got it @Inunction all the resources key must have 3 parts like you wrote it "my.resource.key". But still got something wrong. My label value does not show up with text= ${controller.lang.my.label.name} in my fxml.Tolmach
Hello @Novy, please take a look at my edited answer. I hope it will be clearerInunction
Thanks man. I need to retry and debug with my properties because I could not use properties like item1 = "somestring" . But I finally got the job done. Really nice approach. Need more up vote!Tolmach
L
0

I am currently using singleton (in later stage might be injected via DI framework) as a wrapper for language ResourceBundle.

My plan is to implement observable pattern and notify all components which requires to change (subcomponents injected with @FXML statement).

Livvi answered 8/9, 2015 at 18:39 Comment(2)
If you just use an ObjectProperty<ResourceBundle>, you don't need to implement the observable pattern yourself.Industrious
@Industrious Hi James, I am referring back to the change-language-on-the-fly topic back from 2015. Since I really like the internationalized strings provided by FXML and I would like to continue using them. But FXML file needs to be reloaded whenever a change of ResourceBundle is required (new loading of FXML is quite slow); therefore I ask you whether you have found a new method to change-language-on-the-fly while still using internationalized strings provided by FXML and not needing to reload FXML when language is switched. Cheers, Mate!Prolactin

© 2022 - 2024 — McMap. All rights reserved.