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 org.fipro.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 org.fipro.e4.translation.LocalizationHelper;
import org.fipro.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 org.fipro.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 && !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 ResourceBundle
s. 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 org.fipro.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<String, String> map = new HashMap<String, String>();
for(int i = 0; i < 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<String, String> 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 org.fipro.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 ResourceBundle
s, 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:
- Eclipse Internationalization Part 1/4 – Current Situation
- Eclipse Internationalization Part 2/4 – New Message Extension
- Eclipse Internationalization Part 3/4 – Migration
- Eclipse Internationalization Part 4/4 – New Features
- New Message Extension Update