Showing posts with label Locale. Show all posts
Showing posts with label Locale. Show all posts

Tuesday, October 13, 2015

How to create display message in correct form of singular and plural

Putting the message into resource bundle properties allows this message to be translated into other languages. This is not the end of the story because there are messages need to conform to the language's grammar. The most classic example is the singular and plural form in the English message as below:
There are no plugins available for installation.
There is one plugin available for installation.
There are 3 plugins available for installation.
The highlighted part is a numeric variant and the message change its form based on this variant.


Solution 1: Deficient

Bundle.properties:
none.msg = There are no plugins available for installation.
singular.msg = There is one plugin available for installation.
plural.msg = There are {0} plugins available for installation.
Code:
    private static ResourceBundle BUNDLE = ResourceBundle.getBundle("Bundle");

    private static void showMessage(int total) {
        if (total == 1) {
            System.out.println(BUNDLE.getString("singular.msg"));
        } else if (total > 1) {
            System.out.println(
                    MessageFormat.format(BUNDLE.getString("plural.msg"), total));
        } else {
            System.out.println(BUNDLE.getString("none.msg"));
        }
    }

    public static void main(String[] args) {
        showMessage(0);
        showMessage(1);
        showMessage(2);
    }

Result:
There are no plugins available for installation.
There is one plugin available for installation.
There are 2 plugins available for installation.
Summary:
Although the intention is good, but solution 1 is deficient because
  • The showMessage() implementation does more than just showing a message.
  • A message appears in 3 forms which most likely not true for non-English language. Translator has to provide the same message for 3 different keys in the Bundle.properties.


Solution 2: Not good enough

Bundle.properties:
msg = There is {0} plugin(s) available for installation.
Code:
    private static ResourceBundle BUNDLE = ResourceBundle.getBundle("Bundle");

    private static void showMessage(int total) {
        System.out.println(MessageFormat.format(BUNDLE.getString("msg"), total));
    }

    public static void main(String[] args) {
        showMessage(0);
        showMessage(1);
        showMessage(2);
    }

Result:
There is 0 plugin(s) available for installation.
There is 1 plugin(s) available for installation.
There is 2 plugin(s) available for installation..
Summary:
Solution 2 resolved the problems given by solution 1. However, it gives up the English grammar.


Solution 3: Good

