Internationalization (I18n) and Localization (l10n) Guidelines

= Internationalization (i18n) Guidelines for Vassal Developers =

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.

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  method will return the appropriate translation for the   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. As of Summer 2020, most of the Module Manager (MM) and Player are done, but very little of the Editor is done.
 * Editor-only keys go in.
 * Module Manager, Player or Shared keys go in.
 * keys are available for commonly-used strings.
 * Try to avoid creating a new translation key for a String that already has an existing Key.
 * Translation keys are generally named  or
 * Add a Translator comment to the properties files where more context is required.
 * Consider rewording Strings that are unclear, ambiguous, or use uncommon or overly technical words or phrases.
 * Mark lines containing strings not to be translated with
 * Build whole sentences or phrases to be translated and pass arguments to . Don't build sentences out of translated fragments.
 * Remove  from the end of all atrings. The Configurers will be modified to add them if required.
 * Drop-down list Configurers (e.g. ) need to be replaced with  . Read the section on this carefully or get Brent to do it.

Property Files
VASSAL has two separate property files. and. In general, all keys used only by the Module Editor will be in  file and all other keys will be in the   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. will take care of that for you.

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

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  strings at the bottom of this page.

Translation keys should generally be named  for VASSAL keys and   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 presented with three pieces of information with which to make the translation:
 * 1) The translation key (e.g.  )
 * 2) The English text (e.g.  )
 * 3) The text of the comment on the preceding line of the properties file if one exists (comments begin with the   character).

So if you were 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. This 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.

Consider rewording any English text that is inconsistent, unclear, ambiguous, or uses overly technical words or phrases.

Not-to-be-translated Strings
There are many hard-coded 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  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 -> Internationalization -> Hard Coded Strings) also ignores strings that have already been internationalized.

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). The  should be removed from any existing translation strings that contain it.

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  string:

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

Unique Strings should be added to the  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

StringEnumConfigurers
instances are used to implement drop-down list selection in component configurers and cannot be directly translated. They need to be replaced with a  and a separate list of translation keys supplied through an updated constructor. There are several different ways  instances are created and used.

StringEnumConfigurers 1 - Trait usage
For Traits, just directly instantiate an instance of StringEnumCofigurer and pass an array of Translation keys as an extra parameter to the constructor

destInput = new StringEnumConfigurer(null, "Destination: ", new String[] {"Dest1", "Dest2"}); destInput.setValue("Dest1");

becomes

destInput = new StringEnumConfigurer(null, Resources,getString("Editor.ThisTrait.destination", new String[] {"Dest1", "Dest2"}, new String[] {"Editor.ThisTrait.destination_1", "Editor.ThisTrait.destination_1" }); destInput.setValue("Dest1");

StringEnumConfigurers 2 - Local StringEnum class
This is the simplest implementation for AbstractConfigurables. A local static class is specified in :

public Class<?>[] getAttributeTypes { return new Class<?>[]{ F.class, String.class,

And the local class overrides :

public static class F extends StringEnum { @Override public String[] getValidValues(AutoConfigurable target) { return new String[]{"Horizontal first", "Vertical first"}; } }

This case is handled by the  and a   is automatically created for you. To update this case, you just need to change the local class to extend  and add the translation keys as follows:

public static class F extends TranslatableStringEnum { @Override public String[] getValidValues(AutoConfigurable target) { return new String[]{"Horizontal first", "Vertical first"}; }  @Override public String[] getI18nKeys(AutoConfigurable target) { return new String[]{"Editor.GridNumbering.horizontal_first", "Editor.GridNumbering.horizontal_first"}; } }

Editor.GridNumbering.horizontal_first=Horizontal first Editor.GridNumbering.verticalfirst=Vertical first
 * 1) GridNumbering

The values returned by  are the actual values that will stored in the build file. returns the list of Editor keys used to provide the corresponding drop-down list entries.

NOTE: This assumes that "Horizontal first" and "Vertical first" are the actual values stored in the buildfile. See example 4 below for cases where they are not.

StringEnumConfigurers 3 - Local ConfigurerFactory class
This case is similar to case 1, but the local class implements ConfigurerFactory and explicitly constructs a Configurer:

public Class<?>[] getAttributeTypes { return new Class<?>[] { String.class, SortConfig.class,

public static final String ALPHA = "alpha"; //$NON-NLS-1$ public static final String LENGTHALPHA = "length"; //$NON-NLS-1$ public static final String NUMERIC = "numeric"; //$NON-NLS-1$ public static final String[] SORT_OPTIONS = { ALPHA, LENGTHALPHA, NUMERIC };

public static class SortConfig implements ConfigurerFactory { @Override public Configurer getConfigurer(AutoConfigurable c, String key, String name) { return new StringEnumConfigurer(key, name, SORT_OPTIONS); } }

Convert the local class as follows:

public static final String ALPHA_KEY = "Editor.Inventory.sort_option_alpha"; //$NON-NLS-1$ public static final String LENGTHALPHA_KEY = "Editor.Inventory.sort_option_length"; //$NON-NLS-1$ public static final String NUMERIC_KEY = "Editor.Inventory.sort_option_numeric"; //$NON-NLS-1$ public static final String[] SORT_OPTION_KEYS = { ALPHA_KEY, LENGTHALPHA_KEY, NUMERIC_KEY };

public static class SortConfig implements ConfigurerFactory { @Override public Configurer getConfigurer(AutoConfigurable c, String key, String name) { return new TranslatingStringEnumConfigurer(key, name, SORT_OPTIONS, SORT_OPTION_KEYS); } }

Editor.Inventory.sort_option_alpha=Alphanumeric Sort Editor.Inventory.sort_option_length=By Length, then Alphanumeric Editor.Inventory.sort_option_numeric=Numeric Sort
 * 1) Inventory
 * 2) Option to sort strings alphanumerically
 * 1) Option to sort by length of string, then alphanumerically
 * 1) Option to treat the data as number as sort numerically

