Java i18n (Internationalization) and Localization Tutorial

27 mins read

Java localization and internationalization

As an aspiring developer, finding a quick and reliable way to offer your app to the world can be challenging. It is the reason why you have to learn Java i18n and l10n (internationalization and localization).

Using Java, you already have the tools to detect the user’s locale and translate your app accordingly.

Java Internationalization Is an Important Part of the Development

Internationalization happens at the development stage, and the sooner it’s implemented – the fewer code changes would be required. However, there are some myths about i18n that we’d like to debunk:

  1. Internalization is only about translating the text.
  2. It is challenging to implement it, and the process is burdensome.
  3. Only the project architect has to think about it.

As you might have guessed - these are all false statements. While localization is relatively simple to code even for a junior developer, many bundles and libraries provide ways to translate text from your app and make it culturally correct – this may include date formats, standards of measurement, or even the whole UI design.

But is it demanding to create a new structure for your application? Nobody likes to go back to their code and rewrite everything repeatedly. An agile approach to software development teaches us to implement everything step-by-step, so the best case scenario is to build your project with internalization in mind.

Let’s imagine the process like this:

  • You create an application.
  • The app reads the user’s locale and formats, dates and numbers.
  • It then takes text data from the pre-defined resource and outputs the correct language to the user of our app.

So there must be ways to store all your important messages in one place - you could translate them yourself or automate content updates using a convenient service such as Crowdin.

With Crowdin, you can localize any git branches you want, or you can use CLI/API integrations. Read more life hacks on app translation.

How to Implement Internalization in Java

Java is a powerful programming language that provides convenient ways to work with multi-lingual resources. The best way to do it is what’s called a ResourceBundle.

ResourceBundle Class

ResourceBundle, together with Locale, are the fundamentals of internalization in Java. ResourceBundle class is used to read strings from text files (.properties).

Imagine you could store all your Strings in one file, and the translators do the rest. That’s the job of the .properties file. Every single .properties file serves strings for each locale that your application supports.

Properties usually have a standard naming:


<ResouceBundleName>_<language_code>_<country_code>

for example, MessageBundle_uk_UA for new ResouceBundle

MessageBundle.properties is the source language file

Properties as a source language file

And the strings inside are organized like this:


menu.addCity = Add a new visited city.

menu.chooseAction = Choose an action:

menu.removeCity = Remove city from the tracker.

menu.changeLocale = Change the localization of the app.

menu.exit = Exit the app.

menu.editCity = Edit an existing city.

We should organize all the strings into categories and be clear with their names. When we want to get the string - on the right - in our Java app, we call it using its name - on the left.

Getting Locales List in Java

You may wonder - “I have already created a Resource Bundle, can I assign it to a Locale?” - Yes! That’s where we use the Locale class.

It provides ways to interact with the user’s system, such as using these methods:

(Locale.getDefault()-> returns user’s default locale.

Using the system’s default localization on the app’s startup is a good practice. This method provides the user’s default locale, which you can use to load strings from ResourceBundle without the user even thinking about it.

(String language_tag) -> returns Locale for the language.

As you have already noticed, each Resource Bundle can be created for specific language tags. Sometimes you may need to differentiate between Australian English (en_AU) and American English (en_US) - so you can specify it using this method.

(Locale.getAvailableLocales()-> returns an array of all installed locales.

We use this method when we want to know which locales are installed on the user’s JVM. You parse the list - you know what to serve to the user.

Dates and Numbers

As I mentioned earlier, different countries may have different ways of presenting Dates and Numbers. Let’s assume we have 1 million. In English, it should be formatted as 1,000,000; however, in German, it should be 1.000.000.

To format numbers, Locale-aware creates an instance of NumberFormat class:

NumberFormat nf_en = NumberFormat.getInstance(Locale.ENGLISH);

String number_en = nf_en.format(1000000);

-> 1,000,000

NumberFormat nf_ge = NumberFormat.getInstance(Locale.GERMAN);

String number_ge = nf_ge.format(1000000);

-> 1.000.000

Same thing for dates. To format a date in Java, all you need to do is create an instance of DateFormat class:

DateFormat df_en = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.ENGLISH);

String date_en = df_en.format(new Date());

-> Aug 7, 2022

DateFormat df_ge = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.GERMAN);

String date_ge = df_ge.format(new Date());

-> 07.08.2022

The best part is instead of hard-coding the locale, we can use the user’s locale to format everything according to their culture!

Context

So, you build the app and have all strings in English. Here’s the time you invite translators to your project. A lot of different people will look at the strings you wrote. How to build our resources so they will be translatable?

The concept of context plays a huge role in localization. Without it, some text may be translated poorly, but not because of the translator. Complex resources must have context attached!

If you name a string “string1,” - the translator will have no idea what this string’s for. Here’s a little reading material on why it’s crucial.

Long story short: be sure to correctly name all your strings in the .properties files so that people can know the context of it.

Java i18n Application Example

We have talked about internalization only in theory. Let’s get to the point with our tutorial, where we will use all our new knowledge.

This tutorial will help to create a light Java project and implement internalization. All the project files will be available to fork here.

