Resources

SAS® AppDev Studio 3.0 Developer's Site

Internationalization for Java Components

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.

Overview

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.

I18N Support in com.sas Components

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.

What Needs Internationalization?

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.

Message Strings

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.

Resource Bundles

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.

com.sas.Resources and com.sas.RB

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.

com.sas.util.CommonResources

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.

ExtendedBeanInfo

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.

Resource Name Conventions

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.

  1. Each resource tag should begin with the class name, with the exception of common/shared resources that may be shared across the entire package.
  2. Avoid using spaces before and after the = in the properties file.
  3. Use one of the following suffixes
    .txt
    All translatable text should have the .txt suffix. This is a key which the translators can look for during the L10N process. All .txt items will be translated.
    .txt should also be used for other localizable values, such as entries which refer to help files, URL"s, and so on.
    .fmt.txt
    Translatable text which is used as input to java.text.MessageFormat formatting should use a .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
    images. Images may be localized. These resources are typically the URL or path location of an image resource. The image is localized by redirecting the resource to a different image.
    .url
    Web URLs or other forms of links (URIs, etc.). For example, a resource may be the URL of a help file. Thus URL may be localized for different languages.
    .audio
    audio clips.
    .media
    other media clips
    .object
    some other object
    .notrans
    a non-translated String. This should be used minimally.
    .classname
    a non-translated class name
  4. Use <classname>.<property-name>.pd.txt for property descriptors, such as DualSelector.dynamic.pd.txt
  5. Use <classname>.<property-name>.spd.txt for the short property description, such as DualSelector.dynamic.spd.txt
  6. Use <classname>.<exception-name>.ex.txt for exception messages used to create exception objects, such as DualSelector.invalidCount.ex.txt
  7. Common/shared resources need no class name prefix. This can be within a package, or global to many packages. (See the com.sas.util.CommonResources bundle, above.)
  8. Use case change at word boundaries, for example ViewInterfaceSupport.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

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.

Serialization

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.

Initialization Errors

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;
}

RB.java

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.