Thoughts on handling Translations and Views on Source Code

One of my recent java projects was to be used by users with three different languages. We went with the standard java approach of using properties files for messages. In intellij there is decent tool support for that.
However it seems a bit odd to have a strongly typed language and then rely on string keys for text lookup. At some point we introduced enums representing the keys, but as they were not automatically generated they involved a lot of repetition. Also you are never quite sure how many arguments a message needs.

So I had this idea of using interfaces to represent resource bundles. Each message could be represented as a method, with parameters representing the arguments to the placeholders. It would look somewhat like this:

public interface ExampleMessagePool {
    String sayHello(String name);
    String bye(String name);
    String warning();    
}

From the clients point of view it would works as follows:

var factory = new MessagePoolFactory<ExampleMessagePool>(ExampleMessagePool.class);
 
 
ExampleMessagePool english = factory.getLanguageSource("en");
 
assertEquals("Hello Matthieu!", english.sayHello("Matthieu"));
assertEquals("Good bye Felix!", english.bye("Felix"));
assertEquals("Attention!", english.warning());
 
 
ExampleMessagePool french = factory.getLanguageSource("fr");
 
assertEquals("Bonjour Matthieu!", french.sayHello("Matthieu"));
assertEquals("Au revoir Felix!", french.bye("Felix"));
assertEquals("Attention!", french.warning());
 
 
ExampleMessagePool german = factory.getLanguageSource("de");
 
assertEquals("Guten Tag Matthieu!", german.sayHello("Matthieu"));
assertEquals("Auf Wiedersehen Felix!", german.bye("Felix"));
assertEquals("Achtung!", german.warning());

The interesting point here is that I get completion and also a hint as of which arguments a particular message takes. The MessagePoolFactory takes care of creating instances for the respective languages. The next question is obviously, where these messages do come from. One way would be to use annotations:

@MessagePool(languages={"en", "de", "fr"})
public interface ExampleMessagePool {
    @Translations(
            entries = {
                @Entry(key = "en", value = "Hello {0}!"),
                @Entry(key = "de", value = "Guten Tag {0}!"),
                @Entry(key = "fr", value = "Bonjour {0}!")
            }
    )
    String sayHello(String name);
 
    @Translations(
	        entries = {
	            @Entry(key = "en", value = "Good bye {0}!"),
	            @Entry(key = "de", value = "Auf Wiedersehen {0}!"),
	            @Entry(key = "fr", value = "Au revoir {0}!")
	        }
	)
	String bye(String name);
 
    @Translations(
	        entries = {
	            @Entry(key = "en", value = "Attention!"),
	            @Entry(key = "de", value = "Achtung!"),
	            @Entry(key = "fr", value = "Attention!")
	        }
	)
	String warning();
}

This approach is very sexy in so far as it allows to refactor method and parameter names. Also the implementation is trivial not least because java deals with unicode source files. On the other hand making your translators edit the java sources is probably a bit of a challenge. However you could provide a narrow view on that source code. The eclipse platform lends itself to that kind of experiment, so I actually did a bit of spike. Which I am demoing here:

Whilst this is very cool, it’s an entirely static approach. Especially with translations I find it beneficial if they are stored in a database so that they can be edited at run-time. Ideally some key users can then maintain translations, which are part of their domain anyway. Then the interface could be annotated with some sort of GUID, that could later be used at runtime to identify entries in the database:

public interface ExampleMessagePool {
    @Id("21850286-A914-4C13-88BA-75ACA2248B71")
    String sayHello(String name);
 
    @Id("DA3CD6BE-64E8-4501-A674-0789043CBD6F")
    String bye(String name);
 
    @Id("38211D99-BA2A-4572-9C23-976AC64181D8")
    String warning();    
}

This way the refactorability would be preservered. Also tools support could be provided. Annotating elements with GUUIDs seems to be an interesting concept. Just imagine your database mapping being immune to name changes.

There is definitely some more thinking required here, but it strikes me that the default mechanisms in java are very rudimentary. Much less than a talented developer can dream up in a rainy afternoon.
Further developments could include static or, if you go for the runtime approach, dynamic tools to analyse, whether translations are present, or whether there are duplicates in message pools.

This entry was posted in Software Development. Bookmark the permalink.

2 Responses to Thoughts on handling Translations and Views on Source Code

  1. Tiest Vilee says:

    This is not dissimilar to the MS resources solution, and that uses VS (that amazing piece of software that .net developers love and everyone else wonders why) tooling to maintain them.

  2. Szczepan says:

    I like the solution! I love the idea of taking advantage of IDE hints & strong typing. I like the plugin as well! I feel a bit uneasy about the guids but ability to fix messages at runtime might be worth enough. With the guids it feels that it is easier to lose sync between the method name and the message, e.g. sayHello() that prints “Good bye” 🙂

    If many translators are changing the same file it might be impractical in some context. With properties file I could just send the default file to the translator and ask him to provide a *.pl version. What do you think? Would it make sense to split the file somehow across language?

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.