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

### Dialogs ###
Dialog.apply.button = Áp d\u1EE5ng
Dialog.ok.button = OK
Dialog.close.button = \u0110óng
Dialog.cancel.button = H\u1EE7y b\u1ECF
Dialog.yes.button = Có
Dialog.no.button = Không
Dialog.finish.button = Hoàn thành 
Dialog.next.button = Ti\u1EBFp 
Dialog.previous.button = Tr\u01B0\u1EDBc 
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:
 
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\nloader 2: %s\n", 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.myownres");
        ButtonType OK = new ButtonType(res.getString("btn.ok"), ButtonData.OK_DONE);
        ButtonType CANCEL = new ButtonType(res.getString("btn.cancel"), ButtonData.CANCEL_CLOSE);
        
        Alert alert = new Alert(AlertType.CONFIRMATION, "Are you sure", OK, CANCEL);
        alert.showAndWait();
So, anyone has solution that does not need to create new ButtonType object.
Thanks
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):

Structure of my project:
java
    |------ com\krasutski\language\Messages.java
    |------ com\krasutski\util\PropertyLoader.java
    |------ com\krasutski\util\ReflectionUtils.java
    |------ com\krasutski\view\MainController.java
    |------ com\krasutski\MainApp.java
resources
    |------ com\sun\javafx\scene\control\skin\resources\controls_be_BY.properties
    |------ com\sun\javafx\scene\control\skin\resources\controls_ru.properties
    |------ fxml\main.fxml
    |------ icons\app-128x128x32.png
    |------ messages\messages.properties
    |------ messages\messages_be_BY.properties
    |------ messages\messages_ru.properties
    |------ styles\styles.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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With