Localization

Support for localization, formatting and parsing of locale dependent values, and multi-language messages is an integral part of Civilian.

Introduction

Wikipedia defines localization (l10n) and internationalization (i18n) as "means of adapting computer software to different languages, regional differences".
Localization in the context of web applications deals especially with theses tasks:
  • How to associate a locale with a request and a response.
  • How to parse number, date and time values from request parameters which are formatted according to a certain locale.
  • How to format number, date and time values in response content according to a certain locale.
  • How to construct text messages in response content, translated into a certain language.
If you build an application which only consumes and produces JSON messages, or only constructs responses in a single language, and never serializes a number or date, then localization is obviously not an issue.
Else you might want to learn about the localization support of Civilian.

The basic techniques

Of course the experienced Java programmer knows how to deal with the localization tasks mentioned above:
  • java.util.Locale is the standard way to represent a language or more specific a locale.
  • HTTP specifies how to associate a locale with a request (via the Accept-Language header) and a response (via the Content-Language header). The Servlet API allows to access and override that information in ServletResponse and ServletRequest in form of java.util.Locale objects.
  • Locale dependent parsing and formatting of numbers, date and time values can be achieved using the java.text package.
  • Multi-language applications can be realized by converting message ids into language texts of the target language. java.util.ResourceBundle provides a popular way how to implement such a translation function.
Nevertheless dealing with this tasks is burdensome in detail:
  • Parsing and formatting string based request parameters to and from objects is left to the application.
  • java.text has its own issues (thread-safety, complicated to use, etc.)
  • The various attempts of the JDK to model Date and Time classes were not convincing. (The LocalDate class of Java 8 seems to be the first acceptable solution).

Civilians localization support

Civilians support for locale-dependent and multi-language applications consists of these integrated classes and functionality:
  • MsgBundle allows to translate text id into language texts.
  • TypeSerializer allows for locale-dependent formatting and parsing of values.
  • LocaleService bundles a MsgBundle and a TypeSerializer for a certain Locale.
  • Request and Response can be associated with a LocaleService instance.
  • The applications LocaleServiceList provides LocaleService instances for all supported locales.
  • The LangMixin allows easy formatting and translation in (CSP) templates.
  • The @LocaleValue can be set on controller method parameters to parse injected values into a locale-dependent way.

LocaleService, supported locales

Every Civilian application has a list of supported locales. (If you don't configure explicitly this list will default to the default system locale).
For each supported locale, a LocaleService object is available via the applications LocaleServiceList. The LocaleService itself offers
  • a MsgBundle which can be used to translate text keys into texts of a certain language
  • a TypeSerializer which can be used to format or parse objects of simple types to and from strings.

Associating a LocaleService with request and response

Both Request and Response provide a LocaleService instance. If you don't set the LocaleService explicitly, it will be initialized as follows:
  • The request uses the preferred client locale as specified with the Accept-Language header and asks the LocaleServiceList for the corresponding LocaleService instance. If the locale is not supported, it will fall back to the default locale of the LocaleService.
  • The response initializes its LocaleService instance from the request.
A frequent use case is to ignore the Accept-Language header and set the request LocaleService explicitly – for instance using the locale preference stored in a user session. Just be sure to set the LocaleService before you start to read locale-dependent parameters from the request.

The MsgBundle class

Each LocaleService has a MsgBundle object to translate text ids into texts of the language of the LocaleService.
LocaleService ls = app.getLocaleServices().getLocaleService(Locale.FRENCH);
assertEquals("liberté", ld.getMessages().get("FREEDOM"));
MsgBundle itself is abstract, in order to support different implementations for this translation service. Internally, the LocaleServiceList uses a MsgBundleFactory to create MsgBundle instances for a locale.
In not explicitly configured the MsgBundle(s) of an application are empty: They will just return the text id + "?" as translated text.

Civilian provides classes ResMsgBundle and ResMsgBundleFactory which are a MsgBundle and factory implementations based on java.util.ResourceBundle. Please see the config section on how to configure and use these.

The resource bundle compiler

Using string text ids which are translated into languages texts has an obvious disadvantage: If you pass string literals, you can't easily find where a specific text id is used except using full text search on your code-base.
A better idea is to define Java constants for your text ids, and use the constants on a MsgBundle:
import com.myapp.text.Msg;

MsgBundle msgs = ...
msgs.get("FREEDOM")); // string literal
msgs.get(Msg.FREEDOM); // constant, checked by the compiler
Civilian provides a small command-line tool ResBundleCompiler which is based on that idea. It enables the following workflow:
  • You collect and edit your text ids and translations in a Excel file. It contains a column for the text id and columns for all translated locales.
  • Whenever you change (add, edit, delete) texts in that translation file you invoke the ResBundleCompiler on it.
  • The compiler generates a resource-bundle file for each locale, and a Java class defining constants for every text id (like the Msg class in the above example).
