Making a Visual Novel with Unity (5/5) - Localization

When making your game you might think you only need it in one language. However, it’s always better to be able to reach a wider audience. You might decide you want to support multiple languages at some point, and it’s good to be prepared for that moment. You don’t want to be frantically looking for answers just before the release, after all (definitely not speaking from experience). That’s why we’ll look at how to implement a localization system into our game and how to make it work with ink. Let’s get to it!

I2Loc setup

In order to save time (and keep your sanity) I very much recommend buying plugins and assets to speed up your development process. The one I used is I2 Localisation, which seems to be one of the most popular and highly recommended solutions. A big reason why I chose it is that it offers synchronization with Google Sheets, which made things very easy for our team during development. Everyone could work on the same online document and all I had to do to update the game content was just to press one button (you can also automate and make it update the game with new document content AFTER it’s released which is also pretty cool).

As I mentioned, you can link your game to an external Google spreadsheet. The I2Loc team has created an easy to follow tutorial on how to install the plugin and connect it to Google, which you can check out here. There is also full documentation if you need more information. What I want to focus on is how to actually use it within the game and, most importantly, how to localize our ink file.

Spreadsheet setup

I2Loc (and pretty much any other localization system) works on a key-value basis. Each piece of text has its key, which we can use to find the text within the spreadsheet (or CSV file, if you’re using a different format). That’s why we’ll need a Keys column along with one column for each language we want to support.

tut 5 sheet example

An important thing to remember is that we can generate this spreadsheet content within the Unity editor, using the i2Loc asset, and then merge it with or overwrite the content in the sheet. As you can see below, there are various ways to handle the import/export of text. I preferred to do my edits in the spreadsheet, so I only used Import Merge.

tut 5 i2loc main

When you have a lot of text in your game, it’s a good idea to separate it into different sheets. We will use two for our project, one for UI elements and one for the story.

tut 5 sheets

UI localization

After you’ve set everything up, you should be able to see all your text within the i2Loc asset’s Terms tab. On the right, you can see the name of the sheet the term is defined in. In this case, it’s UI.

i2loc terms

When you click on a term, it will show its translations for each language. As I mentioned, if you notice a mistake, you can edit it right here and then export your changes to the spreadsheet. It also supports automated translation, which can be a good idea for simple things, like button text, or to do a quick, “dirty” translation before hiring someone for the job. I would definitely NOT recommend to use it in the actual release version.

i2loc terms more

Now, let’s localize our main menu. Select one of the button’s text elements and add the I2 Localize component to it.

i2 loc component

In Terms/Term, select the key you want to use for this button. In my case, it was UI/NewGame. That’s all you need to do, the button will now be translated into the currently selected language when we press play. You’ll also see a bunch of options you can modify, such as uppercase/lowercase formatting under Modifier. You can also choose to use a different font for each language, which can be defined under the Secondary tab.

Go ahead and set up the remaining buttons in the same way.

Language Switching

Let’s see if the localization actually worked. On the i2 Languages asset, select the ‘Languages’ tab, and switch Default language to First in the List. Then, change the order of languages in your list and put the one you want to test on top.

tut 5 language testing

Go ahead and press play. Your scene should be now translated!

If you want to change the language from the code, you can do it like so:

I2.Loc.LocalizationManager.CurrentLanguage = "English";

I2.Loc.LocalizationManager.CurrentLanguage = "English (Canada)"; // Language and Variant

You can also set it using language codes, more information about it can be found here.

tut 5 menu

Ink localization

Now onto the fun stuff, translating the story written in our ink file. There are many ways to approach this, we could, for example, have a different ink file per language. This could quickly become a headache though, as we would need to maintain several different files, which are, most likely, all edited by different people. What if you decide to change one line in the middle of the story after everything has been already translated? Or if you realize there was a mistake? You’ll need to update each one of those files separately. Can it be done? Sure it can, but in the case of a narrative game with thousands of lines of text, it’s a dangerous bet to make.

I decided to follow a different strategy and make the google spreadsheet a master file. All changes to the original text would have to be done there and everyone would be working on the same copy. That way, everyone had access to the most up to date version and I could always import it into Unity with one click. Moreover, Google supports version control, so it is very easy to see any changes that were made and rollback to previous versions.

Let’s talk about the actual implementation now. As you know, we need a key for each piece of text. A good way to do it would be to use hashtags and assign a unique key to each line within our story. If your game is very small or if you’re only starting it, this might be a good idea. However, if you (like me), want to add localization to an existing game, you might already have a lot of text in there. You probably don’t want to be adding tens of thousands of hashtags manually line by line.

That said, we’re going to use knots and simple indexing for the keys. For each line of text we request from ink, it’s key is going to be a combination of its knot name and index, which will make every key unique and easy to create in code. First, we need to prepare the spreadsheet by copying all the text from the ink file. The end result is going to look like this:

tut 5 full sheet

