Eclipse Internationalization Part 2/4 – New Message Extension by Dirk Fauth and Tom Schindl

In my last blog post I explained the current available solutions for translating an Eclipse 4 application and what is wrong with them.

In this blog post I will show you a new solution for translating your Eclipse 4 application. This one is created as an OSGi service and gets rid of all the disadvantages of the existing solutions. The main idea and implementation was created by Tom Schindl, while I extended it to the current state, so we are writing this blog post together.

To use the services you need to install the latest E4 tools into your IDE or at least create a target-platform that contains the location to it. You could for example use the p2 update site for the E4 tools provided by vogella to get the latest version. (see here for more information http://blog.vogella.com/2013/03/12/eclipse-4-tools-update-sites-available-from-vogella/)

As the constant based solution has several benefits when used in combination with the tooling, e.g. code completion, refactoring and find references, the solution designed by Tom uses a similar approach. The big difference is, that it is not constant but member based. So using that solution is working on an instance of a message class, which means that it can be collected by the garbage collector if it is not referenced anymore and values can be changed at runtime.

Another advantage over the existing mechanisms is a more technical one. The existing translation mechanism in Eclipse is based on Java 1.4 and has some workaround code to deal with a special use case. What most people are not aware of is that the process on resolving a resource bundle is a little bit different than resolving a key out of a loaded resource bundle. Guess you have a loaded resource bundle for the locale de_DE, the search order for a key in that resource bundle looks like this:

  1. bundle_de_DE.properties
  2. bundle_de.properties
  3. bundle.properties

So it is searching from the most specific to the default bundle for that key. If the default bundle is configured to contain the English translations, if you missed to translate a key to German, the English one will be returned.

For resolving a resource bundle the process differs slightly with a huge impact. Guess you are running on a system that has the German locale set as default locale. Your application contains only the German and the default resource bundle files as shown above. What would you expect to get for translation if you set the application locale to fr_FR? Only knowing about the process above, you would expect to get the English translations out of the default resource bundle. But on resolving and loading the resource bundle, the default locale is used as fallback. So the following search order is used to resolve the bundle for fr_FR:

  1. bundle_fr_FR.properties
  2. bundle_fr.properties
  3. bundle_de_DE.properties
  4. bundle_de.properties
  5. bundle.properties

This means you will get the German translations on a German system if you try to localize your application to fr_FR and there are no such resource bundle files in your application. This follows the specification, but also has the impact that if you are trying to localize your application to en, which is specified in the base bundle without locale suffix, you will also get the German translations because of the fallback.

In Equinox resolving the resource bundle was implemented like resolving keys without taking the default locale as fallback into account. This solves the special case mentioned before, but also has the disadvantage of always skipping the fallback. That was reported and fixed a while ago (https://bugs.eclipse.org/bugs/show_bug.cgi?id=330602). A technical switch was introduced that allows to specify the equinox.root.locale as system property. If it is not set it will be treated as en by default. Now if the value for equinox.root.locale is en and the locale with language code en is requested, the default locale is not used as fallback.

Also the loading of resource bundles out of an OSGi bundle is a bit different because of the fragment support and the classloader per bundle. Using the Java 1.4 API dealing with all of these facts was quite difficult and caused the limitation that only properties based resource bundles are supported.

With Java 1.6 the ResourceBundle.Control class was introduced. It allows you to take control on how resource bundles are loaded, how the candidate locales are determined, e.g. using a fallback locale or not, and how the caching of the loaded resource bundles should be handled. The new message extension is based on Java 1.6 and makes use of this ResourceBundle.Control to deal with the described issues above in a more convenient way. Using the Java default mechanism instead of the custom workaround also enables the usage of class based resource bundles. The advantage of using class based resource bundles will be explained in the last blog post in this series.

To use the new message extension you need to add the org.eclipse.e4.tools.services bundle to the required plugins in your MANIFEST.MF. This bundle contains the OSGi services that are necessary for the new message extension and will give you access to two new annotations, @Message and @Translation.

To explain those annotations and their usage we first need to create the Messages class that will contain our translations.

public class Messages {
	public String label_message;
}

Comparing the implementation with the constant based OSGi NLS solution you will notice that

  • Messages is a POJO and does not implement or extend something
  • There are no constants but member variables
  • There is no static initialization block needed anymore

Now that we have the Messages class, we need to configure where to get the translation values that should be put into those member variables. As different developers have different opinions on where to put their resource bundles to, the new message extension supports a quite flexible approach to specify where the resource bundle files can be found. When processing the initialization, the mechanism searches for the resource bundle files in the following order:

  1. Check for a configuration via @Message annotation
    @Message(contributorURI=”...”)
    public class Messages {
    	public String label_message;
    }
  2. Check for resource bundle files with the same base name and in the same package as the Messages class
    NamedMessages
  3. Check for OSGi resource bundles configured in MANIFEST.MF
    If you placed your resource bundle files in OSGI-INF/l10n and named in bundle, there is nothing else you have to do

@Message annotation

The @Message annotation is optional and only necessary if you want to configure the location of the resource bundle files or the caching behaviour. To configure the location of the resource bundle files, you need to set the contributorURI parameter to the annotation. This parameter supports the following location patterns:

  • platform:/[plugin|fragment]/[Bundle-SymbolicName]
    Load the OSGi resource bundle out of the bundle/fragment named [Bundle-SymbolicName]
    For example:
    @Message(contributorURI="platform:/plugin/com.beone.e4.translation.extension")
    will load the OSGi resource bundle that is configured in the MANIFEST.MF of the com.beone.e4.translation.extension plugin
  • platform:/[plugin|fragment]/[Bundle-SymbolicName]/[Path]/[Basename]
    Load the resource bundle specified by [Path] and [Basename] out of the bundle/fragment named [Bundle-SymbolicName].
    For example:
    @Message(contributorURI="platform:/plugin/com.beone.e4.translation/resources/another")
    will load the resource bundle that is located in the folder resources/other in the com.beone.e4.translation plugin.
  • bundleclass://[plugin|fragment]/[Fully-Qualified-Classname]
    Instantiate the class based resourcebundle specified by [Fully-Qualified-Classname] out of the bundle/fragment named [Bundle-SymbolicName]. Note that the class needs to be a subtype of ResourceBundle.
    For example:
    @Message(contributorURI="bundleclass://com.beone.e4.translation/com.beone.e4.translation.resources.MockBundle")
    will load the class based resource bundle MockBundle in package com.beone.e4.translation.resources in the com.beone.e4.translation plugin.

Using the bundleclass contributorURI enables you to use class based resource bundles. What’s possible by implementing them will be explained in a later post.

While the constant based OSGi NLS translation mechanism had no chance to update the translation values at runtime and kept them in memory forever, you can specify the caching behaviour for messages in the new message extension. This is done by the referenceType parameter of the @Message annotation.

If the same translations are requested for injection at several places, for memory efficiency the same instance will be returned. This is achieved by internally caching the created instances as a SoftReference by default. As they are not hard references, once all requestors are garbage collected (e.g. the parts were closed) the message instance can be garbage collected as well. Using SoftReferences as default has the advantage that e.g. if a part is closed and opened again within a very short time, the message instance can be reused instead of being garbage collected and newly created. In case the default behavior does not suite your needs the @Message annotation provides the following caching/garbage collection strategies:

  • NONE: The message instance is not cached. Each requestor gets its own instance.
  • WEAK: The message instance is cached as a weak reference. If every requestor was garbage collected, the message instance is also discarded at the next garbage collection cycle.
  • SOFT: The message instance is cached as a soft reference. If every requestor was garbage collected, the message instance is not immediately discarded with the next garbage collection cycle, but will retain for a while in memory.

The strategy to use can be specified by setting the annotation parameter referenceType. The following for example will set the strategy to ReferenceType.NONE which means that every requestor will get its own instance of the messages class:

@Message(referenceType=ReferenceType.NONE)

@Translation annotation

After the messages class is created and connected to the resource bundle, it can simply be used by injecting the Messages instance with the @Translation annotation:

@Inject
@Translation
private Messages messages;
...
myLabel.setText(messages.label_message);

Compared with the TranslationService that is already available in Eclipse 4, you are injecting your Messages instance without the need to know the contributionURI of the containing bundle or the key of the translation you are requesting.

@PostConstruct annotation

With the latest implementation it is possibility to add methods annotated with @PostConstruct. Following the dependency injection rules, this method will be executed after the Messages instance is created. This allows you to use placeholders like with using the constant based NLS.bind() solution. Guess you have a property that looks like this:

my.label.message = {0} says {1}

You could initialize this property by adding a method annotated with @PostConstruct to your Messages class.

public class Messages {
	public String my_label_message;

	@PostConstruct
	public void format() {
		my_label_message = MessageFormat.format(
			my_label_message, "Dirk", "Cool");
	}
}

Note that the method annotated with @PostConstruct in the Messages class does not support parameters as it doesn’t support dependency injection at that place. This is quite the default behaviour when thinking of @PostConstruct in the JavaEE context. As @PostConstruct in Eclipse 4 generally supports dependency injection, we need to mention that in this special case it is not supported.

A small but personally huge improvement in properties key handling is the handling of the dot as property key separator. Using the constant based solution you need to use a separator that is valid within a Java variable, e.g. the underscore. This is because the dot is not valid as part of a field name and the binding from key to constant is directly without transformation. The new message extension at least supports the dot as key separator within the properties. So the properties can look like you know them from other Java frameworks. Of course you still can’t use it as separator in the fields. But as you can see in the example above, using the dot in the properties file is possible when using the underscore as separator in the Messages field.

Links:

New Message Extension Update

This entry was posted in Dirk Fauth, Eclipse, OSGi. Bookmark the permalink.

6 Responses to Eclipse Internationalization Part 2/4 – New Message Extension by Dirk Fauth and Tom Schindl

  1. Uwe says:

    Great job, guys! This is a really nice approach to multi-language support, I think.

    What would be your preferred solution for pure Equinox applications (no E4)?

  2. Tom Schindl says:

    @Uwe: What we are using here is the core.di support coming with e4 but you don’t have to use e4 (the UI part) to make use of those core bundles.

    The question is are we talking about server applications or client applications? The biggest obstacle to make use of it is that this stuff is only part of the tools repo so it is not easily consumeable.

    Finally I think one could implement a very similar thing using Google Guice

  3. Uwe says:

    Thanks for your quick reply, Tom.

    I was thinking about a client-/server application in which the server comes without any E4 features. One would like to use the same internationalisation mechanism for client and server, of course.

  4. I guess it’s too late but I created a translation/i18n system which looks like this:

    public final static I18nMessage hello( String name ) {
    return new I18nMessageBuilder().type( Messages.class ).id( “hello” ).args( name ).build();
    }

    So what does that mean? First of all, all messages in the system are of type I18nMessage (no string literals). Message arguments are type and order safe (you can’t mix them up). But most importantly, internal code doesn’t have to load bundles at all.

    Instead, messages bubble up to the UI/interface layer where a translation service can figure out how to turn the I18nMessage into a readable text. This means templates can come from a resource bundle, XML, a database, or a mix of all three.

    Additional bonuses: Unit tests are independent from the current locale, they run fast and reliable (because the message itself never changes). A high level configuration component can decide how to locate and load templates and format messages. Formatters can be injected into the system.

    I’m using an Xtext based DSL to create all necessary Java classes and property files from a single source file.

  5. Uwe says:

    @Tom

    Let me add that it would be great if this thing was available outside of the tools repo. It’s a bit annoying for consumers to have to introduce a dependency to SWT when they want to use this mechanism.

  6. Marian says:

    Hi, I have observed an disturbing fact while moving an OSGi application from Eclipse 3.7 to Eclipse 4.2.2:
    – With Eclipse 3.7, Equinox mirrows perfectly the system’s locale, e.g. ‘de_DE’
    – With Eclipse 4.2.2, Equinox results to ‘de’, skipping the country part

    Any sugguestions? I can prove that with a simple JUnit test:

    public class Test {
    @Test
    public void test() { System.out.println(Locale.getDefault());
    }
    }
    Once executed with and once without ‘Plugin Environement’ in Eclipse. If I switch back to my old Eclipse 3.7 target platform I alywas get my wanted ‘de_DE’.

    I hope someone could give me a hint because I could not find any help with google.

Comments are closed.