
Nowadays it’s rare to create games with only one language available for a player. So today we will tackle localizing games in Unity using Singleton Design Pattern and Data Serialization.
Why should I care?
I know that we are living in a world where the English language becomes a standard mean of communication, but there are still people that don’t speak English! So the only way to get to them is to make a game available in their native language.
Most commonly used are Chinese, German, French, Portuguese, Spanish, Japanese and Russian. So today we are going to prepare a system that will handle displaying labels in different languages and allow us to change the language of our game.
Implementation
In the beginning, we need to prepare the core of our localization system.
It will be data storage with different access keys.
If you haven’t already seen Singleton Design Pattern, I’ll highly recommend checking it out first.
So our LocalizationManager will need to load a file with translations and later use it to distribute correct values to labels. Of course, we can’t forget about the option to change the language!
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; /// <summary> /// Localization Manager /// This class is responsible for loading file with translations and responding to language change. /// It stores translations is currently loaded language. /// </summary> public class LocalizationManager : PersistentLazySingleton<LocalizationManager> { // Path to translation file inside Resource folder private const string TRANSLATION_FILE_PATH = "translations"; // Current language private string currentLang = "en"; // Available languages in translation file private List<string> availableLanguages = new List<string>(); // Loaded translations, key - tranlation private Dictionary<string, string> translations = new Dictionary<string, string>(); // Attach to be notified on language change. public UnityAction OnLanguageChange; /// <summary> /// Unity method called like constructor. /// </summary> protected override void Awake() { base.Awake(); // Loading tranlsations LoadTranslations(); } /// <summary> /// Loading translation file and changing language to default one. /// </summary> private void LoadTranslations() { ChangeLanguage(currentLang); } /// <summary> /// Method that changes language in game. /// </summary> /// <param name="lang">Desired language expresed in 2 letter code.</param> public void ChangeLanguage(string lang) { Debug.LogFormat("[{0}] Changing language to: {1}", typeof(LocalizationManager), lang); // Changing current game language. currentLang = lang; // Loading language. LoadLanguageFile(lang); // This line might return different language than passed in function. // It will happen only if desired language was not found in file. Debug.LogFormat("[{0}] Language changed to: {1}", typeof(LocalizationManager), currentLang); // Notify on language change OnLanguageChange?.Invoke(); } /// <summary> /// Load file with translation and updates translation dictionary /// </summary> /// <param name="lang">Desired language.</param> private void LoadLanguageFile(string lang) { // Loads translation file from Resource folder TextAsset languageFile = Resources.Load<TextAsset>(TRANSLATION_FILE_PATH); // Checking if tranlation file exists if (!languageFile) { Debug.LogErrorFormat("There is no file in resources under path: {0}", TRANSLATION_FILE_PATH); return; } // Splits texts into lines for ease of use. var lines = languageFile.text.Split('\n'); int langIndex = -1; // Clear available languages if required if (availableLanguages.Count > 0) { availableLanguages.Clear(); } // Clear translations if required if (translations.Count > 0) { translations.Clear(); } // Go through loaded lines for (int i = 0; i < lines.Length; i++) { // Remove unnecessary symbols var line = lines[i].Trim(); // Split line into smaller pieces var part = line.Split(';'); // First line contains language symbols if (i == 0) { // Loading available languages for (int j = 1; j < part.Length; j++) { availableLanguages.Add(part[j]); // Check which element is our desired language if (part[j] == lang) { langIndex = j; } } // In case that we don't have desired language in file we can load first (or default) one. if (langIndex == -1) { langIndex = 1; currentLang = part[langIndex - 1]; } } else // In rest there are tranlsations { // Loading keys and translations for selected language. var key = part[0]; translations[key] = part[langIndex]; } } } /// <summary> /// Returns translation for provided key. /// </summary> /// <returns>Translation.</returns> /// <param name="key">Translation key.</param> public string GetTranslation(string key) { if (!translations.ContainsKey(key)) { Debug.LogErrorFormat("Localization in lang: {0} is missing key: {1}", currentLang, key); return key; } return translations[key]; } }
Great! Now we need to display text in labels! Because currently in Unity we can use Text component as well as TextMesh component we need a base class for both.
using UnityEngine; /// <summary> /// Base class for localized labels in UI. /// To use translations you need to create child class and override ReceivedTranslation() method. /// </summary> public class UILocalizedBase : MonoBehaviour { // Translation key used to get translation from LocalizationManager. [SerializeField] private string labelTextKey; /// <summary> /// Override existing key for label. /// </summary> /// <param name="key">Translation key.</param> public void SetKey(string key) { labelTextKey = key; UpdateLabel(); } /// <summary> /// Gets translation from LocalizationManager and passes it to ReceivedTranslation() method. /// </summary> private void UpdateLabel() { var translation = LocalizationManager.Instance.GetTranslation(labelTextKey); ReceivedTranslation(translation); } /// <summary> /// Receiveds the translation. /// Override this function to update desired label. /// </summary> /// <param name="text">Translated text.</param> protected virtual void ReceivedTranslation(string text) { // content of this function will be filled in child classes } /// <summary> /// Unity method called when component or game object is enabled. /// </summary> private void OnEnable() { UpdateLabel(); // Attach to Language Change event LocalizationManager.Instance.OnLanguageChange += UpdateLabel; } /// <summary> /// Unity method called when component or game object is disabled. /// </summary> private void OnDisable() { // Dettach from Language Change event LocalizationManager.Instance.OnLanguageChange -= UpdateLabel; } }
Now it’s time for versions with components!
using UnityEngine; using UnityEngine.UI; /// <summary> /// Component used to update content of Text component with translation /// </summary> [RequireComponent(typeof(Text))] public class UILocalizedText : UILocalizedBase { // Reference to Text label component private Text label; /// <summary> /// Unity method called like constructor. /// </summary> private void Awake() { label = GetComponent<Text>(); } /// <summary> /// Receiveds the translation. /// </summary> /// <param name="text">Translated text.</param> protected override void ReceivedTranslation(string text) { label.text = text; } }
using UnityEngine; using TMPro; /// <summary> /// Component used to update content of TextMeshPro component with translation /// </summary> [RequireComponent(typeof(TextMeshProUGUI))] public class UILocalizedTextMeshPro : UILocalizedBase { // Reference to TextMeshPro label component private TextMeshProUGUI label; /// <summary> /// Awake this instance. /// </summary> private void Awake() { label = GetComponent<TextMeshProUGUI>(); } /// <summary> /// Receiveds the translation. /// </summary> /// <param name="text">Translated text.</param> protected override void ReceivedTranslation(string text) { label.text = text; } }
So this implementation is based on using keys to access proper translation from our little manager.
Localized document
Our next step is to prepare the spreadsheet with available keys, languages, and translations. I’ll let you decide how you want to do it but here is just an example of how this should look like.

After filling the spreadsheet with translations, we can make the next step which will be to export data to CSV file.

Now we just need to put it in Resource folder in our project, and we are done!
Example
To show you how you can use it in your project I built a little demo.
It shows you how it should be set up in your project and how it should work.

Each label has its unique id and if language changes its just update text within it. ?
Simply plug & play!
The project is available on in my public repository.
I hope you find this informative and useful!
See you next time!