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

8 minute read

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 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 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 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:

Updated: