Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaFX: Change application language on the run

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.

like image 918
Wolfer Avatar asked Sep 08 '15 18:09

Wolfer


1 Answers

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")
like image 110
Mumrah81 Avatar answered Oct 21 '22 03:10

Mumrah81