In this case, I have also taken the opportunity to make the drop-down options more meaningful and have provided Translator comments to try and help them understand what is going on.

StringEnumConfigurers 4 - Overridden StringEnumConfigurer class
There are several cases where a  returns a custom class that overrides. The custom class needs to be rewritten to override  and pass the Editor keys to the constructor.

StringEnumConfigurers 5 - Alternate values stored to drop-down list values
Check the  method to see if the component is converting the supplied values before saving them. For example, in our example 1 for RegularGridNumbering,  is doing this:

public void setAttribute(String key, Object value) { if (FIRST.equals(key)) { first = ((String) value).charAt(0); }

and is only storing the first letter 'H' or 'V' into the local variables and thus into the buildFile. In this case, the static  class needs to look like this:

public static class F extends TranslatableStringEnum { @Override public String[] getValidValues(AutoConfigurable target) { return new String[]{"H", "V"}; }  @Override public String[] getI18nKeys(AutoConfigurable target) { return new String[]{"Editor.GridNumbering.horizontal_first", "Editor.GridNumbering.horizontal_first"}; } }

The  entries remain unchanged.

Removing Keys and Problem Dialogs
Translation keys which 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,   and   versions of the same key, your IDE may tell you that these 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 the key root (e.g. in this case ).

Translating Error Messages
Error messages written to the Vassal error log file should not be translated.

Default Command Names
Many traits hard-code a default command name into the no-arg constructor like this:

public Delete { this(ID + :Delete;D", null); // NON-NLS }

We need to provide an appropriate translated default command name as follows:

public Delete { this(ID + Resources.getString("Editor.Delete.delete") + ";D", null); // NON-NLS }

Editor.Delete.delete=Delete
 * 1) Default Delete command

Trait Descriptions
Traits provide a brief descriptive name that appears in the Piece Definition dialog:

public String getDescription { return Resources.getString("Delete"); }

We need to provide a suitable translation:

public String getDescription { return Resources.getString("Editor.Delete.trait_description"); }

Editor.Delete.trait_description=Delete
 * 1) Description of the Delete trait seen by users

Module level translation descriptions of translatable elements
At the bottom of each trait is the implementation of the getI18nData which returns a list of the fields in the trait that can be translated at the module level. We need to provide a translated version of these descriptions. So:

public PieceI18nData getI18nData { return getI18nData(commandName, "Delete Command"); }

becomes:

public PieceI18nData getI18nData { return getI18nData(commandName, Resources.getString("Editor.Delete.delete_command_description")); }

Editor.Delete.delete_command_description=Delete Command
 * 1) Delete Command Name description seen by module translators