Localizing your game in Unity

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.

I hope Google Translate didn’t messed with me on that!

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

Convert your spreadsheet into csv file with ‘;’ as separators

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.

Done and running perfectly!

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!

5 1 vote
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x