Search code examples
javajavafxdialoginternationalization

JavaFX dialog internationalization in java module based project


I would like to ask for a help with internationalization of buttons in JavaFX built in dialogs.

Translations for buttons are available in https://github.com/openjdk/jfx/tree/master/modules/javafx.controls/src/main/resources/com/sun/javafx/scene/control/skin/resources There are some other countries and also fallback to English variant.

From code POV translatiosn are loaded by this class: https://github.com/openjdk/jfx/blob/master/modules/javafx.controls/src/main/java/com/sun/javafx/scene/control/skin/resources/ControlResources.java

public final class ControlResources {

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

    public static String getString(String key) {
        return ResourceBundle.getBundle(BASE_NAME).getString(key);
    }
}

To add custom translation I would create new controls_xx.properties file in resources of my application under following package: com/sun/javafx/scene/control/skin/resources/controls.

The issue is that "java-modules" do not allow to have one package in multiple modules.

What are the possibilities to load custom translations for JavaFX dialogs?

Note: disabling "java-modules" is not an option.

Thank you

EDIT: I'm aware of this thread: Javafx Internationalization with custom language


Solution

  • Ideally, the javafx.controls module would make use of the ResourceBundleProvider SPI. This would let your own module provide an implementation that would find the custom resource bundles. But since JavaFX does not make use of said SPI (as of version 21.0.1), and since split packages are not allowed, another solution is needed.

    One option is to make use of --patch-module. For instance, if you wanted to add translations for Russian (which is not one of the provided languages in JavaFX 21), then you would create the properties file in the correct package:

    com/sun/javafx/scene/control/skin/resources/controls_ru.properties
    

    And place that in some directory; let's say that directory is named bundles for this example. Then at run-time, you would use the following option:

    --patch-module javafx.controls=bundles
    

    Note: This assumes bundles is relative to the working directory. If that's not the case, then use the proper relative path or an absolute path.


    Example

    Here's a full example using Java 21.0.1, JavaFX 21.0.1, and Gradle 8.5.

    Source Code

    module-info

    module app {
      requires javafx.controls;
    
      exports sample to
          javafx.graphics;
    }
    

    sample.Main

    package sample;
    
    import java.util.Locale;
    import javafx.application.Application;
    import javafx.scene.Scene;
    import javafx.scene.control.Alert;
    import javafx.scene.control.Alert.AlertType;
    import javafx.scene.control.Button;
    import javafx.scene.control.ButtonType;
    import javafx.scene.layout.StackPane;
    import javafx.stage.Stage;
    
    public class Main extends Application {
    
      @Override
      public void start(Stage primaryStage) throws Exception {
        Locale.setDefault(Locale.of("ru"));
    
        var dialogBtn = new Button("Show dialog");
        dialogBtn.setOnAction(
            e -> {
              var alert = new Alert(AlertType.CONFIRMATION);
              alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO);
              alert.initOwner(primaryStage);
              alert.setContentText("This text is intentionally English.");
              alert.show();
            });
    
        var root = new StackPane(dialogBtn);
        primaryStage.setScene(new Scene(root, 500, 300));
        primaryStage.setTitle("Demo");
        primaryStage.show();
      }
    }
    

    Bundle Properties File

    controls_ru.properties

    Dialog.yes.button = Да
    Dialog.no.button = Нет
    
    Dialog.confirm.title = Подтверждение
    Dialog.confirm.header = Подтверждение
    

    Note: There are many more properties you'd have to set to properly add a language, but these are all that were needed for this example.

    These translations were taken from Google Translate. I have no idea how accurate they are.

    English Russian
    Yes Да
    No Нет
    Confirmation Подтверждение

    Gradle Files

    settings.gradle.kts

    rootProject.name = "app"
    

    build.gradle.kts

    plugins {
        id("org.openjfx.javafxplugin") version "0.1.0"
        application
    }
    
    group = "sample"
    version = "0.1.0"
    
    application {
        mainModule = "app"
        mainClass = "sample.Main"
    }
    
    javafx {
        modules("javafx.controls")
        version = "21.0.1"
    }
    
    repositories {
        mavenCentral()
    }
    
    tasks {
        named<JavaExec>("run") {
            jvmArgs("--patch-module=javafx.controls=${file("bundles")}")
        }
    }
    

    Project Structure

    <PROJECT_DIRECTORY>
    │   build.gradle.kts
    │   settings.gradle.kts
    │
    ├───bundles
    │   └───com
    │       └───sun
    │           └───javafx
    │               └───scene
    │                   └───control
    │                       └───skin
    │                           └───resources
    │                                   controls_ru.properties
    │
    └───src
        └───main
            └───java
                │   module-info.java
                │
                └───sample
                        Main.java
    

    Note the bundles directory is not under src/main/resources. You don't want to package this in your own module, as doing so will cause a split-package error. Unfortunately, this means you have to find a way to deploy bundles alongside your application. I assume doing this would be relatively easy using a tool like jpackage, but I haven't tried it.

    Output

    From executing the Gradle run task.

    screenshot of running application