0

My Java 8/Swing app uses a ResourceBundle and a couple of .properties files to change the language when a user picks one from a JComboBox:

public static ResourceBundle resourceBundle;
private JComboBox<Locale> comboBox;
private JLabel myLabel;

public Main() {
    //More GUI setup here
    resourceBundle = ResourceBundle.getBundle("Bundle", Locale.ENGLISH); //Set first/default language
    comboBox = new JComboBox<Locale>();
    comboBox.addItem(Locale.ENGLISH);
    comboBox.addItem(Locale.GERMAN);
    comboBox.addItem(Locale.FRENCH);

    myLabel = new JLabel(resourceBundle.getString("myLabelText"));
    myLabel.setFont(new Font("Tahoma", Font.PLAIN, 14));
}

An ActionListener on the JComboBox calls this, which changes the language of all GUI elements instantly:

private void changeLanguage() {
    Locale locale = comboBox.getItemAt(comboBox.getSelectedIndex());
    resourceBundle = ResourceBundle.getBundle("Bundle", locale);
    myLabel.setText(resourceBundle.getString("myLabelText"));
}

There are a lot more labels and buttons in my app that have their language set through changeLanguage (not all of them use localization though, e.g. the "home" button with a house icon) and I intend to add more. With the amount of GUI items increasing, it also gets easier to simply forget to add one to the function, that's why my question is:

Is there a way to "register" a JLabel,... and its key to some class (directly after creating it) and changing the language afterwards (by loading the other Locale) also automatically changes the text of the JLabel,...? Is there a commonly used approach that differs from how I'm doing it?

Neph
  • 1,823
  • 2
  • 31
  • 69
  • You may want to recursively retrieve components of a given type, see : https://stackoverflow.com/questions/694287/iterate-recurse-through-containers-and-components-to-find-objects-of-a-given-c – Arnaud Oct 04 '19 at 10:04
  • @Arnaud Thanks for your suggestion, unfortunately it won't work in my case. While a list would help to keep track of the components, adding all of them to it wouldn't if a couple of labels, buttons,... don't use localization (e.g. a "home" button with a house icon). Furthermore, how would my app know what key to use for a specific UI element? This would require a second list (1 for the component, 1 for the keys), which would quickly get conusing with a lot of elements, or a custom object that keeps track of both, which would be overkill (imo). – Neph Oct 04 '19 at 11:18

2 Answers2

0

I faced this problem recently, so I will share what I tried and what it worked for me. Note that I needed to implement a change-font action at runtime as well.

In a Swing application, we usually expand container classes for the core containers. Let's say we want to create Facebook for desktop. Someone could create 3 core classes (extending JPanel or JScrollPane). Let's say: LeftPanel extends JPanel, MiddlePanel extends JPanel and RightPanel extends JPanel. Left panel will represent the left menu, middle panel will represent the main scrolling view and finally the right panel will represent the ads area. Of course, every single of this panels will have inherited JPanels (and probably some of them will have a new class as well, e.g PostPanel, CommentSectionPanel etc.)

Now, assuming you have read The Use of Multiple JFrames: Good or Bad Practice?, your application uses only one JFrame and it hosts every single component in it. Even modal JDialogs are based on it (have it as its parent). So, you can keep it somewhere like a Singleton.

In order to change components text, we will have to call setText for each one of them. Calling JFrame's getComponents will give us all its components. If one of them is container, we will have to call getComponents for it, since it probably has components in it as well. The solution is a recursion to find all of them:

private static <T extends Component> List<T> getChildren(Class<T> clazz, final Container container) {
    Component[] components;
    if (container instanceof JMenu)
        components = ((JMenu) container).getMenuComponents();
    else
        components = container.getComponents();
    List<T> compList = new ArrayList<T>();
    for (Component comp : components) {
        if (clazz.isAssignableFrom(comp.getClass())) {
            compList.add(clazz.cast(comp));
        }
        if (comp instanceof Container)
            compList.addAll(getChildren(clazz, (Container) comp));
    }
    return compList;
}

