Unity: Advanced Localization

Pudding Entertainment
7 min readFeb 23, 2020

--

This article is a continuation of Unity: Localization Made Easy. If you haven’t read the first part I strongly suggest doing so before proceeding further since the tutorial is based on the code and assets created before.

In the second part I would like to show advanced localization techniques you can use in your games. By the end of this page you will have a fully functional change language button, so much needed component for any project.

The best part of the approach is extensibility — no code changes required in order to add support for additional languages.

As always, source code is available at GitHub, please, find the link at the end of this page.

Part 1. Images and files

First of all, let’s add required change language button images. Note that I’ve placed them at the Images\Resources\LanguageImages folder since we will need access from a script later.

Now we will add the button on a scene. Go to the Hierarchy view, find Canvas, right-click->UI->Button. You can delete the child Text component, we will not use it. Attach one of the images to the Image component. The end result should look like:

Next, let’s move language properties files to the LangFiles folder under Lang/Resources. This approach requires access to all supported languages, in our case each LangFile will represent one language.

Just to recap, this is the Hierarchy view you should have by now:

Most of the components were created in the first part already, LangButton is the new one just added.

Part 2. Scripting

There are quite some changes required for the LangResolver script. Instead of having a Dictionary which holds properties for the current system language only we need a Dictionary containing all supported languages and corresponding textual key-value pairs. As it was mentioned earlier, we will define all supported languages via LangFiles files.

But before we start with LangResolver there is one improvement I want to make to the LangText script created in the previous tutorial. In order to separate concepts in a better way the following method has been added:

public void ChangeText(string text)
{
GetComponent<Text>().text = Regex.Unescape(text);
}

It is definitely not a LangResolver responsibility to change the Text component text.

Moving forward, let me show you the LangResolver script after all the modifications applied:

public class LangResolver : MonoBehaviour
{
private const char Separator = '=';
private readonly Dictionary<SystemLanguage, LangData> _langData = new Dictionary<SystemLanguage, LangData>();
private readonly List<SystemLanguage> _supportedLanguages = new List<SystemLanguage>();
private SystemLanguage _language; private void Awake()
{
DontDestroyOnLoad(gameObject);
ReadProperties();
}
private void ReadProperties()
{
foreach (var file in Resources.LoadAll<TextAsset>("LangFiles"))
{
Enum.TryParse(file.name, out SystemLanguage language);
var lang = new Dictionary<string, string>();
foreach (var line in file.text.Split('\n'))
{
var prop = line.Split(Separator);
lang[prop[0]] = prop[1];
}
_langData[language] = new LangData(lang);
_supportedLanguages.Add(language);
}
ResolveLanguage();
}
private void ResolveLanguage()
{
_language = PrefsHolder.GetLang();
if (!_supportedLanguages.Contains(_language))
{
_language = SystemLanguage.English;
}
}
public void ResolveTexts()
{
var lang = _langData[_language].Lang;
foreach (var langText in Resources.FindObjectsOfTypeAll<LangText>())
{
langText.ChangeText(lang[langText.Identifier]);
}
}
private class LangData
{
public readonly Dictionary<string, string> Lang;

public LangData(Dictionary<string, string> lang)
{
Lang = lang;
}
}
}

Alright, what is going on here? A private inner class that holds the Dictionary of language key-value pairs has been created. It is now used as a value of the _langData Dictionary where the key is SystemLanguage. The main reason to create this inner class is the possibility to extend it in the future with additional Text related properties like Font, Font Style, Font Size etc.

Let’s have a closer look at the ReadProperties method. There are a couple of things happening:

  • All available files are parsed and key-value pairs are stored in the LangData Lang field
  • During the parsing the _supportedLanguages List is filled with each supported SystemLanguage
  • Language to use is determined by PrefsHolder utility class — we will look into it in a second
  • Awake and ResolveTexts methods are not changed

I would like to take a break here and talk about one of the SOLID principles namely Single responsibility principle. As a solo game developer, you might be thinking that clean code and good design are not that important for small to medium projects, at the end of a day nobody will likely look into it anyways. Hence for the sake of development speed certain principles can be violated.

If you disagree with the statement above, you have my respect. Coding in Unity is definitely different from doing Enterprise in Java or C++ and it takes a while to understand it and adjust certain coding behaviors.

