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")