92

I've just started writing my first JavaFX 2 application after learning the basics and would like to internationalize it.

I notice that in JavaFX 1.x, the scripting language allowed for very simple internationalization of strings. Are there any similar features in JavaFX 2?

Basically: what is the best practice for internationalizing a JavaFX 2 application?

Riki137
  • 2,076
  • 2
  • 23
  • 26
wobblycogs
  • 4,083
  • 7
  • 37
  • 48
  • For switching between languages is some information here: [http://stackoverflow.com/a/26318795/2131257][1] [1]: http://stackoverflow.com/a/26318795/2131257 – Androdos Oct 11 '14 at 20:10

3 Answers3

176

The basic steps (among others) of a java app internationalizing, are Localelizing and resource bundling. In JavaFX, you can use FXMLLoader#setResources() for that purposes. Here a SSCCE demo to demonstrate it. The codes are self-descriptive.
Demo package structure:

bundledemo
    |------ BundleDemo.java
    |------ MyController.java
    |------ MyView.fxml  
bundles
    |------ MyBundle_en.properties
    |------ MyBundle_kg.properties

MyBundle_en.properties

key1=Name Surname
key2=How are you?

MyBundle_kg.properties

key1=Aты Жөнү
key2=Кандайсың?

MyView.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.*?>

<BorderPane fx:controller="bundledemo.MyController" xmlns:fx="http://javafx.com/fxml">
    <top>
        <!-- This label's text will be set by the controller -->
        <Label fx:id="lblTextByController"/> 
    </top>
    <center>
        <!-- This label's text will be taken from the bundle automatically -->
        <Label text="%key2"/>
    </center>
</BorderPane>

MyController.java

package bundledemo;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;

public class MyController implements Initializable {

    @FXML private Label lblTextByController;
    private ResourceBundle bundle;
    
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        bundle = resources;
        lblTextByController.setText(bundle.getString("key1"));
    }
}

BundleDemo.java

package bundledemo;
// imports are ignored.

public class BundleDemo extends Application {

    private Stage stage;

    @Override
    public void start(Stage primaryStage) {
        stage = primaryStage;
        Button btnEN = new Button();
        btnEN.setText("English");
        btnEN.setOnAction(new EventHandler<ActionEvent>() {
            @Override public void handle(ActionEvent event) {
                loadView(new Locale("en", "EN"));
            }
        });

        Button btnKG = new Button();
        btnKG.setText("Kyrgyz");
        btnKG.setOnAction(new EventHandler<ActionEvent>() {
            @Override public void handle(ActionEvent event) {
                loadView(new Locale("kg", "KG"));
            }
        });

        VBox root = new VBox(20);
        root.getChildren().add(HBoxBuilder.create().spacing(10).style("-fx-background-color: gray").padding(new Insets(5)).children(btnEN, btnKG).build());
        root.getChildren().add(new StackPane());
        primaryStage.setScene(new Scene(root, 300, 250));
        primaryStage.show();
    }

    private void loadView(Locale locale) {
        try {
            FXMLLoader fxmlLoader = new FXMLLoader();
            fxmlLoader.setResources(ResourceBundle.getBundle("bundles.MyBundle", locale));
            Pane pane = (BorderPane) fxmlLoader.load(this.getClass().getResource("MyView.fxml").openStream());
            // replace the content
            StackPane content = (StackPane) ((VBox) stage.getScene().getRoot()).getChildren().get(1);
            content.getChildren().clear();
            content.getChildren().add(pane);
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Screenshot:

enter image description here


If your internationalized text needs to be rendered in a font that might be on the user's target system, then you can either:

  1. Embed the font with your application:

OR

  1. Use web(Google) fonts in JavaFX.

If the required font is not available, then the internationalized text might be displayed as unintelligible gibberish, even though everything else about the setup is fine.

jewelsea
  • 150,031
  • 14
  • 366
  • 406
Uluk Biy
  • 48,655
  • 13
  • 146
  • 153
  • Excellent answer and I will accept it as is but I should have mentioned that I'm am building the interface in code rather than FXML. Is there a quick and easy way to internationalize in code, I realize I can do a ResourceBundle.getBundle + lookups but I was hoping there was something like the %key notation that I can be used instead. – wobblycogs Apr 13 '12 at 22:36
  • 7
    Then you can do that in ordinary way like in any other Java application. Determine user's/client's locale then change app's locale accordingly (get lang specific data from DB vs.). Load appropriate bundle by `ResourceBundle.getBundle("bundles.MyBundle", locale)`. Change every text you have used in your view/page by `bundle.getString("key")`. – Uluk Biy Apr 13 '12 at 22:37
  • 2
    It is not working for me if I provide a ResourceBundle over the setResources() method. It is working when I provide the ResourceBundle via the load() method. – Jurica Krizanic Mar 25 '13 at 10:27
  • 1
    @Jurica Krizanic: had the same problem, and resolved it via same way: `FXMLLoader.load(getClass().getResource(sceneId), getResources())` where `sceneId` is string and method `getResources()` returns resource with proper locale. – T.G Mar 13 '15 at 12:06
  • Nice, a variant with the static way like in the oracle tutorial works too (and it is shorter): Pane pane = (BorderPane) FxmlLoader.load(this.getClass().getResource("MyView.fxml"),ResourceBundle.getBundle("bundles.MyBundle", locale)); – pdem Jan 19 '16 at 11:25
  • 1
    Hello @UlukBiy Can we Add text Multiple time in `` like "%key2%key1". because i already try it but its give me Exception....but i don't know its possible or not...! – Bhola Nov 27 '18 at 12:00
  • or can we add `key2` + "any other delimiter" like `%key *` because, i tried that but its not working – Bhola Nov 27 '18 at 12:05
  • If you make each fx:id the same name as the bundle key, reflection can be used to avoid some boilerplate code: public void initialize(URL location, ResourceBundle resources) { for (Field fld : this.getClass().getFields()) { try { Object obj = fld.get(this); if (obj instanceof Labeled) { Labeled ctl = (Labeled) obj; String key = fld.getName(); String txt = resources.getString(key); if (!Strings.isNullOrEmpty(txt)) { ctl.setText(txt); } } } catch (Illeg....) {// fld has to be public – karl Jan 04 '19 at 12:30
  • Hello, I would like to know what happens if you reload the view a huge number of times, is there a chance for the JavaFX thread to lag? – 0009laH Jan 21 '21 at 12:20
17

This works for me:

└───src
    ├───app
    ├───bundles // <- here the "bundles"
    ├───dicts
    ├───images
    ├───libs
    └───resources

In the bundles package are

LangBundle_en.properties
LangBundle_de.properties

Sample content:

enter_pwd=Enter your password:

To load them I use the following code:

@Override
public void initialize(URL location, ResourceBundle resources) {
    ResourceBundle lngBndl = ResourceBundle
            .getBundle("bundles.LangBundle", new Locale("en", "EN"));

    tvSetupPwd.setText(lngBndl.getString("enter_pwd"));
    // ...
}
Martin Pfeffer
  • 12,471
  • 9
  • 59
  • 68
6

Look at my example enter image description here

More I described here or on GitHub

Update:

the solution 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;
    }
}
Andrei Krasutski
  • 4,913
  • 1
  • 29
  • 35
  • @Moritz you would have clicked the link and saw the detailed response and full source files, I have included a link in response to stackoverflow.com. Because of You, I have to insert everywhere the same code. I expect you like and not dislike – Andrei Krasutski Aug 31 '17 at 11:59