PSA: Externalize all your strings to make translators live easierThere is nothing worse to translate that bunch of hardcoded strings intertwined with logic. Take my code for example (
PersonSubject.java):
private void addPerson(TooltipMakerAPI info, PersonAPI person) {
List<SkillLevelAPI> skills = person.getStats().getSkillsCopy();
CollectionHelper.reduce(skills, new SkillLevelFilter());
int skillSize = skills.size();
String numberOrNo = skillSize == 0 ? "no " : skillSize + " ";
String skillOrSkills = skillSize == 1 ? "skill" : "skills";
String dotOrColon = skillSize == 0 ? "." : ":";
String adminPara = "%s, level " + person.getStats().getLevel() + " " + entity + " with " + numberOrNo
+ skillOrSkills + dotOrColon;
info.addPara("", 0f);
info.addPara(adminPara, 10f, Misc.getTextColor(), Misc.getHighlightColor(), person.getNameString());
addSkills(info, skills);
}
This method adds a following string: "My Name, level 2 steady officer with 3 skills:".
While the mod is now successfully
translated to Chinese, the pains through which the translator had to go have been considerable, and do not end here. Every time a new version is released, the translator will either have to:
- Rebase his fork, resolve all conflicts, and go through all code base again to find and translate new strings,
- Backmerge upstream, resolve all conflicts, and go through all new strings in the backmerged code,
- Have a meltdown if not using source control (slightly less so if using a diff tool).
Then the translator needs to recompile your mod, and re-release it. The whole process is both inefficient and error prone. Moreover, it risks introducing bugs that the upstream version had not had.
Fortunately, internationalization (i18n) and localization (l10n) are well known problems and as such they are already solved problems too.
Java has a built-in support for both -
ResourceBundle. Starsector also has tools that will let you create translate-friendly mod. Let's explore both.
Java way- Pick one way of storing your translatable strings: you can extend ResourceBundle or ListResourceBundle, or use properties files. In this guide we will use properties files as we want to detach strings from source code altogether.
- Move all strings to one file and replace them with keys.
- Make localized copies of the file holding your strings.
Example (taking the code from above)translations.properties:
person.skills = {0,choice,0#no skills.|1#one skill:|1<{0,number,integer} skills:}
person.adminPara = %s, level {0} {1} with {2}
addPerson:
private void addPerson(TooltipMakerAPI info, PersonAPI person) {
List<SkillLevelAPI> skills = person.getStats().getSkillsCopy();
CollectionHelper.reduce(skills, new SkillLevelFilter());
int skillSize = skills.size();
// next 2 lines should be in a separate, reusable class
Locale locale = Locale.getDefault();// this could be new Locale("zh", "CN") for example, or fetched through Global...getSettings()
ResourceBundle messages = ResourceBundle.getBundle("translations", locale);
// our class from above should expose getFormattedString(String key, Object... args) as it will be used often
String skills = MessageFormat.format(messages.getString("person.skills"), skillSize);
String adminPara = Misc.ucFirst(messages.getString("person.adminPara"));
info.addPara("", 0f);
info.addPara(adminPara, 10f, Misc.getTextColor(), Misc.getHighlightColor(), person.getNameString());
addSkills(info, skills);
}
Done. No strings in the code and translator only needs to worry about two files: translations.properties and, in case of Polish translations for example, trasnlations_pl_PL.properties.
translations_pl_PL.properties:
person.skills = {0,choice,0#zero umiejetnosci.|1#jedna umiejetnosc:|1<{0,number,integer} umiejetnosci:}
person.adminPara = {1} %s ma poziom {0} i posiada {2}
The string added to intel would be now: "Steady officer My Name ma poziom 2 i posiada 3 umiejetnosci:" (forgive my terribad translating skills). Obviously, similar steps need to be done in the parent class, so that "entity" (String) is translated.
Not only did we decouple translatable strings from code, we also isolated language specific rules from the code! Translator can now decide what forms to use when doing enumeration and move partial objects within a translated string to match language specific grammar rules.
Starsector wayVery similar approach as above, just move all strings to a CSV file located somewhere in your "data" folder and use "getMergedSpreadsheetDataForMod(String idColumn, String path, String masterMod)" or to a JSON file and use "getMergedJSONForMod(String path, String masterMod)".
Your translator will then be creating a new mod that contains a translated CSV (or JSON) file and thus "getMergedSpreadsheetDataForMod" (or "getMergedJSONForMod") will take overwritten strings.
This approach also allows to easily translate text in "rules.csv" (which does mix logic with text, see Problems below) and other places (like faction or ship descriptions).
Example (Java way)All of this came up as I was working on my new mod, and as such there is a proof of concept available (very rough)
on Github.
Notice the lack of Polish characters (since I am using Chinese version of Starsector I do have Chinese characters). Also forgive the bug with some Polish highlight.