Friday, August 15, 2014

Internationalization the Simple Way



These days, if you are shipping an application or building a web application for an international audience, one of many questions revolves around how to make the application render content for the correct locale. This has often seemed like a daunting task for many developers. The truth is, internationalization is not that difficult in most modern web frameworks. If you are developing a native application, this has often been a bit more challenging, but after reading this blog post there should be no more excuses.

There are several approaches that have been set forward over the years-- many of which are burdensome or overly complex. I base my strategy upon a very simple fact. In most environments, a locale is attached to the current thread so you can pull the locale during run-time. A few years ago, I encountered the Spring framework which provides a LocalizableMessageSource that makes localization intuitive and automatic. However, for those of you in the .NET world, there isn't anything quite so powerful as Spring. I stole this idea from the guys at Spring. It goes like this:

Load a hash table of keys to message values for each locale. Then, load another hash table of locales to message tables. When you want to retrieve a message to display, look up the correct hash table for the current thread's locale. Next, retrieve the correct message from the returned hash table. That's it! Now the only challenge is providing a useful mechanism for persisting and loading initial hash tables. My approach was to create message files for each locale. Each message file contains key value pairs where the same keys are in each file, and the values are the localized messages for each locale. At start up, I simply load and parse the message files and build my data structure.

So that you don't have to do this manually, I have open sourced my solution: LocalizableMessageStorage.
 Here is the code on github.

Here is how to use it assuming an MVC architecture:

public class LoginController
{
  private LocalizableMessageStorage _localizableMessageStorage;
  public LoginController(LocalizableMessageStorage localizableMessageStorage)
  {
    _localizableMessageStorage = localizableMessageStorage;
  }
 
  public LoginModel InitializeModel(bool isFromPasswordReset)
  {
    return new LoginModel()
    {
      LoginButtonLabel = _localizableMessageStorage["lp_loginBtnLabel"],
      LoginButtonReset = _localizableMessageStorage["lp_loginBtnReset"],
      PageTitle = _localizableMessageStorage["lp_pageTitle"],
      UserNameLabel = _localizableMessageStorage["lp_userNameLabel"],
      UserPasswordLabel = _localizableMessageStorage["lp_userPasswordLabel"],
      UserNameValidationMessage = _localizableMessageStorage["lp_userNameValidationMessage"],
      PasswordValidationMessage = _localizableMessageStorage["lp_passwordValidationMessage"],
      ForgotPasswordText = _localizableMessageStorage["lp_forgotPasswordText"],
      ResetPasswordInstructionsText = _localizableMessageStorage["lp_resetPasswordInstructionsText"],
      UserMessage = _localizableMessageStorage["lp_userMessage"]
    };
  }
}

Now your model is filled with internationalized strings. This can be done for any style .NET application--Windows Forms, WPF, ASP.NET, ASP.NET MVC, and so on...-- as long as you use a sane architecture. I have used it successfully in both MVC and MVP architectures. It is thread safe, so it is okay to have the same instance of this in multiple controllers.

Here is an example of how an instance of LocalizableMessageStorage can be instantiated:

string localizationDirectory = HttpContext.Current.Server.MapPath("~/App_Data/Localization");
const string localizationPattern = @"messages([\w-]+)localization\.properties";
 
//default culture is EN-US. If nothing is found, we still want to return english.
CultureInfo fallbackLocale = CultureInfo.CreateSpecificCulture("EN-US");
LocalizableMessageStorage messages = new LocalizableMessageStorage(localizationDirectorylocalizationPatternfallbackLocale);

Now it is ready to use.  Obviously, this is in the context of an ASP.NET application, but you can use it in any context you choose.

Notice the localizationPattern value. It is a regex that tells the system how to parse your message files. It will look in the specified directory for files that match that pattern. Then it will use the group to pull the locale from the file name. There must be one match group that returns the canonical locale name for this to work.

Finally, here is my sample message file:

# ~/App_Data/Localization/messageEN-USlocalization.properties
# EN-US locale file for website.
 
# Login Page
lp_page_title = My Awesome Website - Login
lp_username = Email Address:
lp_password = Password:
lp_login_btn = Log In
lp_login_reset = Reset
lp_validation_username = Email Address is required.
lp_validation_password = Password is required.
lp_login_failure = Invalid Email Address/Password.
lp_forgot_password = Forgot Password?
lp_reset_password_directions = Are you sure you want to reset your password? An email will be sent to you with instructions on how to recover your password.
lp_email_sent= Email Sent.

I then have files exactly like this for each locale with the name for each locale in the file name, the same keys in each file, and the values as the localized messages.

That's all there is to it. So, internationalization is easy; don't be intimidated, and quit making English only websites!!!

Help yourself to the code. If you have improvements, email me and I will get you write access to the repository.