Elastic Path Commerce Development

String Localization

String Localization

This document describes how Commerce Manager handles string localization. There are two cases to consider, localizations within classes and localization elsewhere. First, a brief overview of localization.

Rather than duplicate information, see Conventions for a list of conventions used within Commerce Manager.

Localization

Localized strings appear in *.properties files either within the plugin itself or within attached fragment plugins. These .properties files may be named anything you want. They are are in java.lang.Properties's key=value format:

# I'm a line comment
EpValidatorFactory_valueRequired=This value is required
!I'm another line comment
EpValidatorFactory_integer=The line can also span multiple\
 lines as long as you escape the newline character

# Spaces between = doesn't matter
EpValidatorFactory_positiveInteger = To display a \\ you need to escape it as well

There must be a *.properties file for each language within the plugin. These files are named <filename><language><country>.properties where <language> and <country> represent the two-letter codes (ISO 639 and ISO 3166) used to signify the language and country. Country and language don't need to be specified, see Default Language below.

Default Language

Properties files can have a "default language" or rather a fallback. For instance when searching for a property file in the en_US locale, a search will look for <filename>_en_US.properties first. If not found, then <filename>_en.properties. If that is not found it resorts to the "default language" <filename>.properties.

This also applies for keys that are undefined for a certain locale. For instance, if you had a messages_en.properties file full of English translations and defined messages_en_US.properties with a specific translation for only one key in messages_en.properties, you would have used the key that lives in messages_en_US.properties instead of the one that lives in messages_en.properties for the en_US locale. For all other keys that don't live in messages_en_US.properties, they will resort back to those that are in messages_en.properties (and finally messages.properties if not in messages_en.properties)

Tip: Tip

To test a locale start the plugin with the -nl <language> or -nl <language>_<country> argument. This must be given to the client as it is undefined for the VM.

Property File Character Encoding

Characters with special accents in *.properties files do not render properly in Eclipse. To fix this issue, convert all your characters with special accents into the corresponding escape sequence. For example, the umlaut character Ö does not render properly, so convert it to: \u00D6

Ö -> \u00D6

This can be done manually by searching and replacing the accent characters in your property files or by using a convertor like Native2ASCII.

Localization Within Classes

Within java code, localized strings can't be looked up automatically, they must be retrieved manually. All Commerce Manager plugins define a class that allows that plugin to retrieve localized strings from a properties file. An short example of the class used is as follows:

public class CoreMessages {
    private static final String BUNDLE_NAME =
        "com.elasticpath.cmclient.core.CorePluginResources"; //$NON-NLS-1$

    private CoreMessages() { }

    public String EpLoginDialog_LoginButton;
    public String ...
    ...

    public String getMessage(final String messageKey) {
        try {
            final Field field = CoreMessages.class.getField(messageKey);
              return (String) field.get(this);
        } catch (final Exception e) {
            throw new MessageException(e);
        }
    }
    public static CoreMessages get() {
        return LocalizedMessagePostProcessor.getUTF8Encoded(BUNDLE_NAME, CoreMessages.class);
    }
}

The static get method takes care of all the initialization. It is important that all the localized string in the class are public strings and not final. It is also important that the names of these static strings occur exactly as they are in the properties file (which means no periods and case sensitive) otherwise getMessage() won't be able to initialize that string.

Other than the keys, one thing must change in order to use this class elsewhere: BUNDLE_NAME. This variable specifies both the package and file name of the properties file to use when initializing the messages. The filename must not include .properties as this is automatically appended. If this file doesn't exist, no exceptions are thrown, you'll just receive "NLS missing message: <key> in: <BUNDLE_NAME>" from the string where your localization is suppose to be.

What if there is some sort of variable in the string? Call the bind() method to do this:

String myMessage = NLS.bind(
    CoreMessages.TemplateMessage,
    "one binding"); //$NON-NLS-1$
myMessage = NLS.bind(
    CoreMessages.TemplateMessage,
    "two bindings", //$NON-NLS-1$
    "for the second"); //$NON-NLS-1$
myMessage = NLS.bind(CoreMessages.TemplateMessage, new Object[] {
    new Object(),
    "more than two objects", //$NON-NLS-1$
    null,
    new Integer(5) });

In this context, it's not at all that interesting. What's important here is that we can insert an arbitrary number of variables into a message. Message reference these variables by inserting {0} for argument 0, {1} for argument 1, and so on. On a side note, if you give a null reference as the object array or not enough arguments in the object array, the string returned will have one or more <missing key> strings within it (no thrown exceptions!). Object arrays that have more objects than there is arguments in the template string will not be used. An important note about this method is that you can give an object to the bind() method, it will just use the toString() method to represent it as a string.

Localizing enum constants

Many messages are stored as constant fields of enum types. For example:

public enum Direction { NORTH, EAST, WEST, SOUTH }

This enum defines the 4 directions. Strings like these must also be localized.

The convention to localize enum types is as follows. Like normal strings, a key-value pair must be inserted into the .properties file and the key of this pair must correspond to a string in the messages class. Unlike normal strings, the string in the messages class must additionally be mapped to its enum constant counterpart. These mappings shall be defined in the respective messages class. For instance:

public static String Direction_North;
public static String Direction_East;
public static String Direction_West;
public static String Direction_South;
public static String Turn_Left;
public static String Turn_Right;