Calling this method with arguments java.awt.Component.class and myJFrame will give you all the components your JFrame has.

Now, we have to separate which of them can be "refreshed" (change language) and which of them can't. Let's create an Interface for that:

public static interface LocaleChangeable {
    void localeChanged(Locale newLocale);
}

Now instead of giving this ability to each one of them, we give it to the big containers (the example with facebook): LeftPanel extends JPanel implements LocaleChangeable, RightPanel extends JPanel implements LocaleChangeable because they have components with text property.

These classes now are responsible for changing the text of their components. A pseduo-example would be:

public class LeftPanel extends JPanel implements LocaleChangeable {
    private JLabel exitLabel;

    @Override
    public void localeChanged(Locale newLocale) {
        if (newLocale == Locale.ENGLISH) {
            exitLabel.setText("Exit");
        } else if (newLocale == Locale.GREEK) {
            exitLabel.setText("Έξοδος"); //Greek exit
        }
    }
}

(Of course instead of a bunch of if-else conditions, the ResourceBundle logic will take place).

So... Let's call this method for all classes-containers:

private void broadcastLocaleChange(Locale locale) {
    List<Component> components = getChildren(Component.class, myFrame);
    components.stream().filter(LocaleChangeable.class::isInstance).map(LocaleChangeable.class::cast)
            .forEach(lc -> lc.localeChanged(locale));
}

That's it! A full example would be:

public class LocaleTest extends JFrame {
    private static final long serialVersionUID = 1L;

    public LocaleTest() {
        super("test");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        getContentPane().setLayout(new BorderLayout());
        add(new MainPanel());

        pack();
        setLocationRelativeTo(null);
        setVisible(true);
    }

    private class MainPanel extends JPanel implements LocaleChangeable {
        private JLabel label;
        private JButton changeLocaleButton;

        public MainPanel() {
            super(new FlowLayout());
            label = new JLabel(Locale.ENGLISH.toString());
            add(label);

            changeLocaleButton = new JButton("Change Locale");
            changeLocaleButton.addActionListener(e -> {
                broadcastLocaleChange(Locale.CANADA);
            });
            add(changeLocaleButton);
        }

        @Override
        public void localeChanged(Locale newLocale) {
            label.setText(newLocale.toString());
            System.out.println("Language changed.");
        }

        private void broadcastLocaleChange(Locale locale) {
            List<Component> components = getChildren(Component.class, LocaleTest.this);
            components.stream().filter(LocaleChangeable.class::isInstance).map(LocaleChangeable.class::cast)
                    .forEach(lc -> lc.localeChanged(locale));
        }
    }

    private static <T extends Component> List<T> getChildren(Class<T> clazz, final Container container) {
        Component[] components;
        if (container instanceof JMenu)
            components = ((JMenu) container).getMenuComponents();
        else
            components = container.getComponents();
        List<T> compList = new ArrayList<T>();
        for (Component comp : components) {
            if (clazz.isAssignableFrom(comp.getClass())) {
                compList.add(clazz.cast(comp));
            }
            if (comp instanceof Container)
                compList.addAll(getChildren(clazz, (Container) comp));
        }
        return compList;
    }

