Eclipse Internationalization Part 4/4 – New Features by Dirk Fauth

Finally I found the time to write the last part of my blog post series about Eclipse internationalization with the new message extension created by Tom Schindl and me. The series started with showing the issues in the current solution in Eclipse itself, introducing the new message extension and explaining how to migrate to it. Now it’s time to finish the series with showing the cool new features that are possible using the new message extension in Eclipse 4. If you’re still not convinced to use the new message extension for internationalization, you surely will be after this post.

Beside all the advantages I described in the second post of this series, there are two new major features I will cover in this post:

  • Locale changes at runtime
  • Class based ResourceBundle

Locale changes at runtime

With the new message extension and the dynamic injection feature in Eclipse 4, it is now possible to change the locale in an Eclipse application at runtime. No more restarting of the application if you want to change the locale in a running application. No starter dialog that is necessary to select the locale before the main application is started. Simply change the locale like in web applications or other UI toolkits.

To support this, you need to restructure your UI code the following way:

  • Make sure that every control that needs to be localized is available as member variable.
  • Create the instances of those controls in the constructor or ensure to call the method that will perform the translation at the end of the method annotated with @PostConstruct. This is important as the method annotated with @PostConstruct is exectued after the method injection is done. Otherwise your UI will be empty until you set the Locale and the dynamic method injection is executed.
  • Create a new method that is annotated with @Inject and @Translation that takes your message implementation as parameter
  • Move every method call that is used for localization to that new method.
package com.beone.e4.translation.parts;

import javax.inject.Inject;

import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.e4.tools.services.Translation;
import org.eclipse.e4.ui.di.Focus;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;

import com.beone.e4.translation.LocalizationHelper;
import com.beone.e4.translation.osgi.OsgiMessages;

/**
 * Example showing the usage of the new message extension by using
 * OSGi ResourceBundles configured in the MANIFEST.MF
 */
public class OsgiExample {

	//the labels that will be localized
	private Label myFirstLabel;
	private Label mySecondLabel;
	private Label myThirdLabel;

	//create the label instances in the constructor because
	//the method injection is executed before @PostConstruct
	@Inject
	public OsgiExample(Composite parent, IEclipseContext context){
		myFirstLabel = new Label(parent, SWT.WRAP);
		mySecondLabel = new Label(parent, SWT.NONE);
		myThirdLabel = new Label(parent, SWT.NONE);
	}

	//the method that will perform the dynamic locale changes
	@Inject
	public void translate(@Translation OsgiMessages messages) {
		LocalizationHelper.updateLabelText(
			myFirstLabel, messages.first_label_message);
		LocalizationHelper.updateLabelText(
			mySecondLabel, messages.second_label_message);
		LocalizationHelper.updateLabelText(
			myThirdLabel, messages.third_label_message);
	}

	@Focus
	public void onFocus() {
		if (myFirstLabel != null) {
			myFirstLabel.setFocus();
		}
	}
}

You also need to ensure that on calling the method, the controls are created and not disposed. Otherwise you might get exceptions for example on closing the application when the method is called but the controls are already disposed.

In my sample I created a LocalizationHelper to encapsulate that task.

package com.beone.e4.translation;

import org.eclipse.swt.widgets.Label;

/**
 * Helper class that checks if the {@link Control} to localize
 * is created and not disposed before setting the text.
 */
public class LocalizationHelper {

	/**
	 * Update the text of a {@link Label}. Checks if the given
	 * {@link Label} instance is <code>null</code> or disposed
	 * before setting the text.
	 *
	 * @param label The {@link Label} to set the text to
	 * @param text The text to set.
	 */
	public static void updateLabelText(Label label, String text) {
		if (label != null &amp;&amp; !label.isDisposed())
			label.setText(text);
	}
}

I call this the “Eclipse translation pattern“. Although I guess there is already a name for that kind of pattern, I love the idea of having created a new one. :-)

Now that our part is prepared for locale changes at runtime, there needs to be a way for the user to trigger the locale change. This action simply needs to set the value for the key TranslationService.LOCALE to the OSGi context of the application. This will trigger the TranslationObjectSupplier to create new instances of your messages classes, which will then be dynamically injected into your part.

/**
 * Item that is added as a ToolControl to the TrimBar of the
 * Window. Shows an input field which allows to enter a
 * locale String and a Button to send the action to update
 * the Locale.
 */
