In this post I want to show a quick example of how to internationalize your application in a easy simple way by taking advantage of MessageFormat class.
The design is fairly simple: for each single locale/language we have a properties file with translated messages. At the start we load the properties into a MessageGenerator object and then use it’s methods to get the formatted messages and use them in the code. To encapsulate the loading we have a MessageGeneratorFactory with static methods creating appropriate MessageGenerator objects.
The following are examples of properties files that contain translated messages. Notice that each line has a message identifying label and a translated text with placeholders for values. The English properties (translation_en.properties):
ImportCsv=Import csv files into database
MapTypeError=Couldn't map type for table='{0}' column='{1}'
MonitorAsOf=Monitor (as of {0,date,full})
|
The same messages in French (translation_fr.properties):
ImportCsv=Importer des fichiers csv dans la base de données
MapTypeError=Impossible de faire correspondre le type \
pour la colonne {1} de la table {0}
MonitorAsOf=Moniteur (au {0,date,full})
|
The class providing the localized messages in the application is MessageGenerator. See that for each single message there is a separate get method. This way not only we get type checking of provided arguments but also separate the way messages are prepared from their usage. Internally MessageGenerator uses precompiled MessageFormat objects – they are prepared in class constructor and based on properies file loaded by ResourceBundle.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: |
public class MessageGenerator {
// For each of the available messages MessageGenerator
// keeps the precompiled MessageFormat object, a identifying
// String label and a 'get' method.
private final String importCsvLabel = "ImportCsv";
private final MessageFormat importCsv;
private final String mapTypeErrorLabel = "MapTypeError";
private final MessageFormat mapTypeError;
private final String monitorAsOfLabel = "MonitorAsOf";
private final MessageFormat monitorAsOf;
MessageGenerator(ResourceBundle bundle, Locale locale) {
// Load and precompile each of the MessageFormat objects
importCsv = new MessageFormat(
bundle.getString(importCsvLabel), locale);
mapTypeError = new MessageFormat(
bundle.getString(mapTypeErrorLabel), locale);
monitorAsOf = new MessageFormat(
bundle.getString(monitorAsOfLabel), locale);
}
public String getImportCsvMessage() {
return importCsv.format(new Object[] {});
}
public String getMapTypeErrorMessage(String tableName,
String columnName) {
return mapTypeError.format(new Object[] {tableName, columnName});
}
public String getMonitorAsOfMessage(Date date) {
return monitorAsOf.format(new Object[] {date});
}
}
|
See that in the code above the constructor is made package-private. This is because we do not want the user to freely construct MessageGenerator objects, but to use a static factory provided by us. This way we can keep only one instance of this object for each locale (notice that it is immutable).
Now the code of MessageGeneratorFactory. See that to load properties files we use ResourceBundle class.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: |
public class MessageGeneratorFactory {
// This is a static factory, so no constructor is needed
private MessageGeneratorFactory() {
}
private static MessageGenerator englishMessages = null;
private static MessageGenerator frenchMessages = null;
// Methods creating/returning MessageGenerator objects
// for specific languages/localisations:
public static MessageGenerator getEnglishMessages() throws IOException {
if (englishMessages == null)
englishMessages = initMessages(Locale.ENGLISH);
return englishMessages;
}
public static MessageGenerator getFrenchMessages() throws IOException {
if (frenchMessages == null)
frenchMessages = initMessages(Locale.FRENCH);
return frenchMessages;
}
// Helper method loading the Properties and creating MessageGenerator
private static MessageGenerator initMessages(Locale locale) {
// We use ResourceBundle to load the properties files.
// It assumes specific file naming. For instance we expect
// the French translations to be in 'i18n/translation_fr.properties'
ResourceBundle resourceBundle =
ResourceBundle.getBundle("i18n/translation", locale);
return new MessageGenerator(resourceBundle, locale);
}
}
|
Now lets use both of the classes shown above and have fun with a piece of code that prints out some of the messages to the console:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: |
public class MainProgram {
public static void printSomeLocalizedMessages(
MessageGenerator generator) {
System.out.println(generator.getImportCsvMessage());
System.out.println(generator.getMapTypeErrorMessage("X", "Y"));
System.out.println(generator.getMonitorAsOfMessage(new Date()));
System.out.println();
}
public static void main(String[] args) throws Exception {
// Prepare 2 generators: one French and one English
MessageGenerator englishGenerator = MessageGeneratorFactory
.getEnglishMessages();
MessageGenerator frenchGenerator = MessageGeneratorFactory
.getFrenchMessages();
// Test the generators by printing some of the messages
printSomeLocalizedMessages(englishGenerator);
printSomeLocalizedMessages(frenchGenerator);
}
}
|
If you are curious what will be printed out go ahead, copy-paste this code and compile it yourself! You can also download the slightly more complicated version of the code above here.
It is important to mention that there are many things that can be improved in this design – this is a very simple code to be used as an example, feel free to use it and improve it yourself. For instance one of the improvements could be loading properties with translations from XML files (see an example how to do that on www.codewrapper.com)
0 Comments until now
Add your Comment!