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)
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.