How to Improve Localized String Formatting in Java
Java localization often involves many stringly typed things where one stray character can break an otherwise simple operation. This tutorial demonstrates how to improve correctness in localized applications.
Steps
-
Check out the base project
Follow the steps in How to Set Up, Compile and Run an AutoRes.uk Project
-
Inspect the localized resource bundle
The files that match
src/main/resources/com/example/i18n*.properties
form a resource bundle. English (root), german (de), french (fr) and italian (it) are included in the project. -
Start with a traditional resource bundle
This uses the ResourceBundle type.
Replace the contents of
Hello.java
with:package com.example; import java.util.Locale; import java.util.ResourceBundle; public class Hello { public static void main( String[] args ) { Locale[] locales = {Locale.ROOT, Locale.FRENCH, Locale.GERMAN, Locale.ITALIAN}; for (Locale l : locales) { ResourceBundle bundle = ResourceBundle.getBundle("com.example.i18n", l); String msg = bundle.getString("hello-world"); System.out.println(l + "=" + msg); } } }
Compile and run the application:
./mvnw --quiet clean compile exec:java
The localized messages will be printed to the console.
-
Make the bundle less stringly typed
Bundles can have a lot of properties. If a property name is misspelled or the property is missing then
null
is returned.Annotate the class with
@uk.autores.Keys("i18n.properties")
.Replace
ResourceBundle.getBundle("com.example.i18n", l);
withResourceBundle.getBundle(I18N.bundle(), l);
.Replace
bundle.getString("hello-world");
withbundle.getString(I18N.HELLO_WORLD);
.Recompile and run the application.
Keys are now verified during compilation.
-
Add some string composition
A common requirement in localization is to compose strings from variables. This is often done using MessageFormat. The root property is
iso-80000-defines=ISO 80000 defines {0} to be {1,number} bytes.
.Replace the contents of
Hello.java
with:package com.example;
import uk.autores.Keys;
import java.text.MessageFormat; import java.util.Locale; import java.util.ResourceBundle;@Keys("i18n.properties")
public class Hello { public static void main(String[] args) { Locale[] locales = {Locale.ROOT, Locale.FRENCH, Locale.GERMAN, Locale.ITALIAN}; for (Locale l : locales) { printBytesPerUnit(l, "GB", 1024*1024*1024); } }private static void printBytesPerUnit(Locale l, String unit, int bytes) { ResourceBundle bundle = ResourceBundle.getBundle(I18N.bundle(), l); String pattern = bundle.getString(I18N.ISO_80000_DEFINES); Object[] params = {unit, bytes}; MessageFormat mf = new MessageFormat(pattern, l); String msg = mf.format(params); System.out.println(l + "=" + msg); }
}Compile and run the application:
./mvnw --quiet clean compile exec:java
The localized messages will be printed to the console.
-
Generate a localized message method
Replace the contents of
Hello.java
with:package com.example;
import uk.autores.Messages;
import java.util.Locale;@Messages("i18n.properties")
public class Hello { public static void main(String[] args) { Locale[] locales = {Locale.ROOT, Locale.FRENCH, Locale.GERMAN, Locale.ITALIAN}; for (Locale l : locales) { String msg =I18N.iso80000Defines(l, "GB", 1024*1024*1024);
System.out.println(l + "=" + msg); } } }Compile and run the application:
./mvnw --quiet clean compile exec:java
The localized messages will be printed to the console.
-
Add a new property
Add the following to
i18n.properties
. Do not edit the other properties files.hello=Hello {0}!
Compile the application:
./mvnw clean compile
Compilation fails because the new property is missing from the localized files. This feature lets developers guard against missing translations.
Alter the annotation to be:
@Messages(value = "i18n.properties", missingKey = Severity.WARN)
Recompile the application.
Compilation succeeds, with warnings. This allows development to continue while waiting for new translations.
missingKey
should be set toERROR
before release.
Tradeoffs
The goal of the library is to provide convenience and correctness. However, light testing with the Java Microbenchmark Harness suggests performance benefits.
The benchmark result below compares using
ResourceBundle
& MessageFormat
versus @Messages
to format the iso-80000-defines
property.
Benchmark Mode Cnt Score Error Units Hello.bundle thrpt 25 131580.518 ± 1640.646 ops/s Hello.method thrpt 25 261516.097 ± 6170.135 ops/s
The generated method is about twice as fast based on throughput.
In the generated method the format template is analysed at compile time. The runtime code is composed of simpler concatenation operations. Buffer sizes can be estimated ahead of time to reduce resizing.
Potential downsides are increased compilation times and greater class file overheads.