Unity: Advanced Localization
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 supportedSystemLanguage
- Language to use is determined by
PrefsHolder
utility class — we will look into it in a second Awake
andResolveTexts
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