App Overview:

Our application is a small city visit tracker. I’ll call it simple - City Visit Tracker. The user enters their name and is presented with a few actions:

  1. Add a new city to the list.
  2. Remove a city from the list.
  3. Edit an already added city (we all had this problem remembering the dates, didn’t we?).
  4. Change the localization of the app.
  5. And, finally, exit the app.

I will use Crowdin for the translation of my resources. Using Crowdin - later in the tutorial.

First things first, let’s define the Client class.


public class Client {
   private Locale userLocale;
   private final String username;
   private Map<City, Date> visitedCities;
   public Client(Locale userLocale, String username) {
       this.userLocale = userLocale;
       this.username = username;
   }
   public Locale getUserLocale() {
       return userLocale;
   }
   public void setUserLocale(Locale userLocale) {
       this.userLocale = userLocale;
   }
   public String getUsername() {
       return username;
   }  
   public void addCity(City city, Date dateVisited) {
       this.visitedCities.put(city, dateVisited);
   }
   public void removeCity(City city, Date dateVisited) {
       this.visitedCities.remove(city, dateVisited);
   }
}

We want our user class to store all the cities visited, the user’s name and, most importantly, the user’s preferred locale. I’ve also added Getters and Setters so we can change the info to our needs.

Next, let’s define the City class.


public class City {
   private final Locale cityLocale;
   private final String cityName;
   private final String cityCountry;
   public City(Locale cityLocale, String cityName, String cityCountry) {
       this.cityLocale = cityLocale;
       this.cityName = cityName;
       this.cityCountry = cityCountry;
   }
}

Since we’re allowing the user to put their cities to the list, let’s add all the cityLocale properties to the class, so we could even add the feature to translate city names in the future if we need to.

Resources

Let’s create a new folder with the Message Bundle name and add a few .properties files. I want my app to have English, Polish, and Ukrainian. Therefore, I’m adding four files to the folder (the fourth one is the default).

MessageBundle/

/MessageBundle.properties

/MessageBundle_en_US.properties

/MessageBundle_pl_PL.properties

/MessageBundle_uk_UA.properties

For the sake of simplicity, we will not use the default file now, and I will start populating the resources in the en_US bundle.

So, what do we want in our Resource Bundle?

We need some menu messages:


menu.printAll = Print all cities.

menu.addCity = Add a new visited city.

menu.chooseAction = Choose an action:

menu.removeCity = Remove city from the tracker.

menu.changeLocale = Change the localization of the app.

menu.exit = Exit the app.

menu.editCity = Edit an existing city.

We want some basic words that we might use:


word.yes = Yes

word.no = No

word.city = City

word.date = Date

And here are all the messages we are going to use in our application:

message.welcome = Welcome to City Visit Tracker!

message.hello = Hello,

message.enterYourName = Enter your name:

message.addCity = Add a new city!

message.addCityName = Enter the city name:

message.addCityDate = Enter the city visited date

message.saved = Successfully saved a new city!

message.cityDeleted = Successfully deleted a city!

message.cityEdited = Successfully edited a city!

message.forExample = for example:

message.noChanges = No changes will be applied.

message.chooseLocale = Choose one of available languages:

message.tryAgain = Try again.

message.noCities = No cities saved yet!

message.chooseDelete = Choose a city to delete.

message.chooseEdit = Choose a city to edit.

message.newCityName = Enter a new city name

message.newCityDate = Enter a new city visited date

Are you ready for the next steps in Java localization and internationalization?

Crowdin and Resources

