JavaFX:在运行时更改应用程序语言

14

我正在制作一个JavaFX桌面应用程序,其中核心组件描述在FXML中,并且我想为用户提供更改语言的选项。但是一旦从FXML加载组件后,我没有找到任何直接更改语言的方法。

问题是是否有标准的JavaFX切换语言的方式。

3个回答

20
你可以这样做。与你的答案类似,你要么实现一个单例,要么使用 DI 框架在需要时注入单个实例:
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);
            }
        };
    }
}

现在您可以执行以下操作:

ObservableResourceFactory resourceFactory = .... ;

resourceBundle.setResources(...);

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

每当您更新资源时,

resourceFactory.setResources(...);

该操作将使标签更新其文本内容。

以下是一个SSCCE(尽管通过极其丑陋的方式将ResourceBundle强制转换为单个可运行类...)

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);
    }
}

完美运作。尽管这使得在FXML中使用国际化字符串有点无用,但我认为这只是一个非常小的代价(所有标签和字符串都可以在初始化()中轻松设置)。 - Wolfer
请注意:这可能会引入内存泄漏(特别是如果在单元格中使用)。虽然绑定中的监听器是弱引用 - 听起来很安全,不是吗 - 但仅当观察到的属性实际更改时才会进行清理。请参见 https://bugs.openjdk.java.net/browse/JDK-8095375,该问题已关闭并不予修复。 - kleopatra

3

像@Chiggiddi一样,我也喜欢国际化字符串的方法。在Java端绑定每个标签太繁琐了。因此,我提出了一种混合方法,在FXML端使用绑定和表达式绑定。我今天在这里分享这个解决方案,因为我在所有我访问过的stackoverflow问题中都没有找到类似的东西。希望它能对某人有所帮助。

首先创建一个可观察的Map of Map,从ResourceBundle的键填充如下:

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);
    }
}

现在,您需要为视图创建一个基类,将LocaleManager作为属性暴露出来,并包含getter和setter函数:
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);
    }

}

现在,如果您的视图扩展了BaseView类

public MyView extends BaseView {}

任何在您的FXML中类似${controller.lang.my.resource.key}的表达式都将绑定到您的ResourceBundle中相同的键。
Java端仍然可以使用绑定:
someField.textProperty().bind(langProperty().bind(BUNDLE_KEY));

现在要实时更改语言,只需使用以下命令:
langProperty().changeLocale(newLocale);

如果您想更改应用程序的语言,记得将LocaleManager设置为单例模式。

在SceneBuilder方面,尚不支持绑定字符串字段的表达式。但是,如果以下拉取请求在未来被接受,则可能会有所帮助:

拉取请求

编辑:

回答@Novy的评论,绑定键包含2个部分。第一部分“controller.lang”允许您访问控制器的lang属性。这与在控制器类中编写this.getLang()相同。

因此,如果在您的属性文件中有以下属性:

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

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

${controller.lang.item1.item2}

可以翻译成在您的控制器类中使用以下Java代码
((Map<String, Object)this.getLang().get("item1")).get("item2").toString()

或者使用JavaFX提供的隐式类型转换

this.getLang().get("item1").get("item2")

我真的很喜欢你的解决方案,但是我在尝试实现它时遇到了问题。你能给出一个属性文件内容的例子吗?也许我的有问题。 - Novy
嗯,我明白了@Mumrah81,所有的资源键都必须像你写的那样有3个部分,比如“my.resource.key”。但是仍然有些问题。我的标签值在我的fxml中不显示文本= ${controller.lang.my.label.name}。 - Novy
你好@Novy,请看一下我编辑过的答案。希望它更清晰明了。 - Mumrah81
谢谢,我需要重试并调试我的属性,因为我无法像item1 =“somestring”那样使用属性。但是我最终完成了工作。真的很好的方法。需要更多的赞! - Novy

0

我目前正在使用单例(稍后可能通过 DI 框架注入)作为语言 ResourceBundle 的包装器。

我的计划是实现可观察模式并通知所有需要更改的组件(使用 @FXML 语句注入的子组件)。


1
如果您只是使用ObjectProperty<ResourceBundle>,则无需自己实现可观察模式。 - James_D
1
@James_D 你好James,我在回顾2015年的“动态更改语言”主题。由于我非常喜欢FXML提供的国际化字符串,并且希望继续使用它们。但是每当需要更改ResourceBundle时,FXML文件都需要重新加载(重新加载FXML相当缓慢);因此,我想问问你是否找到了一种新的方法,在仍然使用FXML提供的国际化字符串而不需要重新加载FXML的情况下动态更改语言。干杯,伙计! - Chiggiddi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接