    public static interface LocaleChangeable {
        void localeChanged(Locale newLocale);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> new LocaleTest().setVisible(true));
    }
}
George Z.
  • 6,643
  • 4
  • 27
  • 47
  • Thanks for the long example! At any time there's only one `JFrame` active in my app, even though it currently uses two with the first being the login screen that is replaced by the normal app window. Both of them use localization and the same `ResourceBundle` but not all of the UI elements have text that should be changed. I see what you're doing: Pressing the button triggers the `ActionListener`, calls `broadcastLocaleChange` and gets all the components. But what happens to components that don't use localized text and how does `localeChanged` find the right key for each of them? – Neph Oct 04 '19 at 12:00
  • @Neph I am not sure if I understand what you are asking. Since you do it manually in `localeChanged` method, you will give for each component its key, and you won't bother about the components that don't use localized text. – George Z. Oct 04 '19 at 15:52
  • If I understand you correctly, does that mean that I have to call `myLabel.setText(resourceBundle.getString("myLabelText"))` for each label/button in `localeChanged()` anyway? Isn't that a lot longer version of the code I posted in the question if there's simply a long list of components that have their text changed? Or is `.forEach(lc -> lc.localeChanged(locale))` calling `localeChanged()` once for each component? If so, how do I know which component is currently used to call it (so I can use the right key)? – Neph Oct 08 '19 at 13:48
  • Could you please explain how the actual language change is called in your code. – Neph Oct 17 '19 at 12:14
  • @Neph When the change language button is pressed, it calls the `localeChanged` of all panels. Now each panel is responsible for changing text to each component it owns. – George Z. Oct 17 '19 at 14:03
  • @Neph `localeChanged` will be called for MainPanel, LeftPanel and RightPanel. Assume `MainPanel` has the `mainLabel` and `mainButton`, so in its `localeChanged` it changes the text of `mainLabel` and `mainButton`. Thats the idea. – George Z. Oct 17 '19 at 14:05
  • Thanks for your reply. Am I correct?: You're setting the `toString` of that `Locale` as the text for the label/button, so changing the language from e.g. english to german would change the label from "en" to "de". What I still don't understand is how your code sets an actual text for different components. Example: A label should read "Hello" with "English" picked in a dropdown menu, "Hallo" with the german option and "Bonjour" in french. With a ResourceBundle I'd set the language like this: `helloLabel.setText(resourceBundle.getString("helloKey"))` but how does your code pick the right key? – Neph Oct 17 '19 at 14:31
  • @Neph No no. `locale.toString` was only to show that it is responding to the button click. Of course you will have to use resource bundle logic with keys. Just like you said. – George Z. Oct 17 '19 at 14:41
  • @Neph Excuse me if I confused you. There is a parenthesis I mentioned that: *Of course instead of a bunch of if-else conditions, the ResourceBundle logic will take place).*. – George Z. Oct 17 '19 at 14:43
  • Ah, I think I understand now: Just dump all the `someComponent.setText(resourceBundle.getString("someKey"))` into `localeChanged`. That's exactly what I'm already doing, except that I don't use any `extends`/`implements` for it and because of that there's also a lot less code. The problem with this approach (listing all the components) is that it's easy to forget one if you change the GUI. That's why I'm looking for a way to "register" all components on creation to the e.g. ResourceBundle, so they remember their keys and can switch language without actually listing all of them. – Neph Oct 17 '19 at 14:54
0

What I ended up with:

class ComponentInfo {
    JComponent component;
    String key;

    public ComponentInfo(JComponent c,String k) {
        component = c;
        key = k;
    }

    public JComponent getComponent() {return component;}
    public String getKey() {return key;}
}

To use this create an ArrayList and add every component that should change their language to it directly after creating it:

List<ComponentInfo> allComponentInfos = new ArrayList<ComponentInfo>();
JLabel label_pw = new JLabel(resourceBundle.getString("pw_key"));
label_pw.setFont(new Font("Tahoma", Font.PLAIN, 14));
allComponentInfos.add(new ComponentInfo(label_pw, "pw_key"));

Change the language by iterating over the list:

private void changeLanguage() {
    Locale locale = comboBox_language.getItemAt(comboBox_language.getSelectedIndex());
    resourceBundle = ResourceBundle.getBundle("Bundle", locale);

    for(ComponentInfo c:allComponentInfos) {
        if(c.getComponent() instanceof JLabel) {
            ((JLabel) c.getComponent()).setText(resourceBundle.getString(c.getKey()));
        } else if(c.getComponent() instanceof JButton) {
            ((JButton) c.getComponent()).setText(resourceBundle.getString(c.getKey()));
        }
    }
}

I'm aware that this creates an extra object for each component. :/ Unfortunately this is the only way I've found that lets me change the language of only the JLabels, JButtons,... that need to have their language changed, without actually having to manually list all of them in a method (and possibly forgetting one).

Neph
  • 1,823
  • 2
  • 31
  • 69