Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
650 views
in Technique[技术] by (71.8m points)

java - Javafx Internationalization with custom language

I'm developing a JavaFX application with multiple language support. My app sometimes shows an alert box, for example:

package application;
    
import java.util.Locale;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.layout.BorderPane;


public class Main extends Application {
    @Override
    public void start(Stage primaryStage) {
        try {
            Button btn = new Button("Show alert");
            btn.setOnAction(this::handleButton);
            
            BorderPane root = new BorderPane();
            root.setCenter(btn);
            Scene scene = new Scene(root,200, 200);
            primaryStage.setScene(scene);
            primaryStage.show();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
    
    void handleButton(ActionEvent e){
        Alert alert = new Alert(AlertType.CONFIRMATION);
        alert.showAndWait();
    }
    
    static Locale getLocaleSettingFromConfigurationFile(){
        return Locale.FRENCH;
        //return new Locale("vi");
    }
    
    public static void main(String[] args) {
        Locale appLocale = getLocaleSettingFromConfigurationFile();
        Locale.setDefault(appLocale);
        
        launch(args);
    }
}

The language setting is obtained via getLocaleSettingFromConfigurationFile() method
In the code above, I used Locale.FRENCH as app language and everything works file:
enter image description here
Two confirm buttons have been translated to French.

Now I want my app to support Vietnamese as well (uncomment return new Locale("vi") from the code above). After digging into details, I found that:

->Two confirm button "Ok", "Cancel" are constructed from:

package javafx.scene.control;

import com.sun.javafx.scene.control.skin.resources.ControlResources;

import javafx.beans.NamedArg;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBar.ButtonData;

/**
 * The ButtonType class is used as part of the JavaFX {@link Dialog} API (more
 * specifically, the {@link DialogPane} API) to specify which buttons should be
 * shown to users in the dialogs. Refer to the {@link DialogPane} class javadoc
 * for more information on how to use this class.
 *
 * @see Alert
 * @see Dialog
 * @see DialogPane
 * @since JavaFX 8u40
 */
public final class ButtonType {

    /**
     * A pre-defined {@link ButtonType} that displays "Apply" and has a
     * {@link ButtonData} of {@link ButtonData#APPLY}.
     */
    public static final ButtonType APPLY = new ButtonType(
            "Dialog.apply.button", null, ButtonData.APPLY);

    /**
     * A pre-defined {@link ButtonType} that displays "OK" and has a
     * {@link ButtonData} of {@link ButtonData#OK_DONE}.
     */
    public static final ButtonType OK = new ButtonType(
            "Dialog.ok.button", null, ButtonData.OK_DONE);

    /**
     * A pre-defined {@link ButtonType} that displays "Cancel" and has a
     * {@link ButtonData} of {@link ButtonData#CANCEL_CLOSE}.
     */
    public static final ButtonType CANCEL = new ButtonType(
            "Dialog.cancel.button", null, ButtonData.CANCEL_CLOSE);

    /**
     * A pre-defined {@link ButtonType} that displays "Close" and has a
     * {@link ButtonData} of {@link ButtonData#CANCEL_CLOSE}.
     */
    public static final ButtonType CLOSE = new ButtonType(
            "Dialog.close.button", null, ButtonData.CANCEL_CLOSE);

    /**
     * A pre-defined {@link ButtonType} that displays "Yes" and has a
     * {@link ButtonData} of {@link ButtonData#YES}.
     */
    public static final ButtonType YES = new ButtonType(
            "Dialog.yes.button", null, ButtonData.YES);

    /**
     * A pre-defined {@link ButtonType} that displays "No" and has a
     * {@link ButtonData} of {@link ButtonData#NO}.
     */
    public static final ButtonType NO = new ButtonType(
            "Dialog.no.button", null, ButtonData.NO);

    /**
     * A pre-defined {@link ButtonType} that displays "Finish" and has a
     * {@link ButtonData} of {@link ButtonData#FINISH}.
     */
    public static final ButtonType FINISH = new ButtonType(
            "Dialog.finish.button", null, ButtonData.FINISH);

    /**
     * A pre-defined {@link ButtonType} that displays "Next" and has a
     * {@link ButtonData} of {@link ButtonData#NEXT_FORWARD}.
     */
    public static final ButtonType NEXT = new ButtonType(
            "Dialog.next.button", null, ButtonData.NEXT_FORWARD);

    /**
     * A pre-defined {@link ButtonType} that displays "Previous" and has a
     * {@link ButtonData} of {@link ButtonData#BACK_PREVIOUS}.
     */
    public static final ButtonType PREVIOUS = new ButtonType(
            "Dialog.previous.button", null, ButtonData.BACK_PREVIOUS);

    private final String key;
    private final String text;
    private final ButtonData buttonData;


    /**
     * Creates a ButtonType instance with the given text, and the ButtonData set
     * as {@link ButtonData#OTHER}.
     *
     * @param text The string to display in the text property of controls such
     *      as {@link Button#textProperty() Button}.
     */
    public ButtonType(@NamedArg("text") String text) {
        this(text, ButtonData.OTHER);
    }

    /**
     * Creates a ButtonType instance with the given text, and the ButtonData set
     * as specified.
     *
     * @param text The string to display in the text property of controls such
     *      as {@link Button#textProperty() Button}.
     * @param buttonData The type of button that should be created from this ButtonType.
     */
    public ButtonType(@NamedArg("text") String text,
                        @NamedArg("buttonData") ButtonData buttonData) {
        this(null, text, buttonData);
    }

    /**
     * Provide key or text. The other one should be null.
     */
    private ButtonType(String key, String text, ButtonData buttonData) {
        this.key = key;
        this.text = text;
        this.buttonData = buttonData;
    }

    /**
     * Returns the ButtonData specified for this ButtonType in the constructor.
     */
    public final ButtonData getButtonData() { return this.buttonData; }

    /**
     * Returns the text specified for this ButtonType in the constructor;
     */
    public final String getText() {
        if (text == null && key != null) {
            return ControlResources.getString(key);
        } else {
            return text;
        }
    }

    /** {@inheritDoc} */
    @Override public String toString() {
        return "ButtonType [text=" + getText() + ", buttonData=" + getButtonData() + "]";
    }
}

->The button displaying text is rendered from ControlResources.getString(key), its source code:

package com.sun.javafx.scene.control.skin.resources;

import java.util.ResourceBundle;

public final class ControlResources {

    // Translatable properties
    private static final String BASE_NAME = "com/sun/javafx/scene/control/skin/resources/controls";

    // Non-translateable properties
    private static final String NT_BASE_NAME = "com/sun/javafx/scene/control/skin/resources/controls-nt";

    // Do not cache the bundle here. It is cached by the ResourceBundle
    // class and may be updated if the default locale changes.

    private ControlResources() {
        // no-op
    }

    /*
     * Look up a string in the properties file corresponding to the
     * default locale (i.e. the application's locale). If not found, the
     * search then falls back to the base controls.properties file,
     * containing the default string (usually English).
     */
    public static String getString(String key) {
        return ResourceBundle.getBundle(BASE_NAME).getString(key);
    }

    /*
     * Look up a non-translatable string in the properties file
     * corresponding to the default locale (i.e. the application's
     * locale). If not found, the search then falls back to the base
     * controls-nt.properties file, containing the default string.
     *
     * Note that property values may be set in locale-specific files,
     * e.g. when a property value is defined for a country rather than
     * a language. However, there are no such files included with
     * JavaFX 8, but may be added to the classpath by developers or
     * users.
     */
    public static String getNonTranslatableString(String key) {
        return ResourceBundle.getBundle(NT_BASE_NAME).getString(key);
    }
}

Now, I tried my solution as follow:
Step 1: create Vietnamese resource file com/sun/javafx/scene/control/skin/resources/controls_vi.properties in the project
enter image description here

### Dialogs ###

Dialog.apply.button = áp du1EE5ng
Dialog.ok.button = OK
Dialog.close.button = u0110óng
Dialog.cancel.button = Hu1EE7y bu1ECF
Dialog.yes.button = Có
Dialog.no.button = Kh?ng
Dialog.finish.button = Hoàn thành 
Dialog.next.button = Tiu1EBFp 
Dialog.previous.button = Tru01B0u1EDBc 

After lauching the app, the button language still English.
Step 2: I figured out that the class loader to load JavaFx resource file is differ from my app class loader (see ResourceBundle.getBundle(BASE_NAME) API). This is resource inside jfxrt.jar:
enter image description here
I tried to load the ControlResources class with application class loader but still no result:

public static void main(String[] args) throws Exception {
    List<Locale> fxSupported = Arrays.asList(Locale.ENGLISH, Locale.FRENCH); // Add later ....
    Locale appLocale = getLocaleSettingFromConfigurationFile();
    Locale.setDefault(appLocale);
    
    // Load class from current class loader
    if (!fxSupported.contains(appLocale)){
        ClassLoader loader = Main.class.getClassLoader();
        Class<?> loadedCls = Class.forName("com.sun.javafx.scene.control.skin.resources.ControlResources", true, loader);
        
        System.out.printf("Loader 1: %s
loader 2: %s
", loader, loadedCls.getClassLoader());
//          Loader 1: sun.misc.Launcher$AppClassLoader@73d16e93
//          loader 2: sun.misc.Launcher$ExtClassLoader@6d06d69c

    }
    
    launch(args);
}

Fallback solution
I can create my own ButtonType "OK", "Cancel" and load my own resource string, the set created button list to the Alert object, but I want to use the system provided resource instead.

ResourceBundle res = ResourceBundle.getBundle("application.

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

I am very upset that the JRE inside very few exotic languages. And this is a big problem. I've been looking for a solution, I created an open source project which demonstrates how to add new languages resources in this project.

Project on GitHub

I translated the system controls JavaFX into a new language (be-BY, ru-RU): enter image description here

Structure of my project:

java
    |------ comkrasutskilanguageMessages.java
    |------ comkrasutskiutilPropertyLoader.java
    |------ comkrasutskiutilReflectionUtils.java
    |------ comkrasutskiviewMainController.java
    |------ comkrasutskiMainApp.java
resources
    |------ comsunjavafxscenecontrolskin
esourcescontrols_be_BY.properties
    |------ comsunjavafxscenecontrolskin
esourcescontrols_ru.properties
    |------ fxmlmain.fxml
    |------ iconsapp-128x128x32.png
    |------ messagesmessages.properties
    |------ messagesmessages_be_BY.properties
    |------ messagesmessages_ru.properties
    |------ stylesstyles.css

the solution to the problem is in 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;
    }
}

An example slice file controls_be_BY.properties

# encoding=utf-8
# ProgressIndicator, the string that's displayed at 100%
ProgressIndicator.doneString=Гатова

# ListView
ListView.noContent=Няма змесц?ва

# TableView
TableView.noContent=Няма змесц?ва ? табл?цы
TableView.noColumns=Няма калонак ? табл?цы

Here you don't need to use a special character u you just write to any text editor which supports Unicode.

You can add your exotic languages folder resources/com/sun/javafx/scene/control/skin/resources of this project. Send me your controls_*.properties and I'll add them to this project.

Ready assembled example you can download in the releases section


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...