Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

l18n framework with compiletime checking

I'm currently working on a bigger Java desktop-application and now are at the point where I want to add translations. What bothers me about the l18n-system is, that it doesn't provide any kind of compiletime checking.

In java's system, you have something like a HashMap, where every localized string has a "Key" and the translated string then is the "Value". This looks something like this (taken from the tutorials example):

Locale currentLocale;
ResourceBundle messages;

currentLocale = new Locale(language, country);

messages = ResourceBundle.getBundle("MessagesBundle", currentLocale);
System.out.println(messages.getString("greetings"));

This works nice if you have a simple/small application. But in a big application with thousands of translated strings, it might so happen that you have a typo in the "Key" and therefore get an empty or wrong string.

With a little luck, the application throws a RuntimeException to tell you about it, but even then it is possible that you don't even come to this point because the wrong "Key" is used in a dialog that might not show up under certain circumstances (say it's an error dialog).

To prevent this from happening, using a system which offers compiletime checking of the used "Keys" would be the better idea. This is for example used in Android, where you specify the Resources in an XML-file which is then indexed and mapped to a class (including the "Keys" to use). This way, you get something like this (from the Android Docs):

// Set the text on a TextView object using a resource ID
TextView msgTextView = (TextView) findViewById(R.id.msg);
msgTextView.setText(R.string.hello_message);

If you have a typo in that "key", you'll get an error at compile-time (also you have "auto-completion" from your IDE).

Now, to make something like this work, you'll need a little tool/script that does the indexing part and generates the Resource-class (R.java). In Android, the Eclipse-plugin (or your IDE in general) does that for you.

My question now is: Is there already a system that that I can use for normal desktop-applications in Java? Or am I horribly wrong with what I'm saying?

like image 701
Lukas Knuth Avatar asked Apr 20 '12 15:04

Lukas Knuth


3 Answers

There is a fairly simple solution to this problem. First, don't use magic strings as codes, define constants and refer to them. So you change..

messages.getString("greetings");

to

messages.getString(I18.GREETINGS_CODE);

and have the corresponding class;

public class I18 {

  public static final String GREETINGS_CODE = "greetings";

}

Next write a test case where each code in your I18 class is looked up in each language resource file. If any resource file is missing the code, then fail the test. Its not compile time, but your project will fail its test if you have any problems.

like image 86
Qwerky Avatar answered Nov 13 '22 01:11

Qwerky


I have seen few fairly large enterprise applications that use standard Resource Bundles and trust me, this is not a problem.

There are few success factors:

  1. Resource Organization
  2. Resource Helpers
  3. Resource Factories
  4. Key naming strategy

Ad 1. Your issues will be especially visible if you decide on using single Resource Bundle. Do not go this way. There is no way to maintain large, single file. You will face problems with key duplication, key naming and so. Besides, with large applications it is common to have few people working on the same set of files at the same time. In case of Resource Bundles it means a lot of merging and getting each other in the way. This is the problem in the regular programming team, but trust me with Localizers it is even worse.
What you need is multiple, small resource files. Depending on the application scale, it might be one file per module, or even one file per dialog. How to split the files depends on the actual application, but what I recommend is not having resource file longer than two screens of text.
With large number of files, the problem is how to organize them - put to valid directories. What I'd suggest is to separate resource files from the source code (place them in the separate directory called - for example - L10n). Under the resource files root it is good to have language folders (so that is easy to separate the translations for each target language), although it would require messing with ResourceBundle.Control class. Then under the language root you would create subdirectories for each module. Under the module root you would have either separate subdirectories (with very large application), or you will place your Resource Bundles directly.

Ad 2. There is no way to use ResourceBundle class directly. One of the problems is MissingResourceException thrown if there is no such key in the file(s), or there is no file you are looking for. Instead, you would probably like to see (on the User Interface) that something went wrong, for example the name of the key between exclamation marks (!the.key.does.not.exist!) - this is what Eclipse's "Externalize Strings" does by default.
You might decide on creating Resource Helper which might be either wrapper on ResourceBundle or may derive from it. The choice is yours (although with serious Resource Organization like above the reasonable choice is to derive, as you need to create your own ResourceBundle.Control class). Beside simple translation handling you can add additional functionality to Resource Helper, like handling Message Formatting, Plural Forms, etc.
For now it won't handle your problem of wrong resource key names. You would need actual UI testing - the typos would be visible. In case of using wrong keys (especially for someone who use Copy-Paste Design Pattern ;)) I don't think that there is any technology that would ever resolve the issue. Machines doesn't think. Anyway, you can handle the problems with keys by placing (for example) nested classes with key constants or nested enums. The problem with such solution is, you would need separate Resource Helper for each module, which is not necessary the good idea (especially in large application). It is feasible, but cumbersome. And of course IDE's built-in mechanism for externalizing strings won't work with this solution.

Ad 3. In order to work with large number of resource files, you need to build static Resource Factory (or Factories) that will create Resource Helpers for you. My strategy is to nest a publicly visible enum inside the Factory and have resource files as its constants. Then, I request valid bundle, for example:

ResourceHelper helper = ResourceFactory(
                          ResourceFactory.Bundles.ERRORS, Locale locale);

Ad 4. In order to avoid mess, the team must decide on single resource key naming strategy. What I'd suggest (having one file per module) for key name is to have:

  • dialog or unit
  • descriptive name
  • the context

For example:

  • validation.invalid.username.error
  • validation.enter.pass.label
  • validation.dialog.title
  • menu.main.file.open.item
  • login.dialog.ok.button

The context (label, dialog.title, item, error, message, pattern and so on) is very important for the translators. The translations usually vary depending on the context. This is also the reason why you should not re-use the translations (apart from very common items like common button names "OK", "Cancel", "Abort", "Help" or common menu names and items).

like image 25
Paweł Dyda Avatar answered Nov 13 '22 01:11

Paweł Dyda


I've never heart about such a tool, that can index your resources. Imho, rather unusual requirement. One of the approach - instead of using string literal in your code, move them all to a single class, declare them with static final modifier. Use this constant as a key for your resource map. And it's quite easy to write test, which using reflection will check, that all resources declared in your single class present in resource's file. It's much more easy, then writing code analyser

like image 2
Anton Avatar answered Nov 13 '22 02:11

Anton