Localizing JavaFx Controls

后端 未结 4 1614
悲哀的现实
悲哀的现实 2020-12-10 19:11

I am trying add a ResourceBundle for my language for the JavaFx controls but am failing to do so.

I tried to add controls_fi_FI.properties in the classpath.. and hop

相关标签:
4条回答
  • 2020-12-10 19:20

    I reversed bundle loading and came up with a dirty solution in case putting a jar to jre/lib/ext is not an option. Default algorithm requires a bundle file to be ISO-8859-1 encoded and doesn't provide any means to specify encoding. My solution addresses encoding issue too.

    The following method loads resource with application classloader and puts resulting bundle to the bundle cache using extension classloader (which is used in runtime by javafx classes to lookup bundles).

    import com.sun.javafx.scene.control.skin.resources.ControlResources;
    import java.util.Locale;
    import java.util.PropertyResourceBundle;
    import java.util.ResourceBundle;
    // rest of the imports is ommitted
    
    private void putResourceBundleInCache(String baseName, Charset cs) throws ReflectiveOperationException, IOException {
        Locale currentLocale = Locale.getDefault();
        ResourceBundle.Control control = ResourceBundle.Control.getControl(ResourceBundle.Control.FORMAT_DEFAULT);
        String resourceName = control.toResourceName(control.toBundleName(baseName, currentLocale), "properties");
        ResourceBundle bundle;
        try (Reader reader = new InputStreamReader(getClass().getClassLoader().getResourceAsStream(resourceName), cs)) {
            bundle = new PropertyResourceBundle(reader);
        }
        Class<?> cacheKeyClass = Class.forName("java.util.ResourceBundle$CacheKey");
        Constructor<?> cacheKeyClassConstructor = cacheKeyClass.getDeclaredConstructor(String.class, Locale.class, ClassLoader.class);
        cacheKeyClassConstructor.setAccessible(true);
        Object cacheKey = cacheKeyClassConstructor.newInstance(baseName, currentLocale, ControlResources.class.getClassLoader());
        Method putBundleInCache = ResourceBundle.class.getDeclaredMethod("putBundleInCache", cacheKeyClass, ResourceBundle.class, ResourceBundle.Control.class);
        putBundleInCache.setAccessible(true);
        putBundleInCache.invoke(null, cacheKey, bundle, control);
    }
    

    I call it from start method like this:

    public void start(Stage primaryStage) throws Exception {
        putResourceBundleInCache("com/sun/javafx/scene/control/skin/resources/controls", StandardCharsets.UTF_8);
        // mian logic here
    }
    
    0 讨论(0)
  • 2020-12-10 19:27

    it's just that I am trying to localize the built-in control strings.

    JavaFX is an Open Source project hosted at: http://openjdk.java.net/projects/openjfx/

    I suggest to file an issue at: https://javafx-jira.kenai.com

    and optionally provide a patch.

    0 讨论(0)
  • 2020-12-10 19:40

    I can only get this to work via a pretty massive hack, that isn't even remotely portable. However, maybe it will give you enough to work from to create a viable solution. (This works under Java 8.)

    I created a package com.sun.javafx.scene.control.skin.resources, and created a controls_fi_FI.properties file in it with the single line

    ProgressIndicator.doneString=Valmis
    

    I created a test app with the following:

    import java.util.Locale;
    import java.util.ResourceBundle;
    
    import javafx.application.Application;
    import javafx.stage.Stage;
    import javafx.scene.Scene;
    import javafx.scene.control.ProgressIndicator;
    import javafx.scene.layout.BorderPane;
    
    
    public class Main extends Application {
        @Override
        public void start(Stage primaryStage) {
                System.out.println(ResourceBundle.getBundle("com/sun/javafx/scene/control/skin/resources/controls").getString("ProgressIndicator.doneString"));
    
                BorderPane root = new BorderPane();
                ProgressIndicator progressIndicator = new ProgressIndicator(1);
                root.setCenter(progressIndicator);
                Scene scene = new Scene(root,400,400);
                primaryStage.setScene(scene);
                primaryStage.show();
        }
    
        public static void main(String[] args) {
            Locale.setDefault(new Locale("fi", "FI"));
            launch(args);
        }
    }
    

    Running this app most ways doesn't work as desired: while the System.out.println(...) produces the correct result, the text displayed in the progress indicator is wrong.

    However, if I bundle com/sun/javafx/control/skin/resources/controls_fi_FI.properties in a jar file, and move that jar file to the jre/lib/ext subdirectory of my JDK installation (ie. the same location as jfxrt.jar), it runs as required (at least in my simple test; as I said, I make no claims for this to be any kind of robust solution).

    The issue appears to be that ControlsResources is being loaded by the extension class loader, and so is using the same class loader to load the resource bundle.

    With better knowledge of class loaders than I have, you might be able to mold this into a reasonable solution...

    0 讨论(0)
  • 2020-12-10 19:40

    I created Simple "Hello, world!" example showing a ready to roll JavaFX project using multiple-language support is for those who need some more exotic language (be-BY, ru-RU etc.)

    Here's how I solved the problem, it works for me

    Messages.java

    /**
     * The class with all messages of this application.
     */
    public abstract class Messages {
    
        private static ResourceBundle BUNDLE;
    
        private static final String FIELD_NAME = "lookup";
        private static final String BUNDLE_NAME = "messages/messages";
        private static final String CONTROLS_BUNDLE_NAME = "com/sun/javafx/scene/control/skin/resources/controls";
    
        public static final String MAIN_APP_TITLE;
    
        public static final String DIALOG_HEADER;
        public static final String MAIN_CONTROLLER_CONTENT_TEXT;
        public static final String MAIN_CONTROLLER_HELLO_TEXT;
        public static final String MAIN_CONTROLLER_GOODBYE_TEXT;
    
        static {
            final Locale locale = Locale.getDefault();
            final ClassLoader classLoader = ControlResources.class.getClassLoader();
    
            final ResourceBundle controlBundle = getBundle(CONTROLS_BUNDLE_NAME,
                    locale, classLoader, PropertyLoader.getInstance());
    
            final ResourceBundle overrideBundle = getBundle(CONTROLS_BUNDLE_NAME,
                    PropertyLoader.getInstance());
    
            final Map override = getUnsafeFieldValue(overrideBundle, FIELD_NAME);
            final Map original = getUnsafeFieldValue(controlBundle, FIELD_NAME);
    
            //noinspection ConstantConditions,ConstantConditions,unchecked
            original.putAll(override);
    
            BUNDLE = getBundle(BUNDLE_NAME, PropertyLoader.getInstance());
    
            MAIN_APP_TITLE = BUNDLE.getString("MainApp.title");
    
            DIALOG_HEADER = BUNDLE.getString("Dialog.information.header");
            MAIN_CONTROLLER_CONTENT_TEXT = BUNDLE.getString("MainController.contentText");
            MAIN_CONTROLLER_HELLO_TEXT = BUNDLE.getString("MainController.helloText");
            MAIN_CONTROLLER_GOODBYE_TEXT = BUNDLE.getString("MainController.goodbyeText");
        }
    
        public static ResourceBundle GetBundle() {
            return BUNDLE;
        }
    }
    

    and in PropertyLoader.java

    public class PropertyLoader extends ResourceBundle.Control {
    
        private static final String PROPERTIES_RESOURCE_NAME = "properties";
    
        private static final PropertyLoader INSTANCE = new PropertyLoader();
    
        public static PropertyLoader getInstance() {
            return INSTANCE;
        }
    
        @Override
        public ResourceBundle newBundle(final String baseName, final Locale locale, final String format,
                                        final ClassLoader loader, final boolean reload)
                throws IllegalAccessException, InstantiationException, IOException {
    
            final String bundleName = toBundleName(baseName, locale);
            final String resourceName = toResourceName(bundleName, PROPERTIES_RESOURCE_NAME);
    
            ResourceBundle bundle = null;
            InputStream stream = null;
    
            if (reload) {
    
                final URL url = loader.getResource(resourceName);
    
                if (url != null) {
                    final URLConnection connection = url.openConnection();
                    if (connection != null) {
                        connection.setUseCaches(false);
                        stream = connection.getInputStream();
                    }
                }
    
            } else {
                stream = loader.getResourceAsStream(resourceName);
            }
    
            if (stream != null) {
                try {
                    bundle = new PropertyResourceBundle(new InputStreamReader(stream, StandardCharsets.UTF_8));
                } finally {
                    stream.close();
                }
            }
    
            return bundle;
        }
    }
    

    More I described here or on GitHub

    Here is a demonstration in the Belarusian language:

    0 讨论(0)
提交回复
热议问题