Internationalization (I18n) and Localization (l10n) Guidelines

Overview
Internationalization or i18n is the process of preparing a software project for translation into other languages.

VASSAL uses the standard Java Property file method to achieve this.

The core i18n process is identifying hard-coded strings in the source code, moving them to a properties file and allocating them a Translation key, and replacing the hard-coded string in the source with a call to Resources.getString.

A typical part of a properties file looks like this:

# # Dialogs.disable=Don't show this again Dialogs.show_details=Show details Dialogs.hide_details=Hide details
 * 1) Dialogs

Then, instead of using the code

add(new JLabel("Show Details"));

you use

add(new JLabel(Resource.getString("Dialogs.show_details")));

The Resources.getString function will return the appropriate translation for the "Dialogs.show_details" key.

However, there are a number of special cases that have to be handled carefully, as discussed below.

The Short Version

 * Every source file will need to be checked. Most of the MM and Player is done, very little of the Editor is done.
 * Editor only keys to Editor.properties.
 * MM, Player or Shared keys to VASSAL.properties.
 * General keys are available for commonly used Strings.
 * Translation keys generally named ComponentName.description or Editor.ComponentName.description
 * Add a Translator comment to the properties files where more context is required.
 * Mark lines containing Strings not to be translated with // NON-NLS
 * Build whole sentences or phrases to be translated and pass arguments to getString. Don't build sentences out of translated fragments.
 * Remove ': ' from the end of all Strings. The Configurers will be modified to add them if required.
 * Drop-down list Configurers (StringEnumConfigurer) needs to be replaced with TranslatingStringEnumConfigurer. Read the section on this carefully or get Brent to do it.

Property Files
VASSAL has 2 separate property files. Editor.properties and VASSAL.properties. In general, all Keys used only by the Module Editor will be in Editor.properties file and all other keys will be in VASSAL.properties file. Except when allocating new key names and adding them to one of the property files, you do not need to worry about which property file they are in. Resources.getString will take care of that for you.

Editor.properties
Editor.properties contains all translation keys that are used ONLY in the VASSAL Editor. All translation keys in Editor.properties MUST commence with 'Editor.'

VASSAL.properties
VASSAL.properties contains all translation keys that are used in the Module Manager or Player, including any translation keys that are also used in the Editor.

Translation Keys
As a general rule, where the same string is used in multiple places in the source code, the string should be replaced by the same Translation key. Each properties file has a set of generic Strings with common word, phrases and labels used in VASSAL. These keys should be used where possible.

This list is subject to change and will evolve over time, so you should check the properties files after major changes are pushed. See the initial lists of General strings at the bottom of this page.

Translation keys should generally be named ComponentName.description for VASSAL keys and Editor.ComponentName.description for Editor keys. See the existing entries in the property files for the patter. For example:

PlayerRoster.retire=Retire PlayerRoster.allow_another=Switch sides, become an observer, or allow another player to take your side in this game Editor.SpecialDiceButton.component_type=Symbolic Dice Button Editor.SpecialDiceButton.symbols=Symbols

Translator comments
Where you would like to provide further information or context to the translator to assist with the translation, you can include a comment on the line immediately preceding the translation key in the properties file and this will be presented to the translator.

The translator is presents with three pieces of information with which to make the translation:
 * 1) The Translation Key
 * 2) The English text
 * 3) The text of the comment on the preceding line of the properties file if one exists.

So if you where to add the following key to the Editor.properties file:

Editor.RandomTextButton.faces=Faces

It is not at all clear to a translator how to translate that. The English word Faces by itself has many meanings. Thus might be better:

Editor.RandomTextButton.faces=Faces
 * 1) A Random Text Button is like a Die with text on the Die Faces instead of numbers. This prompt is for the number of Die Faces and should be translated similarly to the DiceButton section

Although this particular example raises the point of whether we should use better English text for this option.

Not to be translated Strings
There are many hard-codes Strings in Vassal that MUST NOT be translated. These are typically internal keys for attributes, Command prefixes etc. Generally, these are never displayed to users and cannot be translated as the internal workings of Vassal require the original English strings.

public static final String NAME = "mapName"; // NON-NLS public static final String MARK_MOVED = "markMoved"; // NON-NLS public static final String MARK_UNMOVED_ICON = "markUnmovedIcon"; // NON-NLS public static final String MARK_UNMOVED_TEXT = "markUnmovedText"; // NON-NLS

Most IDE's will have a setting that can be turned on that flags hard-coded string as 'errors' or 'warnings' that need attention. This makes the process of finding strings that need i18n much easier.

Adding a comment to the end of the line with the string NON-NLS in it will mark those string as not to be translated and should turn off the display of errors or warnings for those strings. Again, dependent on IDE settings.