public class LocaleChangeItem {

	Button button;

	@Inject
	public LocaleChangeItem(
		Composite parent, final MApplication mApplication) {

		final Text input = new Text(parent, SWT.BORDER);
		input.addKeyListener(new KeyAdapter() {
			@Override
			public void keyPressed(KeyEvent e) {
				if (e.keyCode == 13) {
					updateLocale(
						mApplication
							.getContext()
							.getParent(),
						input.getText());
				}
			}
		});

		button = new Button(parent, SWT.PUSH);
		button.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				updateLocale(
					mApplication
						.getContext()
						.getParent(),
					input.getText());
			};
		});
	}

	/**
	 * Set the new locale String to the OSGi context.
	 * @param context The OSGi context to set the locale String to
	 * 	This is the parent of the application context.
	 * @param input The new locale String to set.
	 */
	private void updateLocale(
			IEclipseContext context, String input) {

		context.set(TranslationService.LOCALE, input);
	}

	@Inject
	public void translate(@Translation OsgiMessages messages) {
		LocalizationHelper.updateButtonText(
			button, messages.button_change_locale);
	}
}

That’s all you have to do to add support for dynamic locale changes at runtime into your Eclipse application.

I also have to mention that at the time writing this post, there is also one downside to this solution. The Application Model does not support it. Therefore part titles, menus, commands, etc. are not affected of the locale change. You are able to workaround this by adding the necessary tasks to one of the localization methods, for example by creating the following translation method.

@Inject
public void translate(
	@Translation LocationMessages messages, MPart part) {

	LocalizationHelper.updateLabelText(
		myFirstLabel, messages.first_label_message);
	LocalizationHelper.updateLabelText(
		mySecondLabel, messages.second_label_message);
	LocalizationHelper.updateLabelText(
		myThirdLabel, messages.third_label_message);

	//also update the part
	if (part != null) {
		part.setLabel(messages.part_title);
	}
}

But it would be much better if the Application Model, respectively the Eclipse platform itself, would add that support. So hopefully one of the Eclipse platform developers is reading this post and shares our oppinion about this feature.

Class based ResourceBundle

As I explained in the second post of this series, with the new message extension it is possible to create class based ResourceBundles. This for example allows to specify translations that are read out of a database instead of a Properties file.

Implementing a class based ResourceBundle is fairly easy. You simply need to create a class for every locale you want to support, following the naming rules for resource bundles
[basename][_language][_country][_variant].class
The following examples will use a MockStore class which can be asked for the available keys and the value for a key and a locale. To implement database related translations, you need to implement these actions as database requests.

The easiest way for implementing a class based ResourceBundle is to extend ListResourceBundle and implement getContents() to return the key-value-pairs in a two dimensional array.

package com.beone.e4.translation.resources;

import java.util.HashMap;
import java.util.List;
import java.util.ListResourceBundle;
import java.util.Map;

public class ListMockBundle extends ListResourceBundle {

	@Override
	protected Object[][] getContents() {
		List keys = MockStore.getKeys();

		Map&lt;String, String&gt; map =
			new HashMap&lt;String, String&gt;();
		for(int i = 0; i &lt; keys.size(); i++) {
			String key = keys.get(i);
			String value =
				MockStore.translate(
					keys.get(i), getLocale());
			if (value != null) {
				map.put(key, value);
			}
		}

		Object[][] result = new Object[map.size()][2];
		int counter = 0;
		for (Map.Entry&lt;String, String&gt; entry : map.entrySet()) {
			result[counter][0] = entry.getKey();
			result[counter][1] = entry.getValue();

			counter++;
		}
		return result;
	}
}

Another option would be to extend ResourceBundle directly. In this case you need to implement how to get all available keys and how to retrieve a value for a key yourself.

package com.beone.e4.translation.resources;

import java.util.Enumeration;
import java.util.Iterator;
import java.util.ResourceBundle;

public class MockBundle extends ResourceBundle {

	@Override
	protected Object handleGetObject(String key) {
		//For every locale there need to be a corresponding
		//subclass. This way getLocale() is able to retrieve
		//the correct locale used for translation.
		return MockStore.translate(key, getLocale());
	}

	@Override
	public Enumeration getKeys() {
		return new Enumeration() {

			private Iterator iterator =
				MockStore.getKeys().iterator();

			@Override
			public boolean hasMoreElements() {
				return iterator.hasNext();
			}

			@Override
			public String nextElement() {
				return iterator.next();
			}
		};
	}
}

