Fractal Softworks Forum

Please login or register.

Login with username, password and session length

Author Topic: [TUTORIAL] Creating translation friendly mod  (Read 3289 times)

Jaghaimo

  • Admiral
  • *****
  • Posts: 661
    • View Profile
[TUTORIAL] Creating translation friendly mod
« on: November 17, 2020, 03:53:32 AM »

PSA: Externalize all your strings to make translators live easier

There is nothing worse to translate that bunch of hardcoded strings intertwined with logic.  Take my code for example (PersonSubject.java):

Code
    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:
Code
person.skills = {0,choice,0#no skills.|1#one skill:|1<{0,number,integer} skills:}
person.adminPara = %s, level {0} {1} with {2}

addPerson:
Code
    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:
Code
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 way

Very 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.

« Last Edit: November 17, 2020, 08:56:19 AM by Jaghaimo »
Logged

Jaghaimo

  • Admiral
  • *****
  • Posts: 661
    • View Profile
Re: [TUTORIAL] Creating translation friendly mod
« Reply #1 on: November 17, 2020, 03:56:07 AM »

Problems

Language selection

Java way - you need to decide which locale to use. Starsector uses "en_US" as default locale. There is "localeOverride" property in settings.json but you can't change it on per-mod basis, thus it should be left to the user to change it in the "starsector-core" directly. Your best bet would be to introduce your mod-specific variable and have that control locale selection.

Starsector way - non-existent as the user downloads an add-on for your mod (language pack basically).

StringMessage.format is awkward

It is still better than coming with home-brewed solutions. It is an industry standard with a lot of online help and examples available. To make it easier for translators you could use comments in properties file, or even use a MapFormat instead of StringMessage.

Fonts and encoding

For any language that uses non-ASCII compliant characters a new bitmap font has to be generated with those characters, otherwise each missing character will be replaced with ? (that's why I didn't use Polish characters with diacritical marks).  Moreover, if going "Java way" you need to encode those using ASCII and Unicode escaped sequences (see native2ascii program bundled with Java).  If going with "Starsector way" you can just type and save the file as it is.

rules.csv

This text file contains both script logic and text (in the form of text and options). You can leave it as is and pray the translator does not break anything, or you can go a step further and introduce additional helper classes.  For example, you can create a PopulateOptions and GetDescription classes that will, in its execute() calls, fetch localized dialog options / dialog text, and populate those accordingly.

Now all your strings live in one place (file or folder depending on your implementation).
« Last Edit: November 17, 2020, 08:56:43 AM by Jaghaimo »
Logged

Tartiflette

  • Admiral
  • *****
  • Posts: 3529
  • MagicLab discord: https://discord.gg/EVQZaD3naU
    • View Profile
Re: [TUTORIAL] Creating translation friendly mod
« Reply #2 on: November 17, 2020, 08:26:16 AM »

There is a built-in externalization tool in the API:

Code
Global.getSettings().getString(String category, String id)

I usually code a shorthand function in my mods to make it easier to use, something like
Code: java
package data.scripts.util;

import com.fs.starfarer.api.Global;

public class SKR_txt {   
    private static final String ML="seeker";   
   
    public static String txt(String id){
        return Global.getSettings().getString(ML, id);
    }       
}
Logged
 

Jaghaimo

  • Admiral
  • *****
  • Posts: 661
    • View Profile
Re: [TUTORIAL] Creating translation friendly mod
« Reply #3 on: November 17, 2020, 08:52:18 AM »

I've added "getMergedJSONForMod" to the OP as an alternative to "getMergedSpreadsheetDataForMod". I guess getString would work as well (since all ids from the same file, across various mods, would merge). Both "getMergedSpreadsheetDataForMod" and "getMergedJSONForMod" are more explicit (in names), and safer as you don't have to worry about mod loading order.
« Last Edit: November 17, 2020, 08:55:30 AM by Jaghaimo »
Logged

Tartiflette

  • Admiral
  • *****
  • Posts: 3529
  • MagicLab discord: https://discord.gg/EVQZaD3naU
    • View Profile
Re: [TUTORIAL] Creating translation friendly mod
« Reply #4 on: November 17, 2020, 09:27:50 AM »

I'd say the benefit of using GetString() is that it offers a unified solution for all mods. Especially since translations using non-ascii characters could just overwrite that one function and get the new text in every mods and vanilla all at once.
Logged
 

Nia Tahl

  • Admiral
  • *****
  • Posts: 792
  • AI in disguise
    • View Profile
Re: [TUTORIAL] Creating translation friendly mod
« Reply #5 on: November 17, 2020, 01:12:42 PM »

There's also the fact that the GetString() method has already been adopted by a decent number of mods at this point.
Logged
My mods: Tahlan Shipworks - ScalarTech Solutions - Trailer Moments
It's all in the presentation