Notice how choice texts are also included in their respective knot section. Also, we don’t need the external function calls and hashtags in here, just the text. As for the indexing, you don’t need to type it all manually. Simply create the first key, press Shift and drag down from the bottom right corner to add the remaining keys like so:

Now that the spreadsheet is ready, let’s look at the code next. First, we’re going to add two new class-level variables to InkManager script:

private int _currentLocIndex;

private string _currentKnot;

These variables will be updated with each line we request from ink and used to create its localization key. Given that our knot indexes start from 1, let’s make sure we initialize the value of _currentKnot to be 1 as well, so add the following line in the Start() function.

_currentLocIndex = 1;

Now, let’s move onto the DisplayNextLine() function. At the moment, it looks like this:

public void DisplayNextLine()
{
  if (_story.canContinue)
  {
    string text = _story.Continue(); // gets next line

    text = text?.Trim(); // removes white space from text

    ApplyStyling();

    _textField.text = text; // displays new text
  }
  else if (_story.currentChoices.Count > 0)
  {
    DisplayChoices();
  }
}

In order to create the localization key, we first need to find out the name of the current knot. We’re going to use this line of code:

var knot = _story.state.currentPathString?.Split('.')[0] ?? _currentKnot;

_story.state.currentPathString returns a string that’s formatted like so: {knot_name}.{index}. The index is different for each line, and it seems to be a random number each time. That’s why we’re only using the first part of the string. If the path is not found for whatever reason, we’re using the _currentKnot as a fallback.

Next, we need to check if the knot for the current line is the same as the one we got for the previous line. If it’s not, it means we’ve moved into a new knot, so we need to reset _currentKnot and _currentLocIndex values.

if (_currentKnot != knot)
{

  // new knot, reset values
  _currentKnot = knot;

  _currentLocIndex = 1;

  Debug.Log($"NEW KNOT: {_currentKnot}");

}

Next, let’s create the key:

var key = $"{_currentKnot }_{_currentLocIndex}";

And use it to request the localized version of the text:

var locText = LocalizationManager.GetTranslation($"Story/{key}");

As you can see, the path to get text uses the name of the sheet the key can be found in. In this case, it’s the Story one.

We now have our localized text, so we just need to update the rest of the function to use it. Also, we’ll need to increment the value of _currentLocIndex. The whole code should look like this:

public void DisplayNextLine()
{
  if (_story.canContinue)
  {
    string text = _story.Continue(); // gets next line

    var knot = _story.state.currentPathString?.Split('.')[0] ?? _currentKnot;

    if (_currentKnot != knot)
    {
      // new knot, reset values
      _currentKnot = knot;

      _currentLocIndex = 1;

      Debug.Log($"NEW KNOT: {_currentKnot}");
    }

    var key = $"{_currentKnot }_{_currentLocIndex}";

    var locText = LocalizationManager.GetTranslation($"Story{key}");

    locText = locText?.Trim(); // removes white space from text

    ApplyStyling();

    _textField.text = locText; // displays new text

    _currentLocIndex++;

  }
  else if (_story.currentChoices.Count > 0)
  {
    DisplayChoices();
  }
}

We can now get through the story in a different language!

You might notice that the choice buttons are still in English though. We need to update our DisplayChoices() function. This time, we don’t have to find the knot name, as we already know it. We simply request each choice text as with the rest of the story.

private void DisplayChoices()
{
  // check if choices are already being displayed
  if (_choiceButtonContainer.GetComponentsInChildren<Button>().Length > 0) return;

  for (int i = 0; i < _story.currentChoices.Count; i++)
  {
    var choice = _story.currentChoices[i];

    var key = $"{_currentKnot }_{_currentLocIndex}";

    var locText = LocalizationManager.GetTranslation($"Story/{key}");

    _currentLocIndex++;

    var button = CreateChoiceButton(locText);

    button.onClick.AddListener(() => OnClickChoiceButton(choice));
  }
}

All done! When you hit play, the whole game should be now fully translated!

Summary

Please be aware that the system introduced here works only with simple knots, where each choice immediately leads to a different knot. Because of the way indexing is done, having alternative, choice-based flows within one knot is not going to work. It might be counter-intuitive to separate everything like this in some cases, but doing it from the start makes it easy to handle localization with just a few lines of code.

Also, please bear in mind that at this stage, the translation won’t work when you load the game. You’ll need to include _currentLocIndex and _currentKnot when saving/loading the game in GameStateManager. If you haven’t followed this series from the beginning, state management was covered in part 4.

That’s it! We’ve covered how to use i2Loc to localize your game from within the Unity Editor as well as from the code. We’ve also discussed how to handle ink localization in a simple way. If you have any issues or questions, don’t hesitate to get in touch. And if you’ve been following this series from the start, I would love to see what you have created!

Happy coding!

All visual assets and sprites used in this tutorial are drawn by Erica Koplitz for the game Guilt Free, created together by me and Erica.

More in this series