Internationalization and localization
This section describes various mechanisms in Elastic Path that support multiple languages and currencies.
Cortex can return content in multiple languages and currencies
The Elastic Path Commerce Manager operates in English only, but content can also be displayed in multiple languages and currencies
Character set encoding
The default encoding in Elastic Path is UTF-8
. This applies to:
- Database data
- Database communications
- Content (HTML pages, emails)
- Data files (import data files, report files)
- URLs (catalog browsing URLs, search URLs)
Setting the database encoding
You will need to specify the encoding when you create a database and set your database server to use the same encoding. For example, to use the UTF-8
encoding for a MySQL database, create the database as follows:
create database DB_NAME character set utf8;
Make sure the MySQL Server encoding is set to UTF-8
. In the Server my.ini
file (.../MySQL/MySQL Server 5.1/my.ini
), set the default-character-set to utf8
:
[mysqld]
...
default-character-set=utf8
The corresponding JDBC URI is:
jdbc:mysql://localhost:3306/COMMERCE_DB?autoReconnect=true
note
Specifying the encoding type in the JDBC URL ("jdbc:mysql://localhost:3306/COMMERCE_DB?autoReconnect=true&useUnicode=true&characterEncoding=UTF8
"), won’t be enough to set database communications to use UTF-8
. Character encoding is overridden by the MySQL Server’s character setting.
You may encounter problems when using some Asian encodings that use a double wide character set, which makes some fields require twice as much storage space. In this case, the schema may need to be updated to accommodate the longer strings.
Setting content encoding
Change the "HTML Encoding" value in the Store configuration of the Commerce Manager.
Change the following settings in
velocity.xml
<prop key="template.encoding">UTF-8</prop> <prop key="input.encoding">UTF-8</prop> <prop key="output.encoding">UTF-8</prop>
Change the following settings in
web.xml
:<!-- Encoding filter --> <filter> <filter-name>Encoding Filter</filter-name> <filter-class>com.elasticpath.commons.filter.impl.EncodingFilter </filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter>
Setting the URL encoding
To change the encoding for URLs, modify the URL_ENCODING
constant in com.elasticpath.commons.constants.WebConstants
.
warning
Changing the encoding for URLs is not recommended.
Languages
There are four mechanisms used to support multiple languages:
- Properties files
- Localized properties
- Locale dependent fields
- Attributes
Each mechanism has characteristics that make it most appropriate for specific situations.
Properties files
Properties files are used as the source of language-specific text. The properties files contain simple key value pairs where the key is a description of the String and the value is the corresponding text in a particular language.
Multiple properties files with file names that indicate the language are used to support multiple languages. The language-independent keys in the properties files are used to retrieve the corresponding text in Velocity templates using a call to a Velocity macro as shown below.
#springMessage("productTemplate.itemsAvailable")
The Velocity macro is part of Spring’s integration with Velocity, and it will look up the value of the key for the currently selected Locale. Properties files are generally created for each Velocity template and stored in the same location. The properties file has the same name as the template but has the extension ".properties
" instead of ".vm
". For Strings that are frequently used across multiple pages, the properties are stored in "globals.properties
".
This multi-language mechanism is suitable only for static email content coded into Velocity templates.
Localized Properties
The Localized Properties mechanism is a database-only solution that allows dynamic content such as a store’s brands to be displayed in multiple languages.
To use this mechanism, a domain object with display values in multiple languages has a reference to a LocalizedProperties object containing the language values retrieved by key and locale. All localized property data for all objects is stored as rows in the TLocalizedProperties table. It is not necessary to change the database schema to add new localized properties. See "How to add Localized Properties to a domain object" below for instructions on adding Localized Property support to a new domain object.
Locale Dependent Fields
Locale Dependent Fields is used programatically in a simlar way to Localized Properties. An object has a reference to a LocaleDependantFields
(LDF) object, from which it can retrieve localized Strings.
In this mechanism, the localized properties are stored in columns in the database table and the table can be joined directly with the parent entity table. This means that performance is better than Localized Properties and the field values can be accessed using methods instead of a map key. However, the disadvantage of this approach is that a schema change is required for each new kind of localized field and a new table is required to support Locale Dependent Fields for each new object that requires them. For this reason, Locale Dependent Fields is only used for performance-critical domain objects such as products and categories.
Attributes
The attribute system in Elastic Path also supports multiple languages. Attributes have the advantage that they can be created and maintained by a business user through the Commerce Manager user interface. Attributes are relatively slower than Locale Dependent Fields and are only supported for Products, Categories, and SKUs.
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
The properties files can have a default language or a fallback. For example, when searching for a property file in the en_US
locale, a search for the <filename>_en_US.properties
file is performed first. If the file is not found, the <filename>_en.properties
file is searched. If the search fails, the <filename>.properties
file in the default language is searched.
This also applies for the keys that are undefined for a locale. For a messages_en.properties
file with complete English translations:
- Use the key in the
messages_en_US.properties
file instead of the key in themessages_en.properties
file for theen_US
locale if themessages_en_US.properties
file is defined with a specific translation for only one key in themessages_en.properties
file. - Use the keys in the
messages_en.properties
file for all other keys that are not in themessages_en_US.properties
file. - Use the definitions in the
messages.properties
if the keys are not defined in themessages_en.properties
file.
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
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, a static method is created in CoreMessages.java
to retrieve the message stored in the field of the given name. For more information, see CoreMessages.getMessage(final String messageKey)
). You can now access this public static string, the field, given only the field’s name.
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 (Standard Widget Toolkit)) 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 appended
.nl1
(or.nl2
for second localization pack)
plugin.xml
and MANIFEST.MF
Localization of - Plugins localization entries should live within
plugin.properties
- Requires
Bundle-Localization: plugin
withinMANIFEST.MF
of the plugin - Fragment localization entries should be separate and therefore live within
nl_fragment.properties
*** RequiresBundle-Localization: nl_fragment
withinMANIFEST.MF
of the fragmentnl_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 its 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
)Most classes follow the
<contributionName><contributionType>
naming conventionReusable 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