![]() |
|
This document is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 2.5 License |
CAL10N Manual
The goal of CAL10N project is to enhance the existing internationalization functionality offered by the Java platform with consistency and verification primitives. It is assumed that you are already somewhat familiar with using ResourceBundles.
Acknowledgment
The original idea behind CAL10N is attributed to Takeshi Kondo. It consolidated into what it is today subsequent to a discussion involving Ralph Goers, Ceki Gülcü, Takeshi Kondo and Pete Muir on the slf4j-dev mailing list.
Core idea
Instead of using values of type String as the key for each
message, CAL10N uses enums
.
For example, let us assume that you wanted to internationalize color names. No one in their right minds would just want to internationalize three color names but probably a much larger set of items. We are using a very small set of colors just as an example. In CAL10N you could start by writing an enum type, named Colors. You can choose any name for the enum type. Colors is just the name this particular author picked.
package com.foo.somePackage; import ch.qos.cal10n.LocaleData; import ch.qos.cal10n.Locale; import ch.qos.cal10n.BaseName; @BaseName("colors") @LocaleData( { @Locale("en_UK"), @Locale("fr") }) public enum Colors { BLUE, RED, YELLOW; }
Once you define a few color keys, you can create a regular resource bundle named after the value of the @BaseName annotation in Colors and the appropriate locale. For example, for the UK locale, you would name your resource bundle as colors_en_UK.properties. It should also be placed in the appropriate folder on your class path or in the root folder of an appropriate jar file. (It is assumed that you know how to work with resource bundles.)
Here is a sample a colors_en_UK.properties file:
BLUE=violets are blue RED=roses are red GREEN={0} are green
For the french locale, the resource bundle would be named colors_fr.properties. Here are sample contents.
BLUE=les violettes sont bleues RED=les roses sont rouges GREEN=les {0} sont verts
Retrieving internationalized messages
In your application, you would retrieve the localized message via an IMessageConveyor instance.
// obtain a message conveyor for France IMessageConveyor mc = new MessageConveyor(Locale.FRANCE); // use it to retrieve internationalized messages String red = mc.getMessage(Colors.RED); String blue = mc.getMessage(Colors.BLUE); String green = mc.getMessage(Colors.GREEN, "pommes"); // note the second argument
CAL10N leverages the existing resource bundle infrastructure you have been accustomed to, but adds compiler verification on top. The default IMessageConveyor implementation, namely MessageConveyor, uses the standard Java convention for parameter substitution as defined by the java.text.MessageFormat class.
An astute reader will comment that even if the messages keys are now verified by the compiler, it is still possible to have mismatching message keys in the resources bundles. For this reason, CAL10N comes with additional tools, including support for writing Junit test cases and a maven plugin.
Requirements / Installation
CAL10N requires JDK version 1.5.
In order to use CAL10N in a project, all you need is to add cal10n-api-${project.version}.jar onto your project's class path.
For Maven users, this is done by adding the following dependency in a project's pom.xml file:
<dependency> <groupId>ch.qos.cal10n</groupId> <artifactId>cal10n-api</artifactId> <version>${project.version}</version> </dependency>
Simplified bundle look-up procedure
The ResourceBundle
class defines a rather involved
look-up
procedure for finding resource bundles. While formally this
procedure is deterministic, it is too error-prone. Moreover, it
clashes with CAL10N's philosophy of verifiability. We thus took the
bold initiative to define a simplified bundle look up procedure,
described below.
Given a locale, the simplified look up procedure only takes into
account that locale, ignoring the default locale and the resource
bundle corresponding to the naked base name, i.e. the default
resource bundle. However, if the locale has both a language
and a country, and corresponding bundle files exist, then
CAL10N will take into account both bundles, establishing the same
parent child relationship as the JDK ResourceBundle
class.
For example, for base name "colors" if the following bundles exist on the class path:
colors.properties colors_en_US.properties colors_en.properties colors_fr_FR.properties
and the system's default locale is "fr_FR", when CAL10N is asked to find resource bundles corresponding to the "en_US" locale, it will ignore the colors.properties (~ default bundle) and colors_fr_FR.properties (~ default locale), while combining the colors_en_US.properties and colors_en.properties bundles in the usual parent-child relationship.
We hope that the simplified look-up procedure, while deviating
from the conventions of the Java platform as defined in the
ResourceBundle
class, will still cause
less surprise.
Pick your charset, per locale (no native2ascii)
CAL10N allows you to encode the resource bundle for a given locale in the charset of your choice. The set of supported charsets depends on your Java platform. See this java encoding and charset list for more details.
Assume you have four resource bundles, for the English, French, Turkish and Greek languages. You decide to encode all of them in UTF-8. To tell CAL10N that UTF-8 is the default encoding for all locales, one would write:
@BaseName("colors") @LocaleData( defaultCharset="UTF8", value = { @Locale("en_UK"), @Locale("fr_FR"), @Locale("tr_TR"), @Locale("el_GR") } ) public enum Colors { BLUE, RED, GREEN; }
If for some reason the Turkish bundle was encoded in ISO8859_3, but the others locales in UTF8, you would write:
@BaseName("colors") @LocaleData( defaultCharset="UTF8", value = { @Locale("en_UK"), @Locale("fr_FR"), @Locale(value="tr_TR", charset="ISO8859_3"), @Locale("el_GR") } ) public enum Colors { BLUE, RED, GREEN; }
The defaultCharset
directive specified in the
@LocaleDat
a annotation applies to all nested @Locale
annotations, unless the value is overriden by a charset
directive (as in the "tr_TR" locale in the example above). If not
specified, the default value for defaultCharset
is the
empty string. In the absence of a defaultCharset
directive, the default value for the charset
directive
is also the empty String.
If both charset
and defaultCharset
are
empty, CAL10N will use the default encoding
for your Java platform to read resource bundles.
Automatic reloading of resource bundles upon change
When a resource bundle for a given locale is changed on the file
system, MessageConveyor
will automatically reload the
resource bundle. It may take up to 10 minutes for the change to be
detected.
Automatic reloading applies if the resource bundle is a regular file but not if nested within a jar file.
Deferred localization
Under certain circumstances, the appropriate locale is unknown at the time or place where the message key and message args are emitted. For example, a low level library might wish to emit a localized message but it is only at the UI (user interface) layer that the locale is known. As another example, imagine that the host where the localized messages are presented to the user is in a different locale, e.g. Japan, than the locale of the source host. e.g. US.
The MessageParameterObj
class is intended to support this particular use case. It allows you
to bundle the message key (an enum) plus any message
arguments. At a later time, when the locale is known, you would
invoke the getMessage(MessageParameterObj)
method in IMessageConveyor
to obtain the localized
message.
Verification as a test case
A convenient and low hassle method for checking for mismatches between a given enum type and the corresponding resource bundles is through Junit test cases.
Here is a sample Junit test for the Colors enum discussed above.
package foo.aPackage; import static org.junit.Assert.assertEquals; import java.util.List; import java.util.Locale; import org.junit.Test; import ch.qos.cal10n.verifier.Cal10nError; import ch.qos.cal10n.verifier.IMessageKeyVerifier; import ch.qos.cal10n.verifier.MessageKeyVerifier; public class MyColorVerificationTest { @Test public void en_UK() { IMessageKeyVerifier mkv = new MessageKeyVerifier(Colors.class); List<Cal10nError> errorList = mkv.verify(Locale.UK); for(Cal10nError error: errorList) { System.out.println(error); } assertEquals(0, errorList.size()); } @Test public void fr() { IMessageKeyVerifier mkv = new MessageKeyVerifier(Colors.class); List<Cal10nError> errorList = mkv.verify(Locale.FRANCE); for(Cal10nError error: errorList) { System.out.println(error); } assertEquals(0, errorList.size()); } }
The above unit tests start by creating a
MessageKeyVerifier
instance associated with an enum
type, Colors
in this case. The test proceeds to invoke
the verify()
method passing a locale as an
argument. The verify()
method returns the list of
errors, that is the list of discrepancies between the keys listed
in the enum type and the corresponding resource bundle. An empty
list will be returned if there are no errors.
The unit test verifies that no errors have occurred by asserting that the size of the error list is zero.
Suppose the key "BLUE" was misspelled as "BLEU" in the
colors_fr.properties resource bundle. The unit test would
print the following list of errors and throw an
AssertionError
.
Key [BLUE] present in enum type [ch.qos.cal10n.sample.Colors] but absent in resource bundle \ named [colors] for locale [fr_FR] Key [BLEU] present in resource bundle named [colors] for locale [fr_FR] but absent \ in enum type [ch.qos.cal10n.sample.Colors]
One test to rule them all
Instead of a separate unit test case for each locale, assuming you declared the locales in the enum type via the @LocaleData and nested @Locale annotations, you can verify all locales in one fell swoop.
package foo.aPackage; import static org.junit.Assert.assertEquals; import java.util.List; import java.util.Locale; import org.junit.Test; import ch.qos.cal10n.verifier.Cal10nError; import ch.qos.cal10n.verifier.IMessageKeyVerifier; import ch.qos.cal10n.verifier.MessageKeyVerifier; public class MyAllInOneColorVerificationTest { // verify all locales in one step @Test public void all() { IMessageKeyVerifier mkv = new MessageKeyVerifier(Colors.class); List<Cal10nError> errorList = mkv.verifyAllLocales(); for(Cal10nError error: errorList) { System.out.println(error); } assertEquals(0, errorList.size()); } }
Maven Plugin
The CAL10N project ships with a maven plugin designed to verify that the keys specified in a given enum type match those found in the corresponding resource bundles and for each locale. Our plugin is unsurprisingly called mvn-cal10n-plugin.
Using maven-cal10n-plugin is pretty easy. To verify
enums in a given project, just declare the
maven-cal10n-plugin in the <build>
section,
enumerating all the enum types you would like to see checked.
Here is a sample pom.xml snippet.
<build> <plugins> ... other plugins <plugin> <groupId>ch.qos.cal10n.plugins</groupId> <artifactId>maven-cal10n-plugin</artifactId> <executions> <execution> <id>aNameOfYourChoice</id> <phase>verify</phase> <goals> <goal>verify</goal> </goals> <configuration> <enumTypes> <!-- list every enum type you would like to see checked --> <enumType>some.enumTpe.Colors</enumType> <enumType>another.enumTpe.Countries</enumType> </enumTypes> </configuration> </execution> </executions> </plugin> </plugins> </build>
After you add the above snippet to the pom.xml file of your project, maven-cal10n-plugin will make sure that your resource bundles and your enum type are in synchronized.
The plugin will iterate through every resource bundle for every
locale listed in the enum type via the @LocaleData
and
@Locale
annotations.
Eclipse plug-in
We are looking for volunteers willing to implement IDE support for CAL10N. Below is a list of possible features of this IDE which we think could have added value.
- mismatch highlighting
While editing a property file, highlight any keys that do not match any keys defined in the corresponding enum type.
- auto-correction
The mismatch highlighting feature could be enhanced by making correction suggestions. For example, if in the property file the user misspells the key "red_elephant" as "red_elefant", by measuring the Levenshtein distance the plug-in could propose the nearest neighbor of "red_elefant" in the enum type, which should be "red_elephant" with high probability.
-
Skeleton generation
Given an enum type, generate the skeleton of the corresponding resource bundle
If interested please contact the cal10n-dev mailing list.
Ant task
We are looking for volunteers to implement an Ant task to verify resource bundles. The Ant task could be modeled after the maven-cal10n-plugin although the Ant plugin is likely to be simpler. If interested please contact the cal10n-dev mailing list.