Proper Translatable Pluralization in .NET With MessageFormat

Introduction

Formatting user-interface messages is hard. This quick tip attempts to ease that pain by resorting to a minimal, cross-platform library for better maintainability.

TL;DR: The library can be found here: https://github.com/jeffijoe/messageformat.net
Scenario

We want to display a message in our UI that indicates how many unread messages a user has.

Example: There are no unread messages for Jeff.

Example: There is one unread message for Bob.

Example: There are 4 unread messages for Joe.

The requirement is proper pluralization, as well as translations in English and Danish.

Background

Let's have a look at a naive implementation for the scenario above.

  1. // Danish version    
  2.   
  3. public string GetUnreadMessagesString_Danish(int unreadMessagesCount, string user) {  
  4.     var message = "Der er";  
  5.     if (unreadMessagesCount == 0)   
  6.     {  
  7.         message += "ingen ulaeste beskedder"  
  8.     } else if (unreadMessagesCount == 1)   
  9.     {  
  10.         message += "kun 1 ulaest besked";  
  11.     } else {  
  12.         message += string.Format("{0} ulaeste beskedder", unreadMessagesCount);  
  13.     }  
  14.     message += string.Format("til {0}", user);  
  15.     return message;  
  16. }  
  17. // Helper    
  18. public string GetUnreadMessagesString(int unreadMessagesCount, string user)   
  19. {  
  20.     // Pseudo code for getting the current language.    
  21.     string currentLanguage = LanguageManager.GetCurrentLanguage();  
  22.     if (currentLanguage == "en")  
  23.     return GetUnreadMessagesString_English(unreadMessagesCount, user);  
  24.     else if (currentLanguage == "da")  
  25.     return GetUnreadMessagesString_Danish(unreadMessagesCount, user);  
  26.     throw new Exception("Not supported!")  
  27. }  
  28. // Usage    
  29. label1.Text = GetUnreadMessagesString(2, "Jeff");

This code is:

  • Hard to maintain
  • Hard to read and understand
  • Hard to get translated externally

We can do better.

A Better Implementation, Using MessageFormat

C, Java, PHP and JavaScript, among others, have this neat utility called MessageFormat. .NET dioes not have one, so I wrote one myself and it is very useful.

It is open-sourced on GitHub at https://github.com/jeffijoe/messageformat.net.

Installing MessageFormat Into Your Project

MessageFormat is available via the NuGet package manager. All you must do, is run:

Install-Package MessageFormat

From the Package Manager Console. Alternatively, you can use the Manage NuGet Packages UI for this.


Figure 1: Package Manager Console

Note: It works with Xamarin as well.

Using MessageFormat to Implement Our Solution

MessageFormat will parse strings and properly format them based on the input given. It does not generate code. This means we can put our actual UI strings in external files and I strongly advise you to do this.

So, for brevity, these are our translation files:

UnreadMessages.en.txt

There {unreadMessagesCount, plural,zero {are no unread messages}one {is one unread message}other {are # unread messages}} for {user}.

UnreadMessages.da.txt

Der er {unreadMessagesCount, plural,zero {ingen ulaeste beskedder}one {kun 1 ulaest besked}other {# ulaeste beskedder}} til {user}.

Notice the format it was written in. I won't go over the specifics, since they can be found in the readme in the GitHub repository.

unreadMessagesCount is the variable, plural is the formatter being used, zero, one and other are the parameters passed to the formatter.

Simply put, it does exactly what the previous, hard-to-maintain code did, except here there's no code, just strings.

  • zero: if the variable equals 0
  • one: if the variable equals 1
  • other: none of the above. The # will be replaced by the variable's value.

Whitespace in the format is ignored. Personally, I think it makes it more readable.

This is how you would use it:

  1. // Load string from an external, maintainable file. Pseudo code!!    
  2. var language = LanguageManager.GetCurrentLanguage();  
  3. var str = File.ReadAllText("UnreadMessages." + language + ".txt");  
  4. // Format it using MessageFormatter. The second parameter is an object     
  5. // or a Dictionary<string, object> containing    
  6. // the parameters.    
  7. label1.Text = MessageFormatter.Format(str, new   
  8. {  
  9.     unreadMessagesCount = 2,  
  10.     user = "Jeff"  
  11. });
Points of Interest

We have now achieved:

  • externalized strings, that can be changed without recompiling the code.
  • no more custom formatting code.

Additionally, there is a select formatter, that works pretty much like a switch statement.

This is what it looks like without MessageFormat:

  1. string message = string.Empty;  
  2. switch (gender)   
  3. {  
  4.     case "male":  
  5.         message += "He likes"  
  6.         break;  
  7.     case "female":  
  8.         message += "She likes"  
  9.         break;  
  10.     default:  
  11.         message += "They like"  
  12. }  
  13. message += " this stuff"; 

And with MessageFormat:

  1. var str = "{gender, select, male {He likes} female {She likes} other {They like}} this stuff.";  
  2. var message = MessageFormatter.Format(str, new   
  3. {  
  4.     gender = "male"  
  5. });

You can read all about it in the project's README.