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 String-typed keys for each message, CAL10N uses
enums
.
For example, let us assume that you wanted to internationalize color names. 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
Strictly speaking, the @LocaleData annotation is optional. It is used by verification tools discussed below.
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. In particular, for messages with parameters, the rules for
using quotes defined by the MessageFormat
class apply
as is.
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 JSR-269, writing Junit tests as well as a maven plugin.
Requirements / Installation
CAL10N requires JDK version 1.5. JSR-269 requries JDK 1.6 or later.
In order to use CAL10N in a project, all you need is to add cal10n-api-0.8.1.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>0.8.1</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 also error-prone. More
importantly, it clashes with CAL10N's goal 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 systematically ignore the colors.properties (i.e. the default bundle), and combine the colors_en_US.properties and colors_en.properties bundles in the usual parent-child relationship. Since CAL10N is asked to lookup resource bundles corresponding to the "en_US" locale, the bundle corresponding to the default locale, i.e. colors_fr_FR.properties will also be ignored. Thus, the CAL10N bundle lookup procedure differs from the standard by ignoring the default bundle, the one with the naked base name, e.g. colors.properties.
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; import ch.qos.cal10n.sample.Colors; 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 org.junit.Test; import ch.qos.cal10n.verifier.Cal10nError; import ch.qos.cal10n.verifier.IMessageKeyVerifier; import ch.qos.cal10n.verifier.MessageKeyVerifier; import ch.qos.cal10n.sample.Colors; 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()); } }
JSR-269 support, i.e. verification at compile time
Verify your bundles for every compilation of an enum class annotated with @BaseName.
As of version 0.8, CAL10N ships with an JSR-269 compliant
annotation processor named
CAL10NAnnotationProcessor
. By enabling this processor
you can have javac, i.e. the java compiler, report discrepancies in
your bundles at compile-time without leaving your favorite IDE. The
verification is triggered at every compilation of an enum class
annotated with @BaseName
.
All majors IDEs, including Eclipse, IntelliJ IDEA and Netbeans support annotation processors.
The fully qualified name of
CAL10NAnnotationProcessor
is
ch.qos.cal10n.verifier.processor.CAL10NAnnotationProcessor
.
As all annotation processors
CAL10NAnnotationProcessor
can be integrated with javac
by creating a file named
META-INF/services/javax.annotation.processing.Processor
containing the fully qualified name of the processor,
i.e. ch.qos.cal10n.verifier.processor.CAL10NAnnotationProcessor
. The
artifact cal10-api-0.8.1.jar
ships
precisely with such a file.
Activating
CAL10NAnnotationProcessor
in Eclipse
The internet abounds with documentaton on installing an annotation processor for Eclipse. The author found the blog entry entitled Using Java 6 processors in Eclipse quite helpful. In the author's experience, the project's Compiler comliance level in Eclipse should be 1.6 or higher. Otherwise, the annotation processor is not activated.
As an additional caveat, it appears that Eclipse (Indigo and Juno) hides resources located udner "src/test/resources" while resources under "src/main/resources" are visible to the annotation processor. Thus, if resource bundles are placed under "src/test/resources" the compiler will complain about missing resource bundles. As soon as the bundles are moved under "src/main/resources" the errors will disappear (assuming the bundles are complete).
Activating
CAL10NAnnotationProcessor
in IntelliJ IDEA
IntelliJ IDEA's support for annotation processors is
superb. Searching for "annotation processor intellij idea" on Google
should yeild the appopriate documentation. Just remember to define a
"Anotation Processor" profile using
CAL10NAnnotationProcessor
fully qualified name and add
the modules in your project which depend on cal10n to that
profile. The integration should proceed rather smoothly.
Maven Plugin
JSR-269 obsoletes maven plugin The maven-compiler-plugin automatically integrates CAL10NAnnotationProcessor for every dependecy of cal10-api rendering the Maven Plugin obsolete.
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.