This setting in Intellij (Settings -> Editor -> Inspections -> Java -> Internationalisation -> Hard Coded Strings) also ignores Strings that have already been Intenationalized.

Compound Sentences
Avoid creating sentences using Vassal supplied text plus individually translated words, sentence fragments or 'formatting' punctuation. Use parameter replacement instead. If you need to create the String

Unit [Ger5-6] did something at Moscow.

Don't do this:

String s = Resources.getString("Component.unit")+" ["+getName+"] "+Resources.getString("Component.did_something_at")+ " " + getLocation; Component.unit=Unit Component.did_something_at=did something at

Do this:

String s = Resources.getString("Component.unit_did_something", getName, getLocation); Component.unit_did_something=Unit [%1$s] did something at %2$s.
 * 1) %1$s will be a unit name. %2$s will be a map location or city name.

This version gives the translator the full context and gives them the flexibility to change the sentence structure if necessary. In some languages, it would not be possible to create a reasonable translation of the first version.

Note the use of the Translator comment to give the translator some idea of what sort of text will replace the parameters.

Configurer Prompt Strings
There are many Strings used in the Editor that are used as labels in automatically generated configurer dialogs. To get the spacing right, these have a colon and 2 spaces ': ' at the end of the String.

The ': ' and two spaces are not to be included in the String sent to the Translators. Instead, the Configurers will be modified to add them if required (and ultimately remove them). Any existing translation Strings that contain ': ' should have it removed.

Many Configurer fields use standard labels shared between many components, so

descConfig = new StringConfigurer(null, "Description: ", p.description);

should be changed to use the existing General string:

descConfig = new StringConfigurer(null, Resources.getString("Editor.description_label"), p.description);

Unique Strings should be added to the Editor.properties files, so

widthConfig = new IntConfigurer(null, "Button Width: ", p.bounds.width);

becomes

widthConfig = new IntConfigurer(null, Resources.getString("Editor.ActionButton.button_width"), p.bounds.width); Editor.ActionButton.button_width=Button Width

Some Configurers are also used by the Player for preferences etc. and have already been translated to include the ': '. These need to be changed, so

Jlabel pwLabel = new JLabel(Resources.getString("Prefs.password_label"); Prefs.password_label=Password:

becomes

Jlabel pwLabel = new JLabel(Resources.getString("Prefs.password_label"); Prefs.password_label=Password

StringEnumConfgurers
StringEnumConfigurers are used to implement drop-down list selection in component configurers and cannot be directlty translated. They need to be replaced with a TranslatingStringEnumConfigurer and a separate list of Translation Keys supplied through an updated constructor.

There are several different ways StringEnumConfigurers are created and used.

Removing Keys and Problem Dialogs
Translation keys that are clearly not used anymore and cannot be found in their supposed component source can be removed.

However, be wary of sets of messages like this:

Error.file_not_found_title=File Not Found Error.file_not_found_heading=File Not Found Error.file_not_found_message=VASSAL could not find the file '%1$s'.

where there are _title, _heading and _message versions of the same key. Your IDE may tell you that they keys are not used and can be removed. However, keys of this form are referenced in the source by the root of the key and the 3 separate keys are referenced programmatically to generate an error dialog. Before removing these sort of keys, you need to search the source for any occurrence of key root "Error.file_not_found".

VASSAL.properties General Strings
General.VASSAL=VASSAL General.add=Add General.save=Save General.cancel=Cancel General.edit=Edit General.edit.shortcut=E General.file=File General.file.shortcut=F General.tools=Tools General.tools.shortcut=T General.help=Help General.help.shortcut=H General.insert=Insert General.menu=Menu General.new=New General.no=No General.quit=Quit General.remove=Remove General.ok=OK General.yes=Yes General.close=Close General.next=Next General.refresh=Refresh General.load=Load General.dock=Dock General.undock=Undock General.select=Select General.open=Open General.properties=Properties General.test=Test General.exit=Exit General.run=Run
 * 1) General Strings

Editor.properties General Strings
Editor.button_icon_label=Button Icon Editor.button_text_label=Button text Editor.tooltip_text_label=Tooltip Text Editor.hotkey_label=Hotkey Editor.color_label=Color Editor.name_label=Name Editor.description_label=Description Editor.report_format=Report Format Editor.menu_command=Menu Command Editor.keyboard_command=Keyboard Command Editor.command_name=Command Name Editor.name_format=Name Format Editor.value=Value Editor.cut=Cut Editor.paste=Paste Editor.copy=Copy Editor.delete=Delete Editor.undo=Undo Editor.move=Move Editor.save=Save Editor.save_as=Save As... Editor.search=Search...
 * 1) General Strings