Dependent on your use cases you should determine which approach fits your needs best. The most important fact for this post is, that the new message extension adds the support for class based ResourceBundles, which is not possible in the platform.

For the sake of completeness, with Eclipse 4 it is already possible to read the localization data out of a database or even from Google translation services by creating a custom TranslationService and set it to the context (as Tom Schindl showed at conferences). But that is a different approach than creating a ResourceBundle that works in every Java application.

If you want to learn more about the ResourceBundle class and its subclasses, you might want to have a look at the Oracle Java Tutorials.

You find an example application showing the usage of the translation service at GitHub. It is a simple Eclipse 4 application that makes use of the new message extension. It consists out of several parts to show the various ways to perform localization by placing the resources at various places. It also contains the examples shown in this post.

https://github.com/fipro78/e4translationexample

I hope you enjoyed my blog post series about Eclipse Internationalization and you like the new message extension as much as we do.  We are happy about feedback and of course we would like to see it in the Eclipse Platform itself soon, as it is definitely an up to date mechanism for internationalization.

Links:

About Dirk Fauth

Dirk Fauth is a Software Architect for Rich Client Systems working for the Robert Bosch GmbH in Stuttgart and a lecturer in Java basics for the Baden-Wuerttemberg Cooperative State University (DHBW). He is active in developing, teaching and talking about OSGi, Eclipse RCP applications and Eclipse related technologies. He is project lead of the Nebula NatTable project, Eclipse Platform committer and also a committer and contributor to several other Eclipse projects. (Twitter: @fipro78)
This entry was posted in Dirk Fauth, Eclipse, OSGi. Bookmark the permalink.

5 Responses to Eclipse Internationalization Part 4/4 – New Features by Dirk Fauth

  1. Velganesh Subramanian says:

    Do you recommend any naming convention for members of Messages class? All fields are attributes and one can say that we have to follow standard Java convention but even you have different_style. What is the practice in 4.4 code? JDT UI and PDE UI uses pattern of kind OverviewPage_bundleTitle. It would be great if there is a convention to help other plug-in developers. Thanks for your informative article.

  2. Dirk Fauth says:

    What do you mean with “standard Java convention”? I only know the standard in web applications where the convention is to use lower case keys, separated with . for each hierarchy. This is why we added the dot separator support for properties files. In JDT and PDE UI it seems they use the class name where the property is used as first part of the key. I personally don’t like camel case properties keys, so I create the properties files per “convention” I know (lower case with dot separators) and create my Messages class according to it. JDT and PDE UI seem to go the other way, creating the properties file out of the messages class. Specifying a convention on this is hard, as it is more a personal preference, but asking me, I like it more to use known Java standards instead of having Eclipse proprietary (IMHO strange looking) properties keys. :)

  3. Velganesh Subramanian says:

    Thanks for your response. “label_message” is the name you have used in your Messages class example. As per Sun Java convention, we have to use labelMessage. We use Sonar and Checkstyle complains about this. We can configure the rules but I would like to hear expert opinions. One good feature in PDE is we can know the actual value when we hover over, have you retained it?

  4. Dirk Fauth says:

    Ah, so you were talking about conventions in naming the member variables and not the property keys. That was not clear in your first comment. Well you are right with your statement that the Java conventions created by Sun (now Oracle) are saying that variables should be named in mixed case, e.g. labelMessage. We could do this. We would need to implement some logic that scans for upper case letters, make them lower case and add a underscore or dot to find the corresponding property key. I suggest to create an enhancement ticket for this. This might also increase the visibility in the Eclipse universe.
    About the IDE integration, well I have to admit that currently there is no such integration. No externalization wizard, no hovering nor anything else at the moment. This is because our solution is currently only available in the E4 tools as we wanted to proof that there is a better way to internationalize an Eclipse 4 application. Unfortunately we haven’t got any response from the platform team, where the integration would need to take place.

  5. Dirk Fauth says:

    To keep you up to date on this. I created a ticket and a patch to add support for camel cased member variables in the Messages class. This way the Java Naming Conventions should be fullfilled.

    This means you can use myMemberVariable in the Messages class and my.member.variable in the resource bundle.

    https://bugs.eclipse.org/bugs/show_bug.cgi?id=418311

Comments are closed.