Improving Localization


My first attempt for switching the game's locale was to ask the player to restart the game after the locale change. In some applications this is common, since the effort to update every visible bit that might already have been loaded in a different locale, is relatively high - is it?

Ambitions Caused by Other Games

To be honest, this approach is not very user-friendly. First, nobody likes to restart an application just for changing one setting. Second, when the player is in mid-game while changing the locale, restarting the game would mean that the game had to be saved if the progress should not be lost. Another point was that I did not come up with a cross-platform-safe method of actually restarting the game (stopping the current game was easy, but I did not bother to find any solution that would work, even if the game was started with command line arguments, or things I could not even think of right now).

Finally, the determining point why I decided to invest some time into live-switchable language settings was that I saw that Cozy Space Survivors had this feature and I also wanted my game to be able to do this.

Cozy Space Survivors is a cute little survivor-like game in space where you collect chocolate and solve some cozy quests. It was made by Simon Trümpler, who is a professional VFX artist and host of a German GameDev Podcast. The game was released in May 2024 on Steam and I highly recommend that you have a look at this game.

Gettext

The first iteration of my localization used the gettext library and tools which are available for the Python programming language. However, I had some philosophical and practical problems with gettext . My first problem with gettext was that you don't define localization keys explicitly, but you use the english version as localization keys by default. This has some major advantages, for example that you always see the exact text in code. Also, the player will never see any abstract translation key such as ui.menu.pressKey, but the English original instead: Please press a key. However, it also has its downsides: For longer texts, like descriptions and conversations, this turns out to be relatively ugly. Further, if you need to change any text, the key also changes (even if you only forgot a comma). I had an extensive discussion about this topic with another developer and he had some really good arguments, but the second (more practival than philosophical problem) determined by decision here: gettext translation files have to be compiled in order to be used by an application.

Now I have one yaml file per locale. Each yaml file contains semantic keys (e.g. conversation.flanduras.noMushrooms) and the corresponding string values (e.g. Sorry, but you don't have enough toxic mushrooms.). I can now even run a script in my CI pipeline that checks whether every key in the default locale (en) is also exising in every other locale file.

I removed all gettext imports and defined an own translation function, which just does a dictionary lookup in the currently loaded locale. The function's name is _ just like in the gettext API, so that _("my.key") returns the translation for the given key.

An advantage of having an own function for translation is that it is easier to debug localization issues. I already found out that the names of entities in the game world (which are displayed when the player focuses them) were translated every frame. Without my own function, I would not have noticed this and would never have been able to fix this. Now, the names are only translated either when the level is loaded or when the locale is changed at runtime.

Live-Switching the Locale

As I already said, the problem with switching an application's locale on the fly is that there might be objects that were already initialized using the previous locale. There are two possible ways to solve this:

The first option would be to create new instances of every already initialized object. This would initialize the new instances with the new locale. The other option would be that every object that contains localizable contents can be updated. Since I wanted to do the locale switching on the fly (also in mid-game, without the need to save and load the game), I decided to do the latter one.

The problem with my game UI framework is that there are no such things as reactive data bindings, which you can use in most modern Javascript web frameworks and wich allow for an automatic live-update of UI elements when the underlying data is changed.

What I did instead was that I introduced a new event type. My game engine has a built-in event broker: Every object of the game can subscribe to certain event types and also publish events of certain types. I introduced the new event type LocaleChangedEvent which does not have any properties (I could have added the old locale and the new one, but I simply didn't need this information). This event is raised after the user switched the locale.

Updating UI Elements

Now, I subscribed every UI container containing localizable contents to this new event. When the event is received, my callback function iterates through every translatable UI element (which is basically only buttons and label texts) and sets the text to the value localized with the new locale.

There was just one problem: The widgets did only contain the already translated text and not the translation key. I did not want the parent elements to remember the translation key of every child item. Instead, I created wrapper classes for the UIButton class and the UIText class and called them LocalizedUIButton and LocalizedUIText. These classes expect a localization key instead of an already translated string. They store the translation key in an internal property and initialize the translated text in the parent classes. Additionally, the classes have a method onLocaleChanged which overrides the text in the parent classes with the translation of the current locale.

Now, when the locale event is received in the UI container elements, all I have to do is to iterate through all translatable child elements and call their onLocaleChanged method.

Caution With Event Subscriptions

I could have subscribed the LocalizedUIButton and LocalizedUIText classes directly to the event, but I wanted to have a better control of who subscribes to events. Unfortunately, it is relatively easy to build memory leaks by subscribing events without ever unsubscribing them, even in a garbage-collected language such as Python or C#. When a class subscribes to an event using a method as a callback, the event implementation holds a reference to the callback method. When the object is not needed anymore, the garbage collector would normally find that the object is not referenced anymore and free its memory. However, since the event implementation still holds a reference to the object, it will be never released from memory and the RAM consumption of your application will grow and grow.

This is why I wanted to have only a few subscriptions where I could ensure that the event is unsubscribed when the UI elements are not needed anymore.

Conclusion

So, this was my short trip into the world of localization and UI label update challenges. I think I solved the problems. The UI and the game contents are now localized using simple YAML files and the locale can be switched at runtime without any interruption or reload.

I hope this little insight was interesting and I would like to thank you for reading this.

Best regards, Carsten

Get The Fire of Ardor - Quest for the Soul Stone

Leave a comment

Log in with itch.io to leave a comment.