This document describes internationalization issues for webAF components. This covers how the existing com.sas classes and packages use resource bundles, as well as how to use some of the Internationalization support in classes build in the webAF component framework.
Internationalization also known as I18N, which is simply an abbreviation standing for "I" + 18 letters + "N" from the word "InternationalizatioN".
Localization, sometimes abbreviated as L10N, is the process of creating a locale specific version of an application. This usually consists of at least translating the text resources. Good I18N design reduces the amount of work necessary during the localization process. This is a good tradeoff if you intend to localize an application several or many locales.
This section should be read and used by all component developers. It discusses how the com.sas Java components support Internationalization.
The Core Java API provides some I18N support in various flavors: Strings are Unicode; the java.text package supports I18N constructs such as java.text.MessageFormat and java.text.NumberFormat; the java.util.ResourceBundle and java.util.PropertyResourceBundle classes provide some built in mechanisms for providing translation support. (I"ll strive to not misspell "bundle" as "bungle" in this document.)
See also Java In a Nutshell by O"Reilly & Associates, Chapter 11, which is a good resource on I18N in Java 1.1.
All natural language text presented to the user needs to be isolated so that it can be translated for different locales. This is done in Java with resource bundles, which are collections of objects that have different values for different locales. (A localeconsists of a natural language, such as French or English, and optionally country or regional variations. Java uses a java.util.Locale object to identify different locales, and the java.util.ResourceBundle abstract class (and several concrete classes) to provide these collections of localized values. Most such values will be string data.
Other natural language or cultural differences should also be localized. This may include date/time and currency formatting, images, audio clips, and so on.
Components should use
java.text.MessageFormat
or
com.sas.text.Message
to format strings to be presented to the user. (com.sas.text.Message is
more convenient, especially when formatting messages from a resource
bundle.)
These message strings
should be used whenever you combine two or more elements (normally
with Java string concatenation) or where you would use
the analogous printf in C.
Places to use
MessageFormat
include:
Simple strings that are not combined with other elements to form a longer string can be accessed directly from a resource bundle. For example, MessageFormat objects may not be required for simple border labels, but the text should still come from a ResourceBundle (or a properties file; see below.)
Do not use Java concatenation to create a string. For example, if you want to display a label for a container which tells how many items have been selected in a list box in the container, use:
public static final String RB_KEY = "MyContainer.";
String labelPattern = RB.getStringResource(RB_KEY,
"selectionCount.txt");
Object args[] = new Object[2];
args[0] = new Integer(nSelected);
args[1] = new Integer(nAvailable);
label.setText( MessageFormat.format(labelPattern, args );
where "MyContainer.selectionCount.txt"
is the key to a resource in the resource bundle for a pattern
like "{0} items of {1} items selected."
(The use of RB.getStringResource() is explained below).
MyContainer.selectionCount.txt={0} items of {1} items selected.
(This pattern says to format the integer count, the 0th argument,
followed by the text " items of ", then the 1st argument,
then the text " items selected.",
to yield a string such as "5 items of 12 items selected" when
nSelected==5
and
nAvailable==12.
You can reuse a MessageFormat object if you need to format the same text over and over again with different arguments. For example:
// declare some instance variables
transient StringBuffer labelMessageBuffer =
new StringBuffer();
transient labelMessage =
new MessageFormat(RB.getStringResource(RB_KEY,
"selectionCount.txt"));
transient Object twoArgs[] = new Object[2];
// then to format the label:
synchronized (labelMessage)
{
twoArgs[0] = new Integer(nSelected);
twoArgs[1] = new Integer(nAvailable);
labelMessage.format( twoArgs, labelMessageBuffer, null);
label.setText( labelMessageBuffer.toString() );
}
Even this is awkward, so we have created the com.sas.text.Message class as an easier way to format messages from a resource bundle:
// declare some instance variables
transient String labelPattern = ;
transient labelMessage =
new Message(RB.getResources(),
RB_KEY + "selectionCount.txt");
label.setText( labelMessage.toString( new Integer(nSelected),
new Integer(nAvailable));
The com.sas.text.Message class, which extends from MessageFormat, has constructors for instantiating a Message from a resource in a bundle, optionally with an array of args, or with from one to six Object arguments which are put into an array for you. You might then format the Message object with its toString method, or format a Message with an array of Objects or with one to six arguments. Also, there are static methods to format a message from a ResourceBundle, using either an Object array or from one to six arguments which are put into an array for you.
Avoid a statement such as
// do not do this
label.setText( nSelected + " items of "
+ nAvailable + " items selected." );
if you want to support L10N in your application.
webAF components support L10N through resource bundles. There is normally use one resource bundle per package, named "Resources" which contains the default resources (that is, US English). The resource bundle is implemented through the "Resources.properties" file in each package (directory) with the properties listed in them. Translation for other locales will require simply creating new .properties files for those locales. For example, the Resources.properties file for com.sas.models may contain
DefaultColorList.description.txt=List of standard AWT colors DefaultColorList.color.txt=color
and Resources_en_GB.properties may contain
DefaultColorList.color.txt=colour DefaultColorList.description.txt=List of standard AWT colours
When the DefaultColorList class loads the resource
public static final String RB_KEY = "DefaultColorList."; RB.getStringResource(RB_KEY,"color.txt");
it returns "colour" for the en_GB locale, and
"color" for the default en_US locale.
When the DefaultColorList class loads the resource
RB.getStringResource(RB_KEY, "properties.root.description.txt");
it returns "name of the root" (from the default Resources.properties) for both locales.
webAF components use a static method RB.getStringResources()
from the RB class for each package to get a String from the resource
bundle. This reduces the number of ResourceBundle.loadBundle(String)
calls to one per package.
See com.sas.beans.RB
for the public interface of the RB class.
Be careful not to confuse or mix RB classes in different packages.
Each webAF class that uses resources also defines a
public static final String RB_KEY = "Class-Name.";
constant (note the trailing ".") with which to name its resources in the properties file. For example, com.sas.visuals.BaseBorder contains
public static final String RB_KEY = "BaseBorder.";
and all the resources that class use, listed in
com/sas/visuals/Resources.properties, have keys that begin with
"BaseBorder."
For example, a class may use:
String label = RB.getStringResource(RB_KEY, "label1.txt");
Resource names are prefixed with the class name unless they can be shared across classes in the package. Thus, if RB_KEY is "MyClass." in the example above, the complete string resource will be "MyClass.label1.txt".
One risk of statically initializing the resource bundle this way is that if the user does a Locale.setDefault(Locale) later, none of the classes which have stored the resource bundle (which was created with the original default locale) will know about the new Locale, and will therefore be retrieving resources based on a Locale that is not the current default Locale.
The com.sas.Resources resource file stores resource
strings for classes in the com.sas package (for the default
Locale, en_US).
For example, a class implementing com.sas.ViewInterface
that wishes to throw an exception from its attachModel method
may do:
throw new
ComponentException(Message.format(
com.sas.RB.getStringResource("ViewInterfaceSupport.attachDenied.ex.txt"),
"com.sas.visuals.DualSelector"));
The named string resource is
{0}.attachModel() : model has denied attachment request from a view object.
where {0} is replaced with the 0th argument, which is
"com.sas.visuals.DualSelector" in this case. Thus, the exception message
will be com.sas.visuals.DualSelector.attachModel() : model has denied attachment request from a view object.
The com/sas/util/CommonResources.properties file will contain common resources
used by for many packages/classes. For example, the labels for OK,
Cancel, and Help buttons will appear in this property file.
You can access the common resource property bundle with
public java.util.ResourceBundle com.sas.Util.getCommonResources()
and you can fetch a string from the common resource bundle with:
String common = Util.getCommonStringResource(key);
PushButton okPushButton =
new PushButton(Util.getCommonStringResource("Ok.txt"));
PushButton cancelPushButton =
new PushButton(Util.getCommonStringResource("Cancel.txt"));
PushButton helpPushButton =
new PushButton(Util.getCommonStringResource("Help.txt"));
For more details on using property bundles, see the Java web site for the Property Bundles Tutorial and string resources using PropertyResourceBundles in the Java Tutorial.
A component ExtendedBeanInfo should also initialize description fields from resource bundles. For example:
public static com.sas.beans.ExtendedBeanInfo getExtendedBeanInfo()
{
String propertyMetadata[][][] =
{
{
{"Name", "root"},
{"Default value", "\"\""},
{"Hidden", "true"},
{"Description", RB.getStringResource(RB_KEY, "root.pd.txt")},
},
};
ExtendedBeanInfo ebi = new ExtendedBeanInfo();
ebi.shortDescription = RB.getStringResource(RB_KEY, "description.txt");
ebi.propertyMetadata = propertyMetadata;
return ebi;
}
The string resource name RB_KEY + "root.pd.txt" indicates it is the
property description for the
property named root. Other translatable property metadata values
should use the same naming convention (such as RB_KEY + ".root.iv.txt" for
initial value (if the initial value is
translatable), and so on
Note that the Resources.properties for the com.sas package has the following reusable properties already defined:
VisualInterface.defaultWidth.pd.txt=The default width VisualInterface.defaultHeight.pd.txt=The default height
so if your component has static defaultHeight and defaultWidth properties, you should use the resource from com.sas.RB, as below:
{
{ "Name", "defaultWidth"},
{ "Default value", "100"},
{ "Description",
com.sas.RB.getStringResource("VisualInterface.defaultWidth.pd.txt")},
},
{
{ "Name", "defaultHeight"},
{ "Default value", "30"},
{ "Description",
com.sas.RB.getStringResource("VisualInterface.defaultHeight.pd.txt")},
},
Note that some property values (like "true" for the "Hidden" meta property) are not translated.
We recommend a consistent naming conventions for resource keys. Part of the resource name will be a tag identifying the type of resource it is.
=
in the properties file..txt.txt suffix. This is a key which the translators can look for
during the L10N process.
All .txt items will be translated.
.fmt.txt.fmt.txt suffix.
All .fmt.txt items will be translated.
Text that is processed by
java.text.MessageFormat
must follow the conventions of the MessageFormat
class. That is, certain characters such as single quotes must be escaped
because of the way
MessageFormat
processes the format string. If you want the message to contain a single quote,
you need two single quotes:
response.fmt.txt={0} don""t mean a thing if it ain""t got that swing.
Also, some text within curly braces {} should
not be translated. For example, {0,number,int}
should not be translated since number and int are
used here as formatting directives..image.url.audio.media.object.notrans.classname.pd.txt
for property descriptors, such as DualSelector.dynamic.pd.txt.spd.txt
for the short property description, such as
DualSelector.dynamic.spd.txt.ex.txt
for exception messages used to create exception objects, such as DualSelector.invalidCount.ex.txtViewInterfaceSupport.trapInterfaceEvents.ex.txt
or ComponentInterface.dumpStarting.txt. Normally
the inner key (after the class name, if it exists, and before the suffixes)
should start with lower case.Customizers should of course use resource bundles to populate
labels and other text the user sees.
You can use the
public Label(ResourceBundle bundle, String resourceKey);
constructor to the com.sas.awt.Label class
which facilitates the use of resource bundles
in customizers.
Certain data models may also be required to use resource bundles. For example, the color names in the default color list class are localized color names read from the resource bundle, even for Java standard colors like red and green.
Enumeration classes will have resource strings which translate the enumeration key. For example, the com.sas.visuals.Placement class has the following resource entries in com/sas/visuals/Resources.properties:
Placement.TOP.txt=Above and centered Placement.BOTTOM.txt=Below and centered Placement.LEFTSIDE_TOP.txt=Left side, near the top Placement.LEFTSIDE_BOTTOM.txt=Left side, near the bottom Placement.RIGHTSIDE_TOP.txt=Right side, near the top Placement.RIGHTSIDE_BOTTOM.txt=Right side, near the bottom Placement.TOP_LEFT.txt=Above, to the left Placement.TOP_RIGHT.txt=Above, to the right Placement.BOTTOM_LEFT.txt=Below, to the left Placement.BOTTOM_RIGHT.txt=Bottom, to the right Placement.LEFTSIDE_CENTER.txt=Left side, centered Placement.RIGHTSIDE_CENTER.txt=Right side, centered
com.sas.util.Enum and the inner Enum.Editor class allows you to control the display of Enum values in property editors. The default display (what you"ll see in the property sheet for a component that has Enum properties) will be just the tags, for example for com.sas.visuals.Placement, the Property Sheet will display just
null TOP BOTTOM LEFTSIDE_TOP LEFTSIDE_BOTTOM RIGHTSIDE_TOP RIGHTSIDE_BOTTOM TOP_LEFT TOP_RIGHT BOTTOM_LEFT BOTTOM_RIGHT LEFTSIDE_CENTER RIGHTSIDE_CENTER
However, if you allow editing of an Enum property in a customizer via a com.sas.visuals.PropertyEditorHost (the preferred way), you can control the display of the Enum values by calling
Enum.Editor.setNextEnumEditorDisplayOptions(boolean displayTags,
boolean displayDescriptions,
boolean listNull)
before your set a property on a PropertyEditorHost. For example, a customizer would do:
Enum.Editor.setNextEnumEditorDisplayOptions(false, true, false); placement.setProperty(style, "placement" ); placement.addPropertyChangeListener(this);
to get a tags list that looks like:
Above and centered Below and centered Left side, near the top Left side, near the bottom Right side, near the top Right side, near the bottom Above, to the left Above, to the right Below, to the left Bottom, to the right Left side, centered Right side, centered
in the PropertyEditorHost.
If a component serializes a property that is initialized from a resource bundle, then that property will no longer be locale sensitive. Thus, components should take care to not serialize such values, and to reinitialize the property to the default resource upon deserialization. One way to achieve this is to make such properties transient, but the drawback is that such values modified in the property sheet or customizer will be lost, so this is not a viable solution.
An alternative solution is to store the locale-specific resource in a static field and initialize the component property to that value. When serializing in a design time environment, if the property value equals (either == or equals(); see Util.equal) the static default, serialize a null value instead.
For example, consider a class MyClass with two String properties, textA and textB. These have the values "Available:" and "Selected:" by default (en_US locale), obtained with
String textA = RB.getStringResource(RB_KEY, "textA.txt"); String textB = RB.getStringResource(RB_KEY, "textB.txt");
MyClass should store these defaults in static values instead:
static String textA_default = RB.getStringResource(RB_KEY, "textA.txt");
static String textB_default = RB.getStringResource(RB_KEY, "textB.txt");
transient String textA;
transient String textB;
...
public void setDefaultValues() {
textA = textA_default;
textB = textB_default;
}
and then override the private void writeObject(ObjectOutputStream stream)
method with the following:
private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject();
writeObject( Util.equal(textA,textA_default) ? null : textA );
writeObject( Util.equal(textB,textB_default) ? null : textB );
}
then override readObject() as:
private void writeObject(java.io.ObjectOutputStream stream)
throws java.io.IOException {
stream.defaultWriteObject();
textA = (String) stream.readObject();
if (textA == null) textA = textA_default;
textB = (String) stream.readObject();
if (textB == null) textB = textB_default;
}
If the field is not a new field in the class but an inherited field one which must be left non-transient (in order to support previously serialized objects), then the proper writeObject/readObject code is:
private void writeObject(ObjectOutputStream stream)
throws IOException {
String textA_orig = textA;
String textB_orig = textB;
try {
if ( Util.equal(textA,textA_default) ) textA = null;
if ( Util.equal(textB,textB_default) ) textB = null;
stream.defaultWriteObject();
}
finally {
textA = textA_orig;
textB = textB_orig;
}
}
private void writeObject(java.io.ObjectOutputStream stream)
throws java.io.IOException {
stream.defaultWriteObject();
if (textA == null) textA = textA_default;
if (textB == null) textB = textB_default;
}
Note that it is insufficient to just write the property value during serializatind and compare it to the default upon deserialization, since the locales may be different, so the field may have been equal to the default at serialization time (for example, "red"), but will be unequal to the default under a different local at deserialization (for example, "rouge"). Thus, some form of normalized value (such as null) must be written to indicate the default.
This level of code complexity is probably not worth the effort.
The situation is more complex if the object constructs other objects based on default values; such objects won"t deserialize in a Locale specific manner. For example, if your class creates a Label object:
Label labelA; ... labelA = new Label(RB.getStringResource(RB_KEY,"labelA.txt"));
then the label object gets serialized with the label and there is no opportunity for MyClass to change that label to a null or some other value that can be detected upon deserialization. Extra fields and logic must be added to detect other (programmatic or property linking) changes made to the other object. For example, MyClass cannot unconditionally do:
public void validateObject()
{
labelA.setText(RB.getStringResource(RB_KEY,"labelA.txt"));
}
since that may undo an explicit customization or other value stored for the label.
If you see an exception like:
java.lang.ExceptionInInitializerError at java.lang.Error.<init>(Compiled Code) at java.lang.LinkageError.<init>(Compiled Code) at java.lang.ExceptionInInitializerError.<init>(Compiled Code)
it can be due to the static initializers of a class failing.
This can happen if the resource bundle could not be loaded.
which in turn can happen if the corresponding Resources.properties file could
not be found, or if the name of the resource file was
incorrect. For example, if in a class in the
com.abc_corp.beans package
you did:
protected static ResourceBundle
resources = java.util.ResourceBundle.getBundle("Resources");
instead of
protected static ResourceBundle
resources = java.util.ResourceBundle.getBundle("com.abc_corp.beans.Resources");
then you might get this exception. You might want to change your getResources method to be:
static public ResourceBundle getResources()
{
if (_resourceBundle == null)
{
try
{
_resourceBundle = ResourceBundle.getBundle("com.abc_corp.beans.Resources");
}
catch (ExceptionInInitializerError e)
{
Throwable t = e.getException();
System.out.println(t);
t.printStackTrace();
}
catch (Exception e)
{
System.out.println(e);
e.printStackTrace();
}
}
return _resourceBundle;
}
You can create an RB class in your packages for convenience and
efficiency. Simply copy the following RB
template as RB.ji and change all occurrences of PACKAGE he the desired
package name and compile. You can use several text processing tools
to do this, such as Perl:
perl -pe s/PACKAGE/com.abc_corp.beans/ < RB.ji > RB.java
or sed
sed s/PACKAGE/com.abc_corp.beans/ < RB.ji > RB.java
or if you do not have perl, the
sed utility, or some other text replacement utility
available, compile the Java program replace.java
and use the Java Runtime Environment provide with webAF and run
java -classpath . replace PACKAGE com.abc_corp.beans < RB.ji > RB.java
(assuming that the webAF.jar file is in your CLASSPATH or Java runtime environment extensions library).
Note: You can add a command like this to your webAF User Tools.