Localization
Translating your application has never been easier...
Concepts
Think global, act local: Often applications should not only be marketed in one country or region, but in several ones. The process of adjusting an application to a specific region is called localization.
J2ME Polish offers a very powerful framework for not only managing the obviously needed translations, but also for adjusting any kind of resources like images or sounds to specific regions. Even enhanced features like locale-aware date-formatting is no problem with J2ME Polish. Traditionally localization involved loading the localized messages from a file during runtime and retrieving these messages with Hashtable-keys. This significantly slows down a localized application and also enlarges the application size. A unique feature of J2ME Polish is that the translations are actually directly embedded into the source-code, so in most cases a localized application has absolutely no overhead at all compared to a non-localized application - both in size as well as in performance.
You can also enable dynamic translations that can be changed during the runtime of the application. In that case any String-keys are converted into simple integer-keys during the preprocessing phase so that translations can be retrieved as fast as possible.
The localization framework extends the concepts of the resource assembling, so that you can for example provide one Nokia specific resource for each supported locale. The localization is controlled by the <localization> element, which is a subelement of the <resources> element.
The <localization> Element and Localized Resource Assembling
You can specify the supported localizations in your build.xml script:
<resources
dir="resources"
excludes="*.txt"
>
<localization unless="test" >
<locale name="en" />
<locale name="de" />
<locale name="cn" encoding="GB2312" />
</localization>
<localization locales="en" if="test" />
</resources>
The <localization> element is responsible for defining which locales should be supported. In the above example the locales "de" (German), "en" (English) and "cn" (Chinese) are used, unless the Ant property test
is set to true
, in which case the application is only build for the English locale.
Locales are defined using the ISO standard of two lowercase letters for a language ("en" for English, "de" for German, "fr" for French and so on) and two optional uppercase letters for the country ("US" for USA, "DE" for Germany, "FR" for France and so on). Possible combinations separate the language and the region with an underline. You can localize your application for French speaking Canadians by supporting the locale "fr_CA" for example.
In each used resources folder you can create a subfolder for a specific locale, e.g.resources/en for general English resources and resources/fr_CA for resources for the French speaking Canadians. The usual specification rules also apply here, so a more specific resource in resources/Nokia/en will override a resource with the same name in resources/Nokia when the English locale is used.
Managing Translations
The Locale Class
The de.enough.polish.util.Locale class is used for the retrieval of translations. It offers three distinct translation methods:
static String get( String name )
,
static String get( String name, String parameter )
, and
static String get( String name, String[] parameters )
.
The following code illustrates the usage of these methods:
import de.enough.polish.util.Locale;
[...]
// getting a simple translation:
this.menuScreen.append( Locale.get( "menu.StartGame"), null);
// getting a translation with one parameter:
this.menuScreen.setTitle( Locale.get( "title.Main", userName ), null);
// getting a translation with several parameters:
String[] parameters = new String[2];
parameters[0] = userName;
parameters[1] = enemyName;
this.textField.setString( Locale.get("messages.Introduction", parameters );
Note: You need to put the ${polish.home}/import/enough-polish-client.jar on the classpath of your project to use the Locale-class in your IDE.
In the file resources/messages.txt the above localizations need to be defined:
menu.StartGame=Start Tickle Fight
# the title of the main-screen with the user-name as the only parameter:
title.Main=Welcome {0}!
# the intro for a new game - with following parameters:
# {0}: the name of the player
# {1}: the name of the remote or computer player
messages.Introduction={1} threatens to tickle you!\n{0} against {1} is loading...
As you can see in the above example you can use parameters in the translations, the first parameter is {0}, the second {1} and so on. You can also use any Java specific characters, e.g. "\t" for a tab or "\"" for using a quotation mark.
The translations are embedded in the actual code during the preprocessing phase, if you have a look at the preprocessed code, you will find the following:
import de.enough.polish.util.Locale;
[...]
// getting a simple translation:
this.menuScreen.append( "Start Tickle Fight", null);
// getting a translation with one parameter:
this.menuScreen.setTitle( "Welcome " + userName + "!", null);
// getting a translation with several parameters:
String[] parameters = new String[2];
parameters[0] = userName;
parameters[1] = enemyName;
this.textField.setString( Locale.get(0, parameters );
The translations for the first two Locale-methods are directly embedded into the sourcecode (unless you are using the dynamic mode) so there is no performance or size impact compared to a non-localized application at all for these kinds of translations.
Only for the third method a call to the Locale class is actually made, but in that call the former String key messages.Introduction
is transformed to a simple integer, thus saving valuable bytes as well as ensuring a fast retrieval of the resource in question.
Defining Translations
All default messages are defined in resources/messages.txt, all German messages in resources/de/messages.txt and all French Canadian resources in resources/fr_CA/messages.txt. You can also use the files resources/messages.txt, resources/messages_de.txt and resources/messages_fr_CA.txt if you prefer to have all translations in one folder. The name of the messages files can be adjusted with the messages
attribute of the <localization>
element by the way.
The translations can be adjusted by the usual hierarchy of the resource assembling, so if you have Nokia-specific translations, these can be defined in the resources/Nokia/messages.txt etc. When an application is localized for the Nokia/N95 phone and the German ("de") language, J2ME Polish tries to find a translation in the following places:
- resources/Nokia/N95/de/messages.txt
- resources/Nokia/N95/messages_de.txt
- resources/[group-name]/de/messages.txt, e.g. resources/Series60/de/messages.txt
- resources/[group-name]/messages_de.txt, , e.g. resources/Series60/messages_de.txt
- resources/Nokia/de/messages.txt
- resources/Nokia/messages_de.txt
- resources/de/messages.txt
- resources/messages_de.txt
When the translation is still not found the same hierarchy is searched again, but this time the default messages.txt file is used. If the translation cannot be found, J2ME Polish will report the error and stop the build. When also a region is specified (e.g. "fr_CA" for French Canadian), J2ME Polish will first try to get a specific "fr_CA" message, e.g. from resources/Nokia/N95/fr_CA/messages.txt, secondly the translation will be searched for the language, e.g. resources/Nokia/N95/fr/messages.txt, before the translation is retrieved from the default messages-file.
In the actual translation files you can insert comments by starting the line with a hash-mark (#). Like in normal Java internationalization you can use parameters within the translations, which are denoted by {0}, {1}, {2} and so on:
menu.StartGame=Start Tickle Fight
# the title of the main-screen with the user-name as the only parameter:
title.Main=Welcome {0}!
# the intro for a new game - with following parameters:
# {0}: the name of the player
# {1}: the name of the remote or computer player
messages.Introduction={1} threatens to tickle you!\n{0} against {1} is loading...
Specifying the Encoding of Messages Files
You can use different encodings of your messages.txt files - all you need to do is to specify the used encoding with the encoding
attribute of nested <locale>
elements. So instead of specifying all the locales within the <localization>
element, you specify each supported localization in a single element:
<resources
dir="resources"
excludes="*.txt"
>
<localization unless="test" >
<locale name="en" />
<locale name="de" />
<locale name="cn" encoding="GB2312" />
</localization>
<localization locales="en" if="test" />
</resources>
When you do not specify an encoding, your operating system's default encoding will be used.
Dynamic Translations
You can also use dynamic translations that can be changed during the runtime of your application. You have to activate dynamic translations by setting the "dynamic"-attribute of the <localization> element to "true" and specifying the "defaultLocale":
<resources
dir="resources"
excludes="*.txt"
>
<localization
dynamic="true"
defaultLocale="en"
locales="de, en, fr_CA"
/>
</resources>
You can now use the Locale class normally, the only difference to using static translations is that the translations are not embedded into your source-code but rather retrieved dynamically from the Locale class. For allowing a fast performance J2ME Polish converts all String-based keys into simple integer-values, so that only a minimal overhead is present.
You can change the translations by calling Locale.loadTranslations( String url )
. For each supported locale J2ME Polish generates a [locale-name].loc
file, e.g. de.loc, en.loc or fr_CA.loc. You can switch to the German translation by calling Locale.loadTranslations( "/de.loc" )
, for example. Don't forget the slash character '/' at the start of the URL.
At application start J2ME Polish will query the microedition.locale
system property and check if a corresponding ${microedition.locale}.loc file is present. If that's not the case, the translations will be loaded from the locale specified in the defaultLocale
attribute of the <localization >
element.
After you have changed a locale during runtime of your application, you need to recreate UI elements that use these translations - have a look at the roadrunner sample application for a real world sample.
External Translations
You can also load translations at a later point in your application. You can use this for limiting the JAR size of your application when you don't know what locale your user prefers during the initial download. Or you can use this technology for providing further translation options to your user.
Follow these steps for externalizing translations - more information about them is given below.
- Turn on the dynamic mode:
<localization dynamic="true" defaultLocale="en"...
- Define your initially required translations in messages_external.txt, messages_external_es.txt, etc and the translations that you want to download later in messages.txt, messages_es.txt, and so on.
- J2ME Polish then generates ${locale-name}.loc files during the build and stores them in the destination directory, typically in ${project.home}/dist.
- Make these *.loc files available on the server, download them in your application and call
de.enough.polish.util.Locale.loadAndStoreTranslations( InputStream in )
.
- From now on you can use all translations defined in your messages_external.txt files, but not the ones defined only defined in the original messages.txt files.
In a typical situation you will have a setup part of your application that relies on the dynamic localization, so that you can guide the user through the setup in different languages. Define these "setup" translations normally in messages.txt, messages_en.txt and so on.
J2ME Polish then generates ${locale-name}.loc files during the build and stores them in the destination directory, typically in ${project.home}/dist.
After the "setup" phase you will know the preferred locale of your user. You can then download a corresponding ${preferred-locale}.loc file from your server and forward the InputStream
to J2ME Polish by calling de.enough.polish.util.Locale.loadAndStoreTranslations( InputStream in )
.
J2ME Polish will then store all translations into the "_translations" record store in your application. At the next application start it will load the external translations automatically.
The following code sample illustrates the download process (in real life you need to add proper exception handling):
HttpConnection connection = (HttpConnection) Connector.open( "http://company.com/locales/en.loc", Connector.READ_WRITE );
connection.setRequestMethod( HttpConnection.GET );
InputStream in = connection.openInputStream();
Locale.loadAndStoreTranslations( in );
in.close();
connection.close();
From now on you can use all translations defined in your messages_external.txt files, but not the ones defined only in the original messages.txt files. For accessing setup translations again you can call Locale.loadTranslations("/" + localeName + ".loc" )
. However, at the next application start J2ME Polish will load the translations again, unless you have removed the "_translations" record store yourself.
Setting and Using Localized Variables
You can set localized variables just by defining them in the appropriate messages-file. Variable-definitions need to start with either "var:" or "variable:":
var:VirtualCurrency=Nuggets
Such variables can also be used within the translations (of course normal variables can also be used):
# The player has won some nuggets, {0} specifies the number of won nuggets:
messages.YouHaveWon=Congrats! You have won {0} ${polish.Vendor}-${VirtualCurrency}!
Naturally the variables can be used during the usual preprocessing in the Java source code as well:
//#= String virtualCurrency = "${VirtualCurrency}";
Using Localized JAD and MANIFEST Attributes
Some JAR- or MANIFEST-attributes need to be localized as well, e.g. the description of the application. This can be done by defining such MIDlet-attributes in the appropriate messages file:
MIDlet-Description=A game where you need to tickle your enemies!
MIDlet-Name=Tickle-Fight
Please compare the documentation of the <info> section for learning the names of the MIDlet-attributes.
Coping with Dates and Currencies
The de.enough.polish.util.Locale class offers some help for dealing with localized content:
static String formatDate( Calendar calendar )
formats a date specific to the current locale, this method is also available for Date and long.
static String LANGUAGE
is a field holding the ISO language code.
static String COUNTRY
is a field holding the ISO country code. This is null when no country is used in the current locale.
static String DISPLAY_LANGUAGE
is a field holding the localized language name, e.g. "Deutsch" for German.
static String DISPLAY_COUNTRY
is a field holding the localized country name, e.g. "Deutschland" for Germany. This is null when no country is used in the current locale.
static String CURRENCY_SYMBOL
is a field holding the symbol of the used currency, e.g. "$" or "?". This is null when no country is used in the current locale.
static String CURRENCY_CODE
is a field holding the three-letter code of the used currency, e.g. "USD" or "EUR". This is null when no country is used in the current locale.
Dealing with Localized Resources
You can store locale dependent resources in subfolders like resources/fr for French resources of resources/fr_CA for Canadian French resources. The resource assembling mechanism will pick them up and store the correct resources in your application's JAR file.
When you use the dynamic localization mode, however, you might want to add resources with the same name several times.
One way to cope with this is to rename resources and add the name of the corresponding locale or language to:
resources/flag_en.png
resources/flag_de.png
resources/flag_cn.png
Another way is to put locale dependent resources into subfolders include the complete folder in your application's JAR file. For realizing this you have to use a <root> element in your build.xml script:
resources/i18n/en/flag.png
resources/i18n/de/flag.png
resources/i18n/cn/flag.png
Within the <resources> element you have to specify the i18n directory as an additional root of which all subfolders should be included:
<resources
dir="resources"
defaultexcludes="yes"
excludes="readme.txt"
>
<root dir="resources/base/i18n"
includeSubDirs="true" includeBaseDir="false" excludes="CVS, readme*"
/>
</resources>
With the above setup you can now load your locale dependent resources like this for example:
String url = "/" + Locale.LANGUAGE + "/flag.png";
Image image = Image.createImage( url );
The advantage of this approach is that you can now add further locale dependent resources just by saving them to the corresponding folder. J2ME Polish is also able to resolve language dependent images automatically: when there is no flag.png file within the root of your JAR file, it tries to load it from the Locale.LANGUAGE
subfolder. For realizing this you have to specify the preprocessing variable polish.i18n.loadResources
to true
:
<variable name="polish.i18n.loadResources" value="true" />
Now J2ME Polish is able to load locale dependent resources specified in styles automatically:
title {
before: url( flag.png );
font-color: green;
}
Localizing the J2ME Polish GUI
The J2ME Polish GUI uses several texts, which can be localized using variables or translations in your messages.txt files. The following table lists the most important translations:
Variable |
Default |
Explanation |
polish.command.ok |
OK |
The label for the OK-menu-item, which is used Screen-menus when the "menu"-fullscreen-mode is used. |
polish.command.cancel |
Cancel |
The label for the Cancel-menu-item. |
polish.command.select |
Select |
The label for the Select-menu-item, which is used by an implicit or exclusive List or ChoiceGroup. |
polish.command.mark |
Mark |
The label for the Mark-menu-item of a multiple List or ChoiceGroup. |
polish.command.unmark |
Unmark |
The label for the Unmark-menu-item of a multiple List or ChoiceGroup. |
polish.command.options |
Options |
The label for the menu when several menu-items are available. |
polish.command.delete |
Delete |
The label for the Delete-menu-item, which is used by TextFields. |
polish.command.clear |
Clear |
The label for the Clear-menu-item, which is used by TextFields. |
polish.title.input |
Input |
The title of the native TextBox which is used for the actual input of text. This title is only used, when the corresponding TextField-item has no label. When the TextField has a label, that label is used as a title instead. |
In this example we specify some of these translations in resources/messages_de.txt for German users:
polish.command.cancel=Abbruch
polish.command.delete=Löschen
polish.title.input=Eingabe
Here is a complete list of all translations with their corresponding default value that are used by J2ME Polish:
polish.command.ok=OK
polish.command.cancel=Cancel
polish.command.select=Select
polish.command.mark=Mark
polish.command.unmark=Unmark
polish.command.options=Options
# for deleting a single char:
polish.command.delete=Delete
# for deleting a complete TextField:
polish.command.clear=Clear
# for hiding menus on BlackBerry devices:
polish.command.hide=Hide
# for showing a default title when no label
# is defined in a TextField. This is only
# used when no direct input mode is activated:
polish.title.input=Input
# used for showing a screen for selecting symbols within TextFields:
polish.command.entersymbol=Add Symbol
# used by the HtmlBrowser for a links:
polish.command.followlink=Go
# used by the HtmlBrowser for submitting a form:
polish.command.submit=Submit
# used by the RssBrowser for returning to the previous page:
polish.command.back=Back
# RSS commands:
polish.rss.command.select=Show
polish.rss.command.followlink=Go
# used for predictive setup
polish.predictive.command.enable=Enable Predictive Input
polish.predictive.command.disable=Disable Predictive Input
polish.predictive.command.install=Install Predictive Input
polish.predictive.registerNewWord.label=New Word:
polish.predictive.registerNewWord.command=Add New Word
polish.predictive.download.title=Predictive Text
polish.predictive.download.message=The predictive dictionary could not be found or has the wrong version. Press OK to exit the application and download the dictionary.
polish.predictive.local.title=Predictive Text
polish.predictive.local.message=The predictive dictionary has the wrong version or could not be found. Press OK to install the dictionary.
polish.predictive.title=Predictive Input
polish.predictive.wordNotFound=Word not found
polish.predictive.setup.title=Predictive Setup
polish.predictive.setup.info=Installing the dictionary used for predictive input. Please wait a moment ...
polish.predictive.setup.status.delete=Deleting previous installation ...
polish.predictive.setup.status.install=Installing ...
polish.predictive.setup.status.finished=Finished, please press exit ...
polish.predictive.setup.cancel=Do you really want to cancel the installation ? This may result in unwanted effects.
polish.predictive.setup.error=An error has occured:
polish.predictive.setup.cmd.cancel=Cancel
polish.predictive.setup.cmd.exit=Exit
polish.predictive.setup.cmd.yes=Yes
polish.predictive.setup.cmd.no=No
Common Traps
Adjusting the JAR-name
You need to remember to adjust the JAR-name of the application in the <info> section of the build.xml, so that the locale is included, otherwise only the last localized application is actually written to the "dist" folder. You can use the variables ${polish.locale} e.g. "fr_CA", ${polish.language} e.g. "fr", and ${polish.country} e.g. "CA" in the jarName-attribute.
An example jarName-attribute is the following:
<info [...]
jarName="${polish.vendor}-${polish.name}-${polish.locale}-example.jar"
Using Quotation Marks and Other Special Characters in Translations
You can use quotation marks as well as any special character if you escape them directly, usually with a backslash-character "\" at the start, e.g.:
Quotation marks: \"
Tab: \t
Backslash: \\
and so on.
In the translations all standard Java escape sequences are supported.
Invalid Locale Calls
Please ensure that the key of the translation is always given directly instead of using a variable, otherwise J2ME Polish will not be able to embed the translation correctly and you end up with compile errors. The following example must not be used:
// never do this:
String key = "menu.StartGame";
this.menuScreen.append( Locale.get( key ), null);
Instead use the key directly in the call as in this example:
// this is just fine:
this.menuScreen.append( Locale.get( "menu.StartGame" ), null);
When you have several parameters, the parameters need to be given in a variable, otherwise J2ME Polish is again unable to process the call correctly:
// never do this:
this.menuScreen.append( Locale.get( "game.StartMessage" , new String[]{ userName, enemyname } ), null );
Instead define the parameters before the actual call:
// this is just fine:
String[] parameters = new String[]{ userName, enemyname };
this.menuScreen.append( Locale.get( "game.StartMessage" , parameters ), null );
JavaDoc
Please refer to the Java Doc documentation of the Locale class for more information:
de.enough.polish.util.Locale