Software localization
The Java i18n Guide You’ve Been Waiting For
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:
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: