JMRI: Internationalization
This page discusses how the JMRI libraries handle internationalization.The JMRI libraries are intended to be usable world-wide. To make this possible, they make use of the "internationalization" features built into the Java language and libraries.
Use of Locales
JMRI uses the default Locale for locating internationalization information. That means that JMRI will present its user interface in the language Java has defined as the default for that computer.Locals are specified by a language, and optionally a country. The language is a two letter lower-case code; the country is a two letter upper case code. "en" is English, "fr" is French, "de" is German, and "de_CH" is German as spoken in Switzerland.
When Java looks for resources (see below), it searches first for a file with the complete current locale at the end of it's name (e.g. foo_de_CH.properties). If that fails, it tries for a file ending in just the current locale's language: foo_de.properties. And if that fails, it goes to the defaults with no suffix: foo.properties. A similar mechanism is used within XML files.
By installing appropriate files and allowing the user to select a default locale (as part of the advanced preferences), we can customize the program to different countries and languages.
Use of Resource Bundles
The text for menus, buttons and similiar controls is contained in property files, which are accessed via the Resource Bundle mechanism of java.util.For example, the property file that's used to configure the Roster panel contains lines like:
FieldRoadName = Road Name:To the left of the equal sign is the resource name that the program uses to refer to the string; to the right of the equals sign is the string that will be displayed.
By convention, resouce names for GUI elements start with one of
- Field - for a visible field, e.g. label, on the GUI
- Button - for a GUI button
- Menu - the name of a top-level menu
- MenuItem - an item in a menu (may be a nested item)
- ToolTip - contents of a tooltip
- Error - for an error message displayed as part of the GUI
Adapting to a new language
The primary steps to adapt JMRI to a new language are:- Create new versions of the .properties files to change the language of the GUI controls.
- Translate the XML files for decoders, programmers and configuration.
- Translate the help files and other web pages.
Get a clean copy of the source code using SVN. (For more info on using SVN, please see the page on getting a copy of the code.)
Translating Properties Files
If they don't exist already, start by making copies of the properties files with suffix for your new locale. On a MacOS X or Unix machine, this would be:cd java/src/apps cp AppsBundle.properties AppsBundle_xy.propertiesand so on. The easiest way to find the proper suffix letters is to set the program to your particular language via the advanced preferences, quit and restart the program, and then look at the suffix that it displays on the main window. You can also check the official list of languages (first part of the suffix) and list of countries/regions (optional second part of the suffix).
You then edit the language-specific files to enter text in your own language. Please don't edit the lines in the file that contain things like $Release: $; those are used by SVN to keep track of the history of changes.
Languages that involve non-roman letters require some extra care. The property files must contain only ISO 8859-1 characters. If you want to use unicode characters, these must be manually escaped. (The 'native2ascii' command-line tool can help with this.) Please see the Java internationalization FAQ for more info on how to include those characters in your property files, particularly the question on "How do I specify non-ASCII strings in a properties file?". An example is the java/src/apps/AppsBundle_cs_CZ.properties file, which contains diacritical letters for the Czech translation.
There are several properties files that are used for internal control, and should not be translated. These are marked by a comment at the top of the file. Examples are the apps/AppsStructureBundle.properties, jmri/web/server/Services.properties and jmri/web/server/FilePaths.properties files.
To check your work:
- Rebuild your copy of the program.
- Start the program and select "Preferences" from the Edit menu
- Click the "show advanced preferences" box
- Select your language from the "Locale" drop-down box,
- Click "Save", quit and restart
- You should immediately seen the items you've translated.
If there's a problem at this point, check to see what language is listed on the application startup screen. Is it showing the same suffix (e.g. _fr or _cs_CZ) as you gave to your files? The suffix the program uses is determined by the Locale you selected in the preferences above.
To make your work available to other JMRI users, please share it with us. To do that:
- Make a patch file containing your changes.
On MacOS X or Linux, this is done with the command
and Windows users can do a similar thing from their SVN application.svn diff java/src > patch.txt
- Upload that file to the "Patches" tracker on sourceforge:
http://sourceforge.net/tracker/?group_id=26788&atid=388315.
On that page:- Click "Create ticket" on the left side
- Fill out the title and summary on the new page that appears (you can lead status, owner, type and priority as they are),
- Click the "I would like to add an attachment" box near the bottom,
- and then select your patch file using the "Choose File" button.
- Click "Save" to upload the file and notify people that you've done this.
Translating XML files
XML files can also be internationalized. There are examples in the decoder definition directories. Look for elements with a xml:lang="fr" attribute. Basically, you create additional elements with that attribute to specify the language used:
<variable label="Vmid" CV="6" default="22" item="Vmid">
<decVal max="64"/>
<label>Vmid</label>
<label xml:lang="fr">Vmoy</label>
</variable>
In the XML files, the 'item' attributes have to stay untranslated, as
does the entire xml/names.xml file.
The English help files are found in the help/en directory.
If you want to create a complete set of files:
Some web references on how to do this:
JMRI is moving toward a set of conventions on how to structure and
use the large amount of I18N information required.
You'll still find code with older approaches, but you
should write new code using the new conventions.
JMRI resource bundles are organized in a heirarchical tree.
For example, code in the jmri.jmrit.display package
may find a resource within a bundle in the jmri.jmrit.display package, jmri.jmrit package
or finally the jmri package. As a special case in this, the apps package is
viewed as being below the jmri package itself, so code in the apps tree also
can reference the jmri package.
Cross-package references, e.g. between jmri.jmrit and jmri.jmrix, are discouraged
and existing ones are being removed.
Access is via a Bundle class local to teach package. A typical one is
Translating help files
(This has only been done once, so these instructions may not be complete)
<HTML LANG="fr"> tag.
Internationalization for Developers
For internationalization to work, you have to do a few things in the code
you write.
static String getMessage(String key) static String getMessage(String key, Object... subs)The first provides direct access to a string via
String msg = Bundle.getMessage("Title").
The second is used to insert specific information into a message like
System name LT1 is already in use
Here "LT1" can't be in the properties file, because it's only known which name to display when the program is running. Different languages may put that part of the message in different places, and supporting that is important. That's handled by putting a placeholder in the message definition:
Error123 = System name {0} is already in use
(You can have more than one insertion, called {1}, {2}, etc)
Next, format the final message by inserting the content into it:
String msg = Bundle.getMessage("Error123", badName);
The first argument is the message key followed by one or more strings to be inserted into the message. (This is better than creating your own output string using e.g. String.format() because it allows the inserted terms to appear in different orders in different languages.)
Different languages may need a different number of lines to express a message, or may need to break it before or after a particular value is inserted. It's therefore better to use "\n" within a single message from the properties file to create line breaks, rather than providing multiple lines in the code itself.
Some parts of JMRI remain English only due to our developer population.
In particular, comments and variable names in the code should remain in
English, as should messages sent to the logging system.
In the Java code, these strings can be marked with a "// NOI18N"
comment at the end of the line.
Adding a new Bundle
If your package does not already have a Bundle class, you can add it by:- Bring a copy of the Bundle class into your package:
svn cp java/src/jmri/jmrit/Bundle.java java/src/jmri/mypackage/ - Edit that new file in three places:
- The 'package' statement at the top should list your package
- The 'class ... extends' should refer to the Bundle class directly above your package
- The assignment to the 'name' variable should be the name of your local bundle, by convention "jmri/mypackage.Bundle".
- Create a new Bundle.properties file in your package directory to hold your properties strings.
- Ideally, you'll add a copy of java/test/jmri/jmrit/BundleTest.java to your
JUnit test directory to check that your strings are working:
svn cp java/test/jmri/jmrit/BundleTest.java java/test/jmri/mypackage/followed by editing the "package" statement in that file to point to your package, adding a few of your strings for testing (including ones you reference from parent bundles, if any), and including a reference in your PackageTest class.
Older code
Older code typically references the bundles directly:
java.util.ResourceBundle.getBundle("jmri.jmrit.beantable.LogixTableBundle");
The getBundle argument is the complete package name (not file name) for the properties file this class will be using. You can have reference more than one of these objects if you'd like to look up strings in more than one properties file.
You can then retrieve particular strings like this:
java.util.ResourceBundle.getBundle("jmri.jmrit.beantable.LogixTableBundle").getString("ButtonNew");
We no longer recommend defining a class-static variable to hold the reference to the Bundle object, as this ends up consuming a lot of permanent memory in a program the size of JMRI. Go ahead and call the getBundle() each time, it's fast because it works through a weakly-referenced and garbage-collected cache.
XML Access
Second, you have to retrieve XML elements and attributes properly.
The jmri.util.jdom.LocaleSelector provides a getAttribute(...) method
that replaces the JDOM getAttribute element when the content of the
attribute might have been internationalized. You use it like this:
where "choiceElement" is a JDOM Element object containing a (possible translated)
"choice" attribute. Null will be returned if nothing is found.
String choice = LocaleSelector.getAttribute(choiceElement, "choice")
Testing
You should check that you've properly internationalized
your code. We provide a tool for doing this which creates
and automatically translated version of your properties files,
following the ideas of Harry Robinson and Arne Thormodsen.
(Their
paper on this is recommended reading!)
To use it:
- Make sure your code compiles and builds OK. We'll be modifying the compiled version.
- Run the "translate.sh" script in your java/ build directory. This creates new, temporary properties files in the classes/ directory tree. You will have to redo this every time the classes/ tree is removed by e.g. "ant clean" or an clean IDE build.
- Run DecoderPro via "ant locale", which starts the DecoderPro program using the new properties files.