I violated the Single responsibility principle in my first released Unity game quite a lot mostly because I didn’t know how to do it better. I also learnt what should be avoided and the next game has a much cleaner code. In a nutshell, for any game you develop try to always have game logic, UI handling, saving, ads and store purchases separated from each other. Like in this tutorial there is a clear responsibility defined for lang files parsing (LangResolver), UI handling (UIResolver) and config saving (PrefsHolder).

For simplicity sake, all code could have been placed into one Script and it would work exactly the same way. But I would never do so in a real project, hence tutorials “inherit” the same design principles. I always strive to show the best design decisions and practices learnt from experience (and previous mistakes).

Let’s take a peek at PrefsHolder:

public static class PrefsHolder
{
private const string Lang = "Lang";
public static void SaveLang(SystemLanguage lang)
{
PlayerPrefs.SetString(Lang, lang.ToString());
}
public static SystemLanguage GetLang()
{
Enum.TryParse(
PlayerPrefs.GetString(Lang, Application.systemLanguage.ToString()),
out SystemLanguage language);
return language;
}
}

This is a simple static utility class, basically, a wrapper around Unity’s PlayerPrefs.

So far so good, now it is time to implement the main purpose of this tutorial — the possibility to change a language. We will extend the UIResolver script to support this. Also, with the language change, the button image should be appropriately selected. After all, modifications applied UIResolver looks now as follows:

public class UIResolver : MonoBehaviour
{
private Image _langButtonImage;
private LangResolver _langResolver;
private readonly Dictionary<string, Sprite> _langImages = new Dictionary<string, Sprite>();
private void Start()
{
_langButtonImage = FindObjectOfType<LangButton>().GetComponent<Image>();
_langResolver = FindObjectOfType<LangResolver>();
_langResolver.ResolveTexts();
foreach (var sprite in Resources.LoadAll<Sprite>("LanguageImages"))
{
_langImages[sprite.name] = sprite;
}
ResolveLangImage();
}
public void ChangeLanguage()
{
_langResolver.ChangeLanguage();
ResolveLangImage();
}
private void ResolveLangImage()
{
_langButtonImage.sprite = _langImages[PrefsHolder.GetLang().ToString()];
}
}

Have you noticed that in the Start method I’m searching for the LangButton component? In Unity there are multiple ways to wire different objects together: one can use Tag, Name or public field. I prefer to use scripts for this. LangButton is an empty class that derives from MonoBehaviour, it exists only to be able to access a GameObject it is attached to. Apart from this, the code is relatively straight-forward, we store all sprites in a Dictionary, so we can apply it after language is changed. And as before all text are initially resolved on a Start. The ChangeLanguage method is called whenever OnClick event is received.

Last missing piece here is the LangResolver::ChangeLanguage method itself, so let’s fill the gap. This method should change current _language, call LangText::ChangeText method and save the selected language in PlayerPrefs for further usage.

public void ChangeLanguage()
{
var currentLanguageIndex = _supportedLanguages.IndexOf(_language);
_language = currentLanguageIndex ==
_supportedLanguages.Count - 1
? _supportedLanguages.First()
: _supportedLanguages[currentLanguageIndex + 1];
ResolveTexts();
PrefsHolder.SaveLang(_language);
}

I didn’t want to keep a separate field for the current language index in the _supportedLanguages list since it is easy to resolve it via IndexOf.

Now it is time to switch back to Unity, press Play and click on the LangButton to change the language!

Part 3. Additional language support

As I promised at the very beginning, in order to extend a list of supported languages no code changes will be required. Let me illustrate that by adding Spanish. There are only two steps needed:

  • Add a new image to LanguageImages
  • Add a new text file to LangFiles

Hit Play, click on the LangButton couple of times and you will see

Afterwards

Well done finishing the tutorial! Should you have any questions please leave them in the comments section below.

The first part of this tutorial is here

The project source files can be found at this GitHub repository

Support

If you like the content you read and want to support the author — thank you very much!
Here is my Ethereum wallet for tips:
0xB34C2BcE674104a7ca1ECEbF76d21fE1099132F0

--

--

Pudding Entertainment
Pudding Entertainment

Written by Pudding Entertainment

Serious software engineer with everlasting passion for GameDev. Dreaming of next big project. https://pudding.pro

Responses (2)