The ResBundleCompiler uses Apache POI to read the Excel file. In order to run the ResBundleCompiler, you must add the POI (3.1+) libraries to your classpath.
When you run the ResBundleCompiler without arguments, it will print a help message how to use it.
The CRM sample demonstrates this technique.

The Type framework

Civilians Type framework builds the basis of locale-dependent formatting and parsing of values. But not restricted to locale-dependent string representations it allows to implement arbitrary serialization schemes:

Generally speaking, a parsing and formatting function depends on both the value type and the serialization scheme:

format: (T value, TypeSerializer ts)  → String
parse: (String s, TypeSerializer ts) → T
The type framework implements such functions using a double-dispatch-pattern: Types like strings, numbers, boolean, dates are represented by a Type object, the serialization scheme is represented by a TypeSerializer object. Given a Type object, a value of that type and a TypeSerializer we can produce a string for the value. In reverse given the Type, TypeSerializer and that string we can reconstruct the value:
TypeSerializer ts = ...;
Type<T> type = ...;
T value = ...;
String s = type.format(ts, value);
T parsed = type.parse(ts, s);
assertEquals(value, parsed);
Civilian implements two serialization schemes, a locale-dependent LocaleSerializer and a StandardSerializer which is based on String.valueOf to serialize values:

Value formatted by LocaleSerializer StandardSerializer
en_UK fr de
String("abc") "abc" "abc" "abc" "abc"
Integer(123456) "123,456" "123 456" "123.456" "123456"
Double(9876.54) "9,876.54" "9 876,54" "9.876,54" "9876.54"
Date(y=2014, m=12, d=31) "12/31/2014" "31/12/2014" "31.12.2014" "20141231"

Date, Time and DateTime values

Civilian provides out of the box support for java.time.LocalDate, java.time.LocalTime, java.time.LocalDateTime, java.util.Date, java.sql.Date, java.util.Calendar.

Localization of controller method parameters

You can inject request values into parameters of a controller action method. For example given
public class SearchController extends Controller
{
@Post public void search(@Parameter("term") String term, @Parameter("from") Date from)
{
...
the query parameters term and from are injected into the respective parameters when this method is invoked.
The String parameter is passed unchanged, but the Date parameter will be converted from a string into a date value. If this conversion is done by a Type object, then by default the StandardSerializer is used, and of course will raise a runtime error if the supplied query parameter does not conform to its format.
But what if the Date value was entered in a HTML form in a locale-dependent manner? Add the @LocaleValue annotation to instruct the Type to use the locale-dependent LocaleSerializer of the request to parse the value:
    @Post public void search(..., @QueryParam("from") @LocaleValue Date from)

Localization and forms

If a HTML form presents a textfield to allow the user to input an integer, a decimal number, a date, a time value, etc. then the input is done in some locale-dependent format and needs to be parsed from a string request parameter when the submitted form is evaluated on the server.
The form controls in Civlians form library don't force you to interact with string values, but already convert request values into typed representations. In addition any form control which displays a HTML textfield to the user, will parse the entered value in a locale dependent manner.

Configuration

You can configure the list of supported locales and the MsgBundleFactory to create MsgBundle for those locale in civilian.ini:
app.myapp.locales   = en-UK,de-AT,de-CH,fr
app.myapp.messages = resbundle:org/example/myapp/text/message
The locales entry lists the supported locales, the default locale coming first.
The message entry defines a MsgBundleFactory. The entry is either
  • The class name of a MsgBundleFactory implementation
  • a string starting with resbundle: followed by a base name which can be passed to java.util.ResourceBundle.getBundle(String baseName, Locale locale).
In the example above the application myapp could provide these resource bundle files
org/example/myapp/text/message_en_UK.properties
org/example/myapp/text/message_de.properties
org/example/myapp/text/message_fr.properties
(in that case locale de-AT and de-CH would share the same resouce bundle).

Alternatively you can also configure the localization settings programmatically during Application.init(AppConfig), using AppConfig.setSupportedLocales() and AppConfig.setMsgBundleFactory().
You may also add own Type implementations for custom types to the type library of the application.