Localizing JavaFx Controls

Posted on

Localizing JavaFx ControlsError is an illegal operation performed by the user which results in the abnormal working of the program. By now, you’ve probably seen a few errors, either when compiling or running your code like Localizing JavaFx Controls. It can be frustrating, but they can also give you a lot of information about exactly how you can fix the problems in your code about java and localization. In this post covers the types of errors you’ll see when programming in Java, and how to fix them. Don’t pay any attention to the number of errors. Just read the first error message and work on fixing that error.

Problem :

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 “comsunjavafxscenecontrolskinresourcescontrols”.

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?

Solution :

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…

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

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
}

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.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *