Localizing JavaFx Controls
Asked Answered
B

5

6

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 hoped it would work, but no.

So then I looked at JavaFx's ControlResources class, which is responsible for localizing the controls, and I noticed that the basename of the bundle is "com\sun\javafx\scene\control\skin\resources\controls".

How can I add my properties-file to this bundle? I find it strange that there is practically no information anywhere on how to do this?

Bowsprit answered 11/9, 2014 at 15:26 Comment(2)
Using a ResourceBundle defined by a properties file in the classpath should work. Can you show how you are retrieving values from your bundle?Helainehelali
I think you misunderstood my problem. I've got my own localization working already with ResourceBundle, it's just that I am trying to localize the built-in control strings. For example, ProgressIndicator has this "done"-string, which is displayed when the control is at 100%. I simply copied the controls.properties file from the jre and translated it myself, and added it to classpath.Bowsprit
H
4

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...

Helainehelali answered 11/9, 2014 at 17:51 Comment(1)
I actually tried to create this package in my project folder, and put the bundle in it, and observed that it didn't work. However, I really didn't think of putting it into the ext-folder. This is a fine hack for me, as I am actually bundling a JRE with my app, so I have no problems adding stuff to ext. However, I'll still refrain (for now) from accepting your solution, as I too, think, that there might be a better solution. Thank you!Bowsprit
H
2

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: enter image description here

Hemorrhoidectomy answered 31/8, 2017 at 11:19 Comment(0)
L
2

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
}
Lifeanddeath answered 13/2, 2018 at 18:30 Comment(1)
The method requires heavy reflection abuse so breaks encapsulation principles. Newer Java versions force them on modules level, so certain VM options are required to workaround it, making it twice as hacky solution... but it works. My take on this (there are also --add-opens and --add-exports flag in comment): gist.github.com/AgainPsychoX/df6ed47d0de7438bb5c553f0b5272ad9Boutonniere
P
0

if you distribute javafx.controls.jar with your app then you can change it there. Unzip it, create properties file compatible with your locale (in com.sun.javafx.scene.control.skin.resources folter) and then zip it back and rename to jar.

Platinotype answered 17/3, 2021 at 18:32 Comment(0)
P
-2

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.

Prevenient answered 12/9, 2014 at 10:36 Comment(1)
possibly OP should determine if it's actually broken first. i.e. is there a way to accomplish this that OP is not aware of. hence the post.Underestimate

© 2022 - 2024 — McMap. All rights reserved.