Sunday, March 10, 2013

Localization in Java can be easy

It’s being a while since my last post. Recently I wrote small localization library for Java. It’s still in development but I wanted to share main ideas.

JDK localization capabilities are comprehensive enough but not easy to use and it misses very important concept of Plural Rules. IMHO GWT is the only Java framework I know which did localication right. I decided to bring these ideas to server side and created ginger. Here is the list of it’s core ideas:

  • Ease of use
  • Compatibility with JDK localization features
  • Type safety
  • Plural rules support
  • Popular libraries and frameworks support

Let’s get to some examples.

This is the simplest way to initialize library:

Localization localization = new LocalizationBuilder()
        .withResourceLocation("classpath:demo.properties")
        .build();

This code creates instance of Localization which I will use in further examples. There are additional methods in LocalizationBuilder for more flexible configuration: locale resolvers, caching options, resource loaders and additional properties files.

Constants

demo_en.properties (standard Java properties file except that it supports UTF-8 encoding without additional tranformation):

day=day
week.start.day=0
# Comma separated list
week.days=Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday

Now type safety comes to the stage. We need to define interface which extends Localizable interface and defines methods corresponding to constansts defined in the properties file.

public interface LocalizableConstants extends Localizable {
    String day();
    Integer weekStartDay();
    List<String> weekDays();
}

Camel case method names are transformed into dot separated property keys. Method can return any primitive wrapper, String, List or Map.

In order to start using localized constants corresponding to the current locale we need to get instance of LocalizableConstants. Note, that default LocaleResolver implmentation uses Locale.getDefault(). Real application would do that only once and then reuse the instance. Localizable instance is thread-safe and independent of current locale.

Getting localized values is really easy:

System.out.println(constants.day());
System.out.println(constants.weekStartDay());
System.out.println(constants.weekDays());

Output:

day
0
[Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]

Messages

The main difference between constants and messages is that messages have parameterized values in text. ginger’s message is backed by JDK’s MessageFormat with additional enhancements.

demo_en.properties with messages:

planet.event=At {1,time} on {1,date}, there was {2} on planet {0,number,integer}.

Property value follows MessagesFormat syntax:

  1. {0,number,integer} number formatted as integer. 1st format parameter
  2. {1,time} time part of a Date. 2nd format parameter
  3. {1,date} date part of a Date. 2nd format parameter
  4. {2} default format and depends on parameter type. 3rd format parameter

Similar to constants messages should be defined as methods on Localizable interface. With the only difference that methods for messages should have parameters.

public interface LocalizableMessages extends Localizable {
    String planetEvent(int planet, Date eventDate, String event);
}

Message method supports the same set of parameter types which is supported by MessageFormat. As an additional benefit it also supports all Joda Time types.

Instance of LocalizableMessages can be created similar to example with constants. Furthermore they are no different to constants and could be defined together.

And this how we can create message with specified parameters:

int planet = 7;
String event = "a disturbance in the Force";
System.out.println(messages.planetEvent(planet, new Date(), event));

Output:

At 8:10:33 PM on Mar 9, 2013, there was a disturbance in the Force on planet 7.

As I said, it’s easy!

Plurals

English speaking people would probably say: “What’s the matter? 1 - single, 2..n - plural”. Ok, but it does not work for all languages. Just look at list of languages and plural rules. For examples, in Russian there are three forms:

  • one - n mod 10 is 1 and n mod 100 is not 11
  • few - n mod 10 in 2..4 and n mod 100 not in 12..14
  • many - n mod 10 is 0 or n mod 10 in 5..9 or n mod 100 in 11..14

Looks pretty bad, isn’t it? And it’s not the worst case. There is ICU project which has C++ and Java implementation and seems to be the most comprehensive internationalization library. The downside is it’s not easy to use and it can be an overkill for small and medium size projects.

This was original reason why I decided to create ginger. Make pluralization simple.

Let’s consider the following demo_en.properties. This time with messages for different plural forms:

users.found[0]=No users found
users.found[one]=Found one user
users.found[other]=Found {0} users

Parameters in square brackets have plural form selectors. Different languages may have different selectors. one, other and many are the most commonly used selectors. Exact list of selector for any particular language can be found at unicode.org. 0 and 1 are special selectors and can be used independently of language for exact count match.

Parameters used for plural form selection should be annotated with @PluralCount annotation.

public interface LocalizableMessages extends Localizable {  
    String usersFound(@PluralCount int usersCount);
}

And now it can be used in the actual code.

System.out.println(messages.usersFound(0));
System.out.println(messages.usersFound(1));
System.out.println(messages.usersFound(2));

Displays messages based on passed plural count:

No users found
Found one user
Found 2 users

Selectors

Selectors are similar to plurals. You might need to choose message based on something besides a count. For example, you want to display different messages based on person’s gender.

public enum Gender {
    FEMALE,
    MALE
}

And you have messages corresponding to FEMALE and MALE Gender:

present.sent[FEMALE]=She sent you a present
present.sent[MALE]=He sent you a present

This how method definition for message with gender selector looks like.

public interface LocalizableMessages extends Localizable {
	String presentSent(@Select Gender gender);
}

@Select annotation indicates that this parameter is a selector.

The following code prints messages for MALE and FEMALE Gender.

System.out.println(messages.presentSent(Gender.MALE));
System.out.println(messages.presentSent(Gender.FEMALE));

As expected output displays messages based on Gender parameter:

She sent you a present
He sent you a present

This was brief overview of ginger’s core features. I also published above examples on Github. I intentionally didn’t cover Spring and JSP integration in this post and will dedicate separate post for them.

Should you have any questions or feature requests feel free to create Github issue. I am also planning to add more detailed information on the project page.

You can add it to you project using Maven:

<dependency>
    <groupId>com.vityuk</groupId>
    <artifactId>ginger-core</artifactId>
    <version>0.2.1</version>
</dependency>

All Maven artifacts including Spring and JSP integration are available on public Maven repositiories

blog comments powered by Disqus