Software localization

The Java i18n Guide You’ve Been Waiting For

Learn how to internationalize in Java with the Locale class, and create a toolkit of services you can use in your future Java i18n projects!
Software localization blog category featured image | Phrase

As you may already know, the default supported i18n services exposed by the Java Development Kit (JDK) is the Locale class. In fact, we already have a beginner's tutorial on internationalization in Java, as well as an article on internationalization with Java Locale. In this Java i18n guide, we will build on that knowledge and walk you through the Java localization process by creating a small survey application. All the code examples we're using are hosted on GitHub.

Our Demo App

Our demo app is a command-line application that asks a series of pre-compiled questions; аs soon as the survey is completed, they get printed in a complete overview.

Here’s what it looks like when running it from the command line for English and Greek:

Your Name: ​Theo

Your Age: ​20

Your Email: ​email@gmail.com

What is your favourite Javascript Framework: ​React

What is your favourite fruit: ​Apples

What is your favourite movie: ​Gattaca

SURVEY OVERVIEW

==============

Name: Theo

Email: email@gmail.com

Age: 20

What is your favourite Javascript Framework: React

What is your favourite fruit: Apples

What is your favourite movie: Gattaca

Press any key to abort [_]: ​
Το όνομα σας: ​Θεοφάνης

Ηλικία: ​30

Η ηλεκτρονική διέυθυνση σας: ​email@email.com

Ποιό είναι το αγαπημένο σας πρωινό: ​Ομελέτα

Ποιό είναι το αγαπημένο σας φρούτο: ​Μήλα

Ποιά είναι το αγαπημενη σας ταινία: ​Gattaca

SURVEY OVERVIEW

==============

Το όνομα σας: Θεοφάνης

Η ηλεκτρονική διέυθυνση σας: email@email.com

Ηλικία: 30

Ποιό είναι το αγαπημένο σας πρωινό: Ομελέτα

Ποιό είναι το αγαπημένο σας φρούτο: Μήλα

Ποιά είναι το αγαπημενη σας ταινία: Gattaca

Πατήστε οποιοδήποτε πλήκτρο για να φύγεται: ​

By the time we complete this tutorial, we'll accomplish the following:

  • Installing and setting up the library dependencies,
  • Determining the default locale,
  • Determining the list of available locales,
  • Handling plural messages.

Starting Code

In the beginning, we are going to create an empty Java + Gradle project and import the dependencies. We are going to use the text-io library for handling the interactive part of it (reading input, passwords, and values).

Here is the file tree view of the initial project:

.

├── build.gradle

├── gradle

│   └── wrapper

│       ├── gradle-wrapper.jar

│       └── gradle-wrapper.properties

├── gradlew

├── gradlew.bat

├── settings.gradle

└── src

    ├── main

    │   ├── java

Add this is the text-io dependency inside the build.gradle file:

dependencies {

    testCompile group: 'junit', name: 'junit', version: '4.12'

    compile 'org.beryx:text-io:3.4.1'

}

Run the build task to compile the application resolving any dependencies:

./gradlew build

To run the application, we just need to call the run task:

./gradlew run

Determining Default and Available Locales

As a starting point, it would be useful to get the current (or default) locale of the JVM, so that we don't have to use an additional parameter to get it every time. Using the Locale.getDefault() static method, we can get the default JVM locale, as defined in the system properties. In case this method does not match a supported locale, though, we should best fallback to a default.

Locale defaultLocale = Locale.getDefault();

We also need to know the list of available locales that the JVM supports during runtime. We can use the following method:

Locale[] availableLocales = Locale.getAvailableLocales();

Next, we need to set the current locale for the operations when we request to do the review in a different language:

Locale.setDefault(Locale.forLanguageTag("el"));

Given the above examples, we can create a small service class for those operations. Create a new file in the following location, and add this code:

src

├── main

│   ├── java

│   │   └── services

│   │       └── I18n.java

package services;

import java.text.MessageFormat;

import java.util.Arrays;

import java.util.Locale;

import java.util.ResourceBundle;

public final class I18n {

    private final static String MESSAGES_KEY = "messages";

    private I18n() {

    }

    private static ResourceBundle bundle;

    public static Locale getLocale() {

        Locale defaultLocale = Locale.getDefault();

        return defaultLocale;

    }