I added all the messages we needed. However, when developing an application, you want it to be Agile. Crowdin’s Github integration is available. I will go through integration.

  1. Go to your Crowdin account and sign in.
  2. Open a new project.
  3. Enter the info about your project.
  4. Choose the Source language and Target languages (in our case, the source is English, and the target is Ukrainian and Polish.
  5. Select “Choose integration” in your main menu -> GitHub Personal -> Authorize it with GitHub.
  6. Choose a repository you want to work with and select the branch.
  7. You want to create a configuration file in your repository, where you need to specify the /src/resources folder for source files.
  8. You also want to specify the translated files folder. That way, your translators can give out their work piece-by-piece without needing to touch Git. Isn’t it cool?

The next step is to hire professionals to translate your text. But one more excellent feature of Crowdin allows us to translate our source files manually! Let’s do just that.

  • Go to Home - choose target language - press Translate. Now you should see Crowdin’s UI that allows you to choose the most appropriate translation of the Strings in our Source file.
  • Go through them and choose Suggestions below (by the way, they are pretty good!).
  • Once you’ve reached the end of both Ukrainian and Polish files, go back, and press Proofread to approve the translations. (this is usually a task of the localization director, so don’t be afraid to do it now - it’s only for practice :)
  • In your Home folder, press “Build & Download” to download the translations you’ve just created. Now you can copy them into your folder and use them as custom internationalization of your application!

Let’s Go On with Our Application

We want our application to write, edit and remove cities. We also want it to list all the cities we’ve already added.

With the use of DateFormat, ResourceBundle and Locale Java classes, let’s add these methods to our main class:

To change the app’s locale, we simply print the user list of locales and try loading it from our ResourceBundle.


private static void changeLocale() {   System.out.println(userResourceBundle.getString("message.chooseLocale"));
   System.out.println(Arrays.toString(locales));
   Scanner scanner = new Scanner(System.in);
   String newLocale = scanner.nextLine();
   try {
       userResourceBundle = ResourceBundle.getBundle("resources/MessageBundle", new Locale(newLocale));
   } catch (MissingResourceException e) {
      System.out.println(userResourceBundle.getString("message.tryAgain"));
       changeLocale();
   }
}

Next, we provide the user with the menu and wait for the input. Here you see only a few methods. Visit the app’s GitHub page for more.


private static void menu() {
   Scanner scanner = new Scanner(System.in);
   // Print out the menu
   System.out.println(userResourceBundle.getString("menu.chooseAction"));
   System.out.println("0 - " + userResourceBundle.getString("menu.printAll"));
   System.out.println("1 - " + userResourceBundle.getString("menu.addCity"));
   System.out.println("2 - " + userResourceBundle.getString("menu.removeCity"));
   System.out.println("3 - " + userResourceBundle.getString("menu.editCity"));
   System.out.println("4 - " + userResourceBundle.getString("menu.changeLocale"));
   System.out.println("5 - " + userResourceBundle.getString("menu.exit"));
   String answer = scanner.nextLine();

   // If Menu Action is "Add City"
   if (answer.equals("1")) {
       try{
           addCity();
       } catch (Exception ex) {
     System.out.println(userResourceBundle.getString("message.noChanges"));       System.out.println(userResourceBundle.getString("message.tryAgain"));
       }
       menu();
   }



Here we define the addCity() method that adds city to our user’s repo. As you can see here, we use all the power of ResourceBundle, DateFormats and Locales to give the user the most authentic and internationalized experience.


private static void addCity() throws ParseException {
   // Take user's input
   // Create a new City instance to fill it in
   Scanner scanner = new Scanner(System.in);
   City newCity = new City();
   // Add the user's locale to the City instance
   newCity.setCityLocale(userResourceBundle.getLocale());
   // Print out the city name prompt and set the new name
   System.out.println(userResourceBundle.getString("message.addCity"));
   System.out.print(userResourceBundle.getString("message.addCityName"));
   newCity.setCityName(scanner.nextLine());
   // Print out the date prompt and set the new date
   System.out.print(userResourceBundle.getString("message.addCityDate") + ' ');
   // Here, we transform the input data according to the user's locale
   // and save it as usual Date class
   DateFormat df = DateFormat.getDateInstance(DateFormat.DEFAULT, userResourceBundle.getLocale());
   System.out.println('(' + userResourceBundle.getString("message.forExample") + ' ' + pdf.format(new Date()) + ')');
   Date newDate = df.parse(scanner.nextLine());
   newCity.setVisitedDate(newDate);
   repo.addCity(newCity);
   // Tell user we saved a new city!
   System.out.println(userResourceBundle.getString("message.saved"));
}

Let’s define our pinnacle - the primary method of City Visit Tracker!

We’ll have to define three static variables in the CityApplication.java file:


protected static ResourceBundle userResourceBundle;

private static final String[] locales = new String[]{"en_US", "pl_PL", "uk_UA"};

private static final CityRepository repo = new CityRepository();

ResourceBundle helps us switch between different l10n bundles. Locales tell us which languages are supported by our app. Repo is initialized to store the cities.

From now on, all I need to do on the app’s startup is get the user’s default locale, print some welcome messages and start the menu() method.


public static void main(String[] args) {
   // Get user's default locale
   // Load ResourceBundle for the locale
   Locale locale = Locale.getDefault();
   userResourceBundle = ResourceBundle.getBundle("resources/MessageBundle", locale);

   Scanner scanner = new Scanner(System.in);
   System.out.println(userResourceBundle.getString("message.welcome"));
   System.out.print(userResourceBundle.getString("message.enterYourName") + ' ');

   String name = scanner.nextLine();
   Client client = new Client(locale, name);

   System.out.println(userResourceBundle.getString("message.hello") + ' ' + client.getUsername() + "!");

   menu();
}

And we’re done. Now is your turn! This app is a small example of what you can do with Java’s i18n and l10n bundles.

Conclusion

In this tutorial, we discovered what Java offers us in terms of localization and internationalization of our applications. I18n and l10n are extremely important when you want to provide the absolute best experience to the user.

ResourceBundle class gives us an easy way to work with resources.

Locale class gives us ways to interact with the user’s locale.

DateFormat and NumberFormat allow us to format dates and numbers for the user’s locale.

Don’t forget to make your translations agile and clean. Create a default package and let the translators and proofreaders do the work. We’ve also explored a way to automate the translation process with Crowdin – as you can see, it’s simple and fast.

Automate Java localization process

Make it simple and fast. Try Crowdin.
Danyil Subotin

Link
Previous Post
Localization at Kinsta: Tech-enabled Human Translation
Next Post
A Word From a Tech and UI Localization Manager on QA management