Module io.github.mmm.nls


module io.github.mmm.nls
Provides advanced native language support.

Native Language Support (NLS)


Applications that should be used by people all over the world need native language support (NLS). The developers task is the internationalization (i18n) where the application has to be written in a way that the code is (mostly) independent from locale-specific informations. This is a challenging task that affects many aspects like GUI-dialogs as well as all text-messages displayed to the end-user. The NLS provided here only addresses the internationalization of text-messages in a way that allows localization (l10n) to the users locale.

The Problem

Java already comes with great i18n support. But IMHO there are some tiny peaces missing to complete the great puzzle of NLS:
There is almost no support if an application needs NLS that is handling multiple users with different locales concurrently (e.g. a web-application).
You will typically store your messages in a ResourceBundle. Now if you store the technical key of the bundle in a message or exception the receiver needs the proper ResourceBundle to decode it or he ends up with a cryptic message he can NOT understand (e.g. as illustrated by the screenshot).
On the other hand you need to know the locale of the receiver to do the l10n when creating the message or exception with the proper text. This may lead to sick design such as static hacks. Also if you have to translate the text at the creation of the message every receiver has to live with this language. Especially for logging this is a big problem. An operator will be lost in space if he gets such logfiles:

 [2000-01-31 23:59:00,000][ERROR][n.s.m.u.n.a.MasterService] The given value (256) has to be in the range from 0 to 100.
 [2000-01-31 23:59:01,000][WARN ][n.s.m.u.n.a.MasterService] Der Benutzername oder das Passwort sind ungültig.
 [2000-01-31 23:59:02,000][ERROR][n.s.m.u.n.a.MasterService] 文件不存在。
 [2000-01-31 23:59:03,000][FATAL][n.s.m.u.n.a.MasterService] ข้อผิดพลาดที่ไม่คาดคิดได้เกิดขึ้น
 

The Solution

The solution is quite simple:
We simply bundle the message in default language together with the separated dynamic arguments in one container object that is called NlsMessage. For exceptions there is additional support via ApplicationException. Here is an example to clarify the idea of NlsMessage:
The i18n message is "Hi {name}! How are you?" and the dynamic argument is the users name e.g. "Lilli". Now if we store this information together we have all we need. To get the localized message we simply translate the i18n message to the proper language and then fill in the arguments. If we can NOT translate we always have the message in default language which is "Hi Lilli! How are you?".
But how do we translate the i18n message to other languages? The answer is quite easy:

NlsBundle

The recommended approach is to create a final class derived from NlsBundle. For each message you define a method that takes the arguments to fill in and returns an NlsMessage:
 package foo.bar;

 public final class NlsBundleExample extends NlsBundle {

   public static final NlsBundleExample INSTANCE = new NlsBundleExample();

   NlsMessage messageSayHi(String name) {
     return create("messageSayHi", "Hi {name}! How are you?", NlsArguments.ofName(name));
   }

   NlsMessage errorLoginInUse(String login) {
     return create("errorLoginInUse", "Sorry. The login '{name}' is already in use. Please choose a different login.", NlsArguments.ofName(login));
   }
 }
 
From your code you now can do this:
 String userName = "Lilli";
 NlsMessage msg = NlsBundleExample.INSTANCE.messageSayHi(userName);
 String text = msg.getMessage());
 String textDefault = msg.getLocalizedMessage());
 String textDe = msg.getLocalizedMessage(Locale.GERMAN));
 
For the error message create an exception like this:
 public class LoginAlreadyInUseException extends ApplicationException {

   public LoginAlreadyInUseException(String login) {
     this(null, login);
   }

   public LoginAlreadyInUseException(Throwable cause, String login) {
     super(NlsBundleExample.INSTANCE.errorLoginInUse(login), cause);
   }
 }
 
For further details see NlsBundle.

For localization you can create property files with the translations of your NLS-bundle. E.g. l10n/foo/bar/NlsBundleExample_de.properties with this content:
 messageSayHi = Hallo {name}! Wie geht es Dir?
 errorLoginInUse = Es tut uns leid. Das Login "{login}" ist bereits vergeben. Bitte wählen Sie ein anderes Login.
 
Please note that the path to the localized resource bundle needs to be prefixed with l10n (for localization) and must not be deployed as a java module dues to restrictions of the JPMS.
Unlike the Java defaults, you will use named parameters instead of indexes for parameter expressions in messages. This makes it much easier for localizers as they can understand the context of the parameter. There are even more advanced features such as recursive translation of arguments and choice format type. See NlsMessage for further details. In order to support you with creating and maintaining the localized properties, this solution also comes with the io.github.mmm.nls.sync.NlsSynchronizer.

Conclusion

As we have seen the NLS provided here makes it very easy for developers to write and maintain internationalized code. While messages are created throughout the code they only need to be localized for the end-user in the client and at service-endpoints. Only at these places you need to figure out the users locale (e.g. using org.springframework.context.i18n.LocaleContextHolder).
  • The NlsMessage allows to store an internationalized message together with actual arguments to fill in.
  • The arguments can be arbitrary objects including LocalizableObjects that will be localized recursively.
  • There are powerful ways to format these arguments including variable expressions for optional arguments or plural forms. See NlsMessage for advanced examples.
  • Instead of numbered arguments we support named arguments what makes maintenance of the messages a lot easier. Your localizers will love you for choosing this solution.
  • Resource bundle properties are read in UTF-8 encoding making it easier for localizers as they do not have to escape characters to unicode number sequences.
  • The localization (translation to native language) is easily performed by NlsMessage.getLocalizedMessage(java.util.Locale).
  • For exceptions there is additional support via ApplicationException.