    public static boolean isSupported(Locale l) {

        Locale[] availableLocales = Locale.getAvailableLocales();

        return Arrays.asList(availableLocales).contains(l);

    }

    public static void setLocale(Locale l) {

        Locale.setDefault(l);

    }

    public static String getMessage(String key) {

        if(bundle == null) {

            bundle = ResourceBundle.getBundle(MESSAGES_KEY);

        }

        return bundle.getString(key);

    }

    public static String getMessage(String key, Object ... arguments) {

        return MessageFormat.format(getMessage(key), arguments);

    }

}

Main Application Code

Now that we have the basic services sorted, we can create the main body of the program. We need to have one class that creates the survey's list of questions. The questions will have to be translated into the supported locale list. Here is the gist of the code:

import org.beryx.textio.TextIO;

import org.beryx.textio.TextIoFactory;

import org.beryx.textio.TextTerminal;

import services.I18n;

import java.time.Month;

import java.util.Locale;

import java.util.ResourceBundle;

import java.util.function.Consumer;

public class Survey implements Consumer<TextIO> {

    public static void main(String[] args) {

        TextIO textIO = TextIoFactory.getTextIO();

        Locale currentLocale = I18n.getLocale();

        String language = currentLocale.getLanguage();

        String country = currentLocale.getCountry();

        if (args.length == 1) {

            language = args[0];

        } else if (args.length == 2) {

            language = args[0];

            country = args[1];

        }

        var locale = new Locale(language, country);

        if (!I18n.isSupported(locale)) {

            System.err.println("Specified Locale is not supported: " + locale.toString());

        }

        I18n.setLocale(locale);

        new Survey().accept(textIO);

    }

    @Override

    public void accept(TextIO textIO) {

        TextTerminal<?> terminal = textIO.getTextTerminal();

        String name = textIO.newStringInputReader()

                .read(I18n.getMessage("Username"));

        int age = textIO.newIntInputReader()

                .withMinVal(13)

                .read(I18n.getMessage("Age"));

        String email = textIO.newStringInputReader()

                .read(I18n.getMessage("Email"));

        String question1 = textIO.newStringInputReader()

                .read(I18n.getMessage("Question1"));

        String question2 = textIO.newStringInputReader()

                .read(I18n.getMessage("Question2"));

        String question3 = textIO.newStringInputReader()

                .read(I18n.getMessage("Question3"));

        terminal.println("\n\nSURVEY OVERVIEW");

        terminal.println("==============\n");

        terminal.printf("%s: %s\n", I18n.getMessage("Username"), name);

        terminal.printf("%s: %s\n", I18n.getMessage("Email"), email);

        terminal.printf("%s: %d\n", I18n.getMessage("Age"), age);

        terminal.printf("%s: %s\n", I18n.getMessage("Question1"), question1);

        terminal.printf("%s: %s\n", I18n.getMessage("Question2"), question2);

        terminal.printf("%s: %s\n", I18n.getMessage("Question3"), question3);

        textIO.newStringInputReader().withMinLength(0).read(I18n.getMessage("Abort"));

        textIO.dispose();

    }

}

Let's walk through this example code.

First, we try to determine the locale of the application. We use the I18n.getLocale() and then we try to check if it is in the list of available locales. We, then, create a new Locale object and set is as default using the I18n.setLocale(locale)

Next, we call the Survey action that will ask a series of questions in the respective language. The question keys are all the same, so we don't have to change anything there afterwards.

The Translation Messages

Now, in order to make our program work, we need to define the translation messages.

First, we create property files with the following naming scheme: messages_[LANGCODE]_[COUNTRY_CODE]. For example:

  • messages_en_US.properties
  • messages_de_DE.properties

Create two files in the following location:

src

│   ├── messages_el.properties

│   └── messages_en_GB.properties

Then, add the respective translations:

en_US:

Username=Your Name

Age=Your Age

Email=Your Email

Question1=What is your favourite Javascript Framework

Question2=What is your favourite fruit

Question3=What is your favourite movie

Abort=Press any key to abort

el:

Username=Το όνομα σας

Age=Ηλικία

Email=Η ηλεκτρονική διέυθυνση σας

Question1=Ποιό είναι το αγαπημένο σας πρωινό

Question2=Ποιό είναι το αγαπημένο σας φρούτο

Question3=Ποιά είναι το αγαπημενη σας ταινία

Abort=Πατήστε οποιοδήποτε πλήκτρο για να φύγεται