Bundle.properties:
msg = There {0, choice, 0#are no plugins|1#is one plugin|1<are {0, number, integer} plugins} available for installation.
Code:
    private static ResourceBundle BUNDLE = ResourceBundle.getBundle("Bundle");

    private static void showMessage(int total) {
        System.out.println(MessageFormat.format(BUNDLE.getString("msg"), total));
    }

    public static void main(String[] args) {
        showMessage(0);
        showMessage(1);
        showMessage(2);
    }

Result:
There are no plugins available for installation.
There is one plugin available for installation.
There are 2 plugins available for installation.
Summary:
The message in the Bundle.properties is a java.text.ChoiceFormat pattern. It can be digested by MessageFormat. With a single message in Bundle.properties, solution 3 is able to meet the requirement perfectly.



References:
http://docs.oracle.com/javase/7/docs/api/java/text/MessageFormat.html
http://docs.oracle.com/javase/7/docs/api/java/text/ChoiceFormat.html

Saturday, October 10, 2015

How to internationalize a display message

The requirement is to display the following message from our Java program.
Your last transaction was on October 11, 2015 at 8:46:03 AM with transaction amount $100.00.
The message has to be able to support the localization process. That means the message could be translated and conform to the expectations of a given user community. The highlighted parts are variances of date, time, and currency that are supplied to the message.

Solution 1: Deficient

Bundle.properties:
msgPart1 = Your last transaction was on
msgPart2 = at
msgPart3 = with transaction amount
Code:
// Message stored as parts in Bundle.properties
ResourceBundle bundle = ResourceBundle.getBundle("Bundle");

// Retrieve message parts from resource bundle
String msgPart1 = bundle.getString("msgPart1");
String msgPart2 = bundle.getString("msgPart2");
String msgPart3 = bundle.getString("msgPart3");

// Variants
double trxnAmount = 100;
Date trxnDateTime = new Date();

// Applying formating based on user default locale
DateFormat shortDateFormat = SimpleDateFormat.getDateInstance(DateFormat.LONG);
DateFormat shortTimeFormat = SimpleDateFormat.getTimeInstance(DateFormat.MEDIUM);
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance();

// Construct message
StringBuilder message = new StringBuilder();
message.append(msgPart1).append(' ').append(shortDateFormat.format(trxnDateTime)).append(' ')
        .append(msgPart2).append(' ').append(shortTimeFormat.format(trxnDateTime)).append(' ')
        .append(msgPart3).append(' ').append(currencyFormat.format(trxnAmount))
        .append('.');

// Print and display the message
System.out.println(message.toString());

Result:
Your last transaction was on October 11, 2015 at 11:54:24 AM with transaction amount $100.00.
Summary:
Solution 1 is deficient because
  • The message is split into a few parts and this make translation unfriendly.
  • All the message parts are required to be concatenated and it is awkward to append space and period to construct to the whole message.
  • Verbose line of code to construct the message.

Solution 2: Good

Bundle.properties:
msg = Your last transaction was on {0} at {1} with transaction amount {2}.
Code:
    // Message stored in Bundle.properties
    ResourceBundle bundle = ResourceBundle.getBundle("Bundle");

    // Retrieve message parts from resource bundle
    String msg = bundle.getString("msg");

    // Variants
    double trxnAmount = 100;
    Date trxnDateTime = new Date();

    // Applying formating based on user default locale
    DateFormat shortDateFormat = SimpleDateFormat.getDateInstance(DateFormat.LONG);
    DateFormat shortTimeFormat = SimpleDateFormat.getTimeInstance(DateFormat.MEDIUM);
    NumberFormat currencyFormat = NumberFormat.getCurrencyInstance();
    
    // Collect variants into array
    Object[] arguments = new String[] {
        shortDateFormat.format(trxnDateTime),
        shortTimeFormat.format(trxnDateTime),
        currencyFormat.format(trxnAmount)
    };
    
    // Print and display the message
    System.out.println(MessageFormat.format(msg, arguments));

Result:
Your last transaction was on October 11, 2015 at 11:54:24 AM with transaction amount $100.00.
Summary:
Solution 2 makes use of java.text.MessageFormat. This allows the message to includes the variants in it and hence is translation friendly.

Solution 3: Better

Bundle.properties:
msg = Your last transaction was on {0, date, long} at {0, time, medium} with transaction amount {1, number, currency}.
Explanation:
{0, date, long} : Use the Date Formatter to format the arguments[0] object and apply with DateFormat.LONG style.
{0, time, medium} : Use the Time Formatter to format the arguments[0] object and apply with DateFormat.MEDIUM style.
{0, number, currency} : Use the Currency Formatter to format the arguments[1].

Code:
    // Message stored in Bundle.properties
    ResourceBundle bundle = ResourceBundle.getBundle("Bundle");

    // Retrieve message parts from resource bundle
    String msg = bundle.getString("msg");

    // Variants
    double trxnAmount = 100;
    Date trxnDateTime = new Date();
    
    // Collect variants into array
    Object[] arguments = new Object[] {
        trxnDateTime,
        trxnAmount
    };
    
    // Print and display the message
    System.out.println(MessageFormat.format(msg, arguments));

Result:
Your last transaction was on October 11, 2015 at 11:54:24 AM with transaction amount $100.00.
Summary:
Solution 3 is even better because
  • The formatting jobs are not required in the code because they are delegated to java.text.MessageFormat.
  • The formatting style such as LONG or MEDIUM date could be configured in the properties file instead of hardcoded in the source code.

References:

Friday, May 15, 2015

Locale Evolution

"Locale" is used to performs locale-sensitive operations such as formatting numeric value; it is also used to tailors locale-sensitive information for a user who from different geographical, political, or cultural region. While we could choose which locale to use in our application, but in common, the application locale is based on the computer platform locale. In other words, the application is taking the default locale. This post does not cover every feature of locale. It is about the change of default locale's behavior on Windows platform along with the evolution from Java 6 to 8.

Locale in Java 6

In Java 6, Locale is the only instance that determines the value format as well as the information display. The locale data returned by Locale.getDefault() and the locale data used in NumberFormat.getInstance() in the same.

>> Window platform

Change of the Region and Language - Formats - Format (as shown in the image below), will change the default locale in our application.


NumberFormat format1 = NumberFormat.getNumberInstance();
String formattedValue = format1.format(88000);
System.out.println(formattedValue);
       
NumberFormat format2 = NumberFormat.getNumberInstance(Locale.getDefault());
Number parsedValue = format2.parse(formattedValue);
System.out.println(parsedValue);

No matter what Windows region-format setting we set, the numeric value that is handled by format1 and format2 object in the code snippet above always maintain its value. For example, if the region format is set to Portuguese (Portugal), then the formattedValue is 88.000 ("." is the grouping symbol in Portuguese locale); and the parsedValue is 88000. It is confirmed that, NumberFormat.getNumberInstance() and NumberFormat.getNumberInstance(Locale.getDefault()) behaves the same.

>> Locale provider

However, it is important to be aware that, Java application takes the locale data which is provided in JRE based on the setting above. The locale data is not come from the hosting platform. This is why the customized formats in the Windows Region-Format setting do not apply to the application. In the case where JRE itself does not support the requested locale, then Locale methods will look for the locale data from any installed locale sensitive service provider interfaces (SPIs). This order is fixed.

>> VM options

There is another way to set the application locale. This can be done by setting the language and country VM options as below when launching the Java application. VM options below will make the application to picks up Portuguese (Portugal) locale data from JRE. In this way, the Java application will ignore the Windows Region-Format setting.

-Duser.language=pt -Duser.country=PT

Locale in Java 7

Since Java 7, Locale comes in two categories. Locale.Category.Format is for formatting value; Locale.Category.Display is for displaying texts. The locale data returned by Locale.getDefault() is display locale. The locale data used in NumberFormat.getInstance() is format locale.

>> Window platform

Change of the Region and Language - Formats - Format will change the format locale data; Change of the Region and Language - Administrative - Change System Locale (as shown in the image below) will change the display locale data. In others words, an Java application could have two different default locale data at the same time.


Running the same code snippet again with Portuguese (Portugal) region format setting and English (US) system locale, we now get the different formatted value for both format objects.

// Region-format: Portuguese (Portugal)
NumberFormat format1 = NumberFormat.getumberInstance(); // taking format locale (Portuguese)
String formattedValue = format1.format(88000);
System.out.println(formattedValue); // output: 88.000
       
// System locale: English (US)
NumberFormat format2 = NumberFormat.getNumberInstance(Locale.getDefault());  // taking display locale (English)
Number parsedValue = format2.parse(formattedValue);
System.out.println(parsedValue); // output: 88

The value formattedValue and parsedValue has significant difference even thought they derived from the same numeric value. NumberFormat.getNumberInstance() and NumberFormat.getNumberInstance(Locale.getDefault()) is no longer behaves the same. The resolution is just let the format object to use its default format locale, and only supply locale data to it if you know what you are doing.

>> Locale provider

Same as Java 6.

>> VM options

Same as Java 6. But to be specific, setting the language and country VM options to, let's said Portuguese (Portugal) will make the format locale and display locale become Portuguese basis.

Locale in Java 8

Same as Java 7.

>> Window platform

Same as Java 7.

>> Locale provider

Besides JRE and SPI, there are two new locale data providers supported in Java 8.
  • Common Locale Data Repository (CLDR): Released by Unicode Consortium "to support the world's languages, with the largest and most extensive standard repository of locale data available.
  • Host: The current customized locale setting of the underlying operating system.

>> VM options

The language and country VM options effect are same as Java 7. Moreover, the order of locale providers searching is not fixed and could be specified using the VM options as below.

-Djava.locale.providers=SPI,HOST,CLDR,JRE

Calendar calendar = Calendar.getInstance();
DateFormat df = SimpleDateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL); 
System.out.println(df.format(calendar.getTime()));

Run the code snippet above with different locale provider VM options on Windows platform will get the result as below:

HOST (Customized to MMMM dd, yyyy):- February 27, 2015 12:15:02 AM
JRE:- Friday, February 27, 2015 12:13:10 AM SGT
CLDR:- Friday, February 27, 2015 12:13:42 AM Singapore Standard Time

Related posts:
Text Collation

References:
http://stackoverflow.com/questions/24616848/get-number-format-from-os http://docs.oracle.com/javase/8/docs/technotes/guides/intl/enhancements.8.html http://docs.oracle.com/javase/8/docs/api/java/util/spi/LocaleServiceProvider.html http://docs.oracle.com/javase/7/docs/api/java/util/Locale.html