As you may or may not know, my partner is Ukrainian, and we were talking last night and she brought up a good point: Ukrainians like playing games too, it would be neat if it was in Russian so that more people could play it (specifics on why Ukrainians all speak Russian is outside the scope of this thought process).
So, this gave me a bout of inspiration, and I pulled her out of bed to sit down and translate the dozen or so item names and job titles into Russian for me, so that I could start to tackle localization now instead of later. So here's what I've done, with some guidance from the local horse and everyone's favourite HR professional, Lummox:
First, setting up the translations:
Initially, I was using the new 516 feature of asslists to just store a dictionary of the languages, something along the lines of:
var/alist/languages(
"english" = alist(
"sword" = "sword",
"spear" = "spear"
))
It was pointed out to me that I would likely end up hitting the instruction limit by doing it this way), so I was advised to use json files for the languages themselves, which has opened up some absolutely nifty possibilities.
The structure of the json is the same:
spanish.json
{
"sword": "espada",
"spear": "lanza"
}
The game loads in the json files into a collection of alists as needed which means if nobody uses korean localization, it won't load the korean translation to the server. This way I can avoid loading a massive amount of language data at world startup if it will never end up being used.
Where this goes from something nice to something arguably over-engineered is in how the Localize(string) proc handles missing translations. If a string is passed that the client's language doesn't have a translation to, it adds the string to an errors list for the language, which is then appended to the json file. This means that the game will automatically create new translation files for me by only adding a new language to the list of available languages to the game.
Another great aspect of the way this is being handled, is that the localization system is almost completely modular. There is no need to go combing through the codebase and adding in lists of strings that need to be translated. If something is meant to be localized, all that needs to happen is to call client.Localize(string) in place of the raw string. If the translation for the string doesn't exist in the language files, it will be created and initialized to a unique string that signals a missing translation. This happens at runtime, which means the language files can also be updated at runtime.
This goes into overdrive when you consider that this can be leveraged to create tooling for translators to view their translations in realtime. All that they need from here is a method to run the game in a special mode that allows them to create and edit the language packs on the server, allowing them to view them in-game in realtime. This way, they can double check that the length of the translated strings will fit within the maptext bounds of whatever it is that they happen to be translating, and that there aren't any conflicts with the font and special characters (which byond does a pretty good job of handling itself!)
All in all, it's a nifty little setup, and I'm glad I decided to tackle this early on. It will make things much easier down the line.
Happy MakGam!
This method doesn't handle dynamic strings at all. Since my game doesn't use dynamic strings, this isn't an issue for me, but might be for someone using a similar setup in their game.
This method also will not work for realtime chat translation, which is an entirely different beast.
This also has an issue with it where if the hard-coded version of a string is changed down the line (ie has a typo) that will require every language file to be updated, which can lead to dead k/v pairs in the language files. One way around this would be to use string constants (ie ITEM_SWORD) instead of "sword", and referencing that constant string instead of the actual word itself as the key in the language files. But that adds another step to adding new content to the game, so I opted to not go for it.