Plurals

Now, if you are wondering how to add plural messages, look no further. We can use the ChoiceFormat Class to render a message depending on some plural rules.

First, we need a base pattern message with placeholders for each locale in place:

QuestionLeftPattern=You have {0} left

Then we need to add messages for each cardinality:

OneQuestion=one question

MultipleQuestions={1} questions

Next, we need to create the ChoiceFormat object that will match a number to a message:

double[] questionLimits = {1,2};

String [] questionStrings = {

        I18n.getMessage("OneQuestion"),

        I18n.getMessage("MultipleQuestions")

};

ChoiceFormat choiceForm = new ChoiceFormat(questionLimits, questionStrings);

Here, the questionLimits match a number with a message. For example, when we have one question left, it will match the OneQuestion message key. If we have 2 or more, it will match the MultipleQuestions key.

Next, we need to get the message pattern and apply it into a MessageFormat object. We add the following methods to the I18n class:

private static MessageFormat messageForm = new MessageFormat("");

...

public static void setLocale(Locale l) {

    Locale.setDefault(l);

    messageForm.setLocale(l);

}

public static void applyPattern(String pattern, ChoiceFormat choiceForm) {

    messageForm.applyPattern(pattern);

    Format[] formats = {choiceForm, NumberFormat.getInstance()};

    messageForm.setFormats(formats);

}

public static String getPatternMessage(Object[] messageArguments) {

    return messageForm.format(messageArguments);

}

Then, we call this method in the main Survey:

String pattern = I18n.getMessage("QuestionsLeftPattern");

I18n.applyPattern(pattern, choiceForm);

Now, every-time we need to print how many questions are left we simply do:

terminal.println(I18n.getPatternMessage(new Object[]{3, 3})); // Prints "Έχεται 3 ερωτήσεις ακόμα"

terminal.println(I18n.getPatternMessage(new Object[]{1, 3})); // Prints "Έχεται μιά ερωτήση ακόμα"

terminal.println(I18n.getPatternMessage(new Object[]{2, 2})); // Prints "Έχεται 2 ερωτήσεις ακόμα"

Another – more modern – way to use plurals is with the ICU4j package. Let us have a look at it...

First, we include this into the dependency list:

dependencies {

    testCompile group: 'junit', name: 'junit', version: '4.12'

    compile 'org.beryx:text-io:3.4.1'

    compile group: 'com.ibm.icu', name: 'icu4j', version: '67.1'

}

Then, you import the MessageFormat class in the main application:

import com.ibm.icu.text.MessageFormat;

You need to change the QuestionsLeftPattern key to:

QuestionsLeftPattern="You have {0, plural, one{one question}other{# questions}} left"
QuestionsLeftPattern="Έχεται {0, plural, one{μία ερώτηση}other{# ερωτήσεις}} ακόμα"

You, then, need to create a MessageFormat object and pass this pattern:

String pattern = I18n.getMessage("QuestionsLeftPattern");

MessageFormat mf = new MessageFormat(pattern);

To render the plural string, we need to create a StringBuffer a FieldPosition object and an Object[] with the number we want to localize:

Object objectsToFormat[] = {3};

FieldPosition fp = new FieldPosition(1);

StringBuffer sb = new StringBuffer();

Then, we can use the .format method passing the three objects and print the message:

sb = mf.format(objectsToFormat, sb, fp);

terminal.println(sb.toString()); // Prints "You have 3 questions left" in English and "Έχεται 3 ερωτήσεις ακόμα" in Greek

Now, we are ready to run the example program.

Running the Example Program

We can run the example program with java as:

java Survey <language>

For example:

java Survey el

java Survey

The application will pick up the language passed and open up a new console terminal asking the survey questions:

Translated survey | Phrase

Wrapping It Up

We sincerely hope this Java i18n guide could be of help! Now, if you are looking into possibilities for growth and want to scale your localized app, look no further than Phrase! The leanest and most reliable translation management platform on the market will automate all your i18n tasks and provide a powerful translation UI for your translators. Translations continuously sync to your development environment(s) and work seamlessly with your app(s). Phrase provides a flexible API, powerful web console, and many advanced i18n features: OTA translations, branching, machine translation, and more. Take a look at all of Phrase's products, and sign up for a free 14-day trial.

If you are still hungry for knowledge, you might want to learn how to use Java i18n for the Spring Framework. Check out the following tutorials: