Translation support for Unity games

Unity provides you with pretty much all the tools you need for creating a game - one really handy feature that’s unfortunately missing though is inbuilt support for doing translations into different languages.

It wasn’t too complicated to add it myself in the end, but since I couldn’t really find any articles on this topic I thought I’d do a quick write-up of how I approached it in case it’s helpful for anyone :)

So, if you want to add translation support to software you’ll have to ask yourself how and where to store your text strings and their translated versions, and how to load them into your program. Especially the latter was really important to me - I wanted to have a system that requires minimal code changes and requires the least possible amount of work when adding new strings that needed translations. What I especially did not want was having to manually manage a database of all strings in the game with cryptic lookup keys like ID_UI_INGAME_WINDOW_BUILDER_TITLE that some systems use, because having to maintain that database would be super uncomfortable and I’d rather not have to copy these keys around where after a couple weeks I don’t know anymore which text exactly is hidden away behind them.

When researching translation systems you’ll quickly stumble over gettext (up-to-date Windows binaries can be found here), and it satisfies the above requirements perfectly.
All you have to do is to wrap your strings that need translations into some function, so for example this:

string text = “Some text”;

becomes this:

string text = I18N.GetString(“Some text”);

You then run a command line program named “xgettext” that’s part of the gettext system over your source code. It looks for appearances of your wrapper function and extracts their string argument into a separate .pot file. Using “msginit” you then take this .pot file and create individual .po files for each language you want to support. The .po is a simple plaintext file containing both your original language strings and their translations.
Here’s an example of what that .po file looks like for German:

msgid “Some text”
    msgstr “Irgendein Text”

    msgid “Another text”
    msgstr “Ein weiterer Text”

You then parse this file and put its contents into a hashtable, using the msgid as key and msgstr as value. Your GetString()-wrapper method from the example above then simply looks up and returns the proper value for the given key (or returns the key if it doesn’t exist in your hashtable, so if some strings are missing translations they’ll at least show up in English).
There’s a source code example of how to do this here that should get you started.

If you’re like me and try to use the Windows command line only if you really have to you can make it even less of a hassle by creating a couple of batch files that do all the gettext stuff and a Unity editor window that executes them. Now you can run the string extraction with a single click, without having to leave the Unity Editor!

Another benefit of using gettext is that it’s a pretty common translation system, so there are plenty of tools already supporting it. For example there’s poEdit, a GUI application for editing the .po files, which makes life easier for your translators. And if you’re neither into command line programs nor Unity editor scripting it can do the string extraction for you as well.

Making it work with Unitys UI system

So, this system is great for text that you’re directly assigning from within your code - but what about the Text components in Unitys UI system? I certainly didn’t want to set all the Text component values from code and wanted to keep being able to visually create my UI in the Unity editor.

My solution is to do the same thing as for the code basically. I “tag” the Text components that need to be translated by adding another special component to them (named “I18NText”):

It does nothing more than take the text value and run it through the wrapper function:

public class I18NText : MonoBehaviour {
     void Awake() {
        Text textComponent = GetComponent<Text>();
        textComponent.text = I18N.GetString(textComponent.text);
    }
}

Of course it’s also necessary to extract these UI strings into the .po file, so I wrote a small editor script that loops over my UI assets, gets their Text components and writes them into a .pot. The .pot containing the UI strings and the .pot containing the source strings are merged into a single .pot (using the gettext tool “msgcat”), that is then used for creating the .po files.

Phew! :)
This sounds like a lot, but once it’s set up all you really have to do in the end is to wrap your strings into the lookup function, mark your UI texts with the additional component, and run the gettext tools before doing a build. That’s it!

Update 33

Art Stream

There’s another art stream coming up! Watch Garret create some Parkitect art on his Twitch channel, on Wednesday from 1pm to 3pm PST.

Devlog

We added guest activity logging! We’re recording certain events for individual guests and there’s a neat visualization overlay showing you what they’ve been up to:

I was a bit concerned what this would do to the filesize of our savegames, but judging by some quick tests it should be alright. And if not there are ways to optimize it.

Guests became cleverer this week - they learned how to properly use transport rides when heading for a specific location. So if for example they want to leave the park they might either walk or hop onto a transport ride and drop off at a suitable station.

Here’s a couple of debug screenshots, with the yellow line showing which path the guests would take if they wanted to get from one endpoint of the line to the other (for rides it marks where they enter the queue and the station they leave at):

It takes into account the average waiting time at a station, the time the ride needs from the entrance to the exit station (on average) and the ride entrance fee in relation to the travelled distance. The weighting of these factors is something we’ll have to balance.

Update 32

Guests are now able to walk to their seats inside the cars of the new transport ride - here’s a view through the cars roof:

Fortunately it was possible to reuse the same waypoints tools I created for flat rides.

We thought about how to display the parks finances this week. We wanted to have something that’s a bit less overwhelming than a big table of numbers and that lets you quickly see how you’re doing. It needs some more polish and isn’t complete yet but here’s what we came up with:

Your parks expenses/income is grouped into these categories. Each category has a colored bar showing the expenses/income of that category. The little black arrow marks the expenses of the previous month - if it’s in the green area it means you’re doing better this month than last, if it’s in the red you’re doing worse.

The categories can be expanded…

…so if you’re interested in the detailed numbers they’re all there.

Update 31

A very busy week with lots of big code changes!

There’s a terrain gridlines toggle…

…but the real work was spent on coasters - or tracked rides as I should say.

As mentioned earlier building tracked rides with multiple stations didn’t work yet. Now it does:

(Sped up for demonstration)

Having multiple stations doesn’t make that much sense on coasters though, sooo…the inevitable next step was to start working on a transport ride!

Implementing it took some time since it works fairly different from the coasters we had so far - it’s powered, suspended, and then there’s also the shape of these supports.

Speaking of suspended, it does swing a bit in curves, although it’s hard to notice since it’s rather subtle and the framerate of this GIF is a bit low:

Now that it works adding other suspended or powered rides should go much faster though. It should also be fairly easy to add other kinds of tracked rides now that the code doesn’t expect everything with a track to be a coaster.

Just because we got it nicely driving on it’s track doesn’t mean there isn’t lots of work left to do on it though! More on that in the coming weeks I guess.