// Define the map of enum constants to localized names
private static Map<Enum< ? >, String> localizedEnums = new HashMap<Enum< ? >, String>();
static {
    localizedEnums.put(Direction.NORTH, Direction_North);
    localizedEnums.put(Direction.EAST, Direction_East);
    localizedEnums.put(Direction.WEST, Direction_West);
    localizedEnums.put(Direction.SOUTH, Direction_South);
    localizedEnums.put(Turn.LEFT, Turn_Left);
    localizedEnums.put(Turn.RIGHT, Turn_Right);
}

All enum constant mappings shall be stored in a single generic hashmap. Note in the above example that the hashmap contains our Direction enum constants as well as Turn enum constants.

Now that the mappings are defined, create a method in the messages class that, given an enum constant, returns the appropriate localized string:

// Returns the localized name of the given enum constant
public static String getLocalizedName(final Enum< ? > anEnum) {
    return localizedPromotionEnums.get(anEnum);
}

Now, when any enum constant needs to be localized, simply call the getLocalizedName() method:

String localizedNorthString = CoreMessages.getLocalizedName(Directions.NORTH);

If a new enum constant is created, create the string in the .properties file and messages class as before, and also added a mapping to the hashmap in the messages class.

Externalized Strings

Within classes, all strings must be externalized or commented specifying that they should be internalized. Take the following for example:

action.run("Title", "desc"); //$NON-NLS-1$ //$NON-NLS-2$

The //$NON-NLS-1$ specifies that the first string on the line should not be externalized, 2 indicates the second. It is not necessary to put a space between these comments, but the double slashes on the second one is required.

Localization When Only The Message Key String Is Available

The majority of localization solutions work as explained above; the localized strings are initialized at startup time based on the chosen locale, and are referenced via public static String keys in the *Messages classes. Any Rich Client code that needs a localized String can ask for the *Messages.Key_Name and get a reference to the localized String. However, there are cases when the message key is only available as a dynamic String; it may not be hard-coded, but is instead determined dynamically at runtime. Perhaps the localized Key is retrieved from the database as a String, or perhaps it exists as a String constant in a class that lives outside the CmClient package hierarchy.

To translate a dynamically-determined key into a "public static String" key that exists in the *Messages file, so that you can access this public static string (the field) given only the field's name, a static method has been created in CoreMessages.java to retrieve the message stored in the field of the given name (see CoreMessages.getMessage(final String messageKey)).

An example of this usage can be found in the implementation of an AttributeType. The system consists of a defined group of AttributeType's, each with a defined name to display to the user, which needs to be localized. AttributeType was changed to include a getNameMessageKey() to return a message key to look up the localized message. Each AttributeType's message key and field name were then added to the CoreMessages.java and CorePluginResources.properties files. Now, using CoreMessages.getMessage(final String messageKey) and passing in the message key, the method will return the localized message. A runtime exception will be thrown if the message key does not exist in CoreMessages.java, as the message key is always expected to exist.

Localization Elsewhere

Files that are not within classes could also require string localization. These string are all put into another properties file, which has the same format as other properties files. The name is plugin.properties by convention but could be named otherwise. To specify that a plugin should use plugin.properties, Bundle-Localization: plugin should be inserted into the plugins MANIFEST.MF file. This is only required in the host plugin with respect to fragments. Bundle-Localization specifies a path to a file, relative to the root of the plugin, where the properties file is stored. The file name must not include .properties because this is automatically appended.

Localization can happen almost anywhere, including within MANIFEST.MF and plugin.xml. Both of these use plugin.properties to store their string localizations.

There exists a special file called about.mappings (required name) that can be used here as well. This file is useful for storing global strings common to all lanugages (i.e. a version number). It is important to note though that mappings in this file are only used within the about text (Help->About...) which isn't really useful, but helpful to know. The file has the same format as *.properties files except that keys must be non-negative integers.

Fragments

Think of a fragment as a half-plugin. It's not quite big enough to be a whole plugin, but big enough to be modularized. Things that are stored in here are generally aren't required for the host plugin (i.e. OS specific bindings for SWT) or localization files. Fragments reference plugins which means that a fragment can reference only 1 plugin and not multiple.

Conventions

  • File Naming
    • The messages class should be named <pluginName>Messages
    • Properties files should be named <pluginName>PluginResources
    • In the case of multiple messages classes, <pluginName> should be replaced with something meaningful
    • Fragments are named the same as the referencing plugin with appened .nl1 (or .nl2 for second localization pack)
  • Localization of plugin.xml and MANIFEST.MF** Plugins localization entries should live within plugin.properties*** Requires Bundle-Localization: plugin within MANIFEST.MFof the plugin
    • Fragment localization entries should be separate and therefore live within nl_fragment.properties*** Requires Bundle-Localization: nl_fragment within MANIFEST.MFof the fragment
      • nl_fragment.properties only needs to be included in the fragment and not the plugin
    • about.mappings should live within the plugin (if required by a plugin)
  • Location
    • Each plugin will have it's own specialized messages class located in the root package (i.e. src/main/com/elasticpath/cmclient/core/CoreMessages.java)
    • The properties file for this messages class will live in the same location
    • There should be only one messages class, although some extenuating circumstances might require more than one
  • Properties files
    • All comments should use # instead of !
  • Property Keys
    • Keys should generally follow the convention <contributionName><contributionType>_<something> (i.e. productEditor_skuCode)
      • Note: Most classes follow the <contributionName><contributionType> naming convention
    • Reusable keys should try not to follow the above convention
  • Strings
    • Every string in classes must either be externalized or have //$NON-NLS-x$comments
      • Multiple strings on the same line are specified by //$NON-NLS-1$ //$NON-NLS-2$ for the first and second string on the line
  • Default Language
    • Default language files (.properties) will live in their respective directories within the *plugin not the fragment
    • All other language files will live inside a fragment