Simple text localization in Unity

Saturday, August 6, 2016 8:07 PM

Localization can help your game reach more people and is fairly easy to do. The best way to do that is having a text file for each languages that is easy to read and edit for people who will translate your text and who might not be technical at all. If you translate your game yourself it's still very practical to have language text files that you can edit with a simple text editor.

 

The file can be a simple .ini formated file, json or xml. Each format has its pros and cons. I believe ini files are the easiest for human to parse but lack flexibility. Json is easier to parse by computer but hard to understand when you read it. I personnaly like xml, it's still hard to read sometimes but you can always format it in a way that stay simple enough to edit by hand. Also xml has build in support with c# and it's really easy to parse.

 

For my game In The Shadows I use a simple localization system based on xml files. I am sharing the basics of it here so that you can use it and extend on it if you find it useful. I tried to make the simpliest yet usable localization system I could think of for localizing text in a game here. I guess this tutorial is targeted to people with a moderate level of knowledge of C# and Unity.

 

XML format

Let's start defining the xml file structure. I will keep the example simple so it's easier to explain. This file will be saved somewhere in your project Assets folder. Save this file as ENGLISH.xml

 

<?xml version="1.0" encoding="utf-8"?>
<Language LANG="english" ID="0">
  <!-- Main Menu -->
  <text key="MAIN_TITLE">My Game Title</text>
  <text key="CONTINUE">Continue</text>
  <text key="START_NEW_GAME">Start New Game</text>
  <text key="OPTIONS">Options</text>
  <text key="QUIT">Quit</text>
</Language>

 

The structure here is very simple. Easy to parse and easy to edit manualy. The main root node of the xml file define the language and the ID attribute will be used set the language used ingame. Set a different ID to each language file.

 

Then I use a "text" node with a "key" attribute and the value of the node is the actual text that we will see in-game. With this system each language has it's own file and you just need to edit the "text" node value for each language. The node "key" attribute stay the same for each language. This file is saved in the Asset folder under the filename "ENGLISH.xml". If you have a file for french name it "FRENCH.xml" and so on.

 

Now we need a way to read the language xml file content to use the key value pair in the game.

 

Localization Class

This is a simple class that will read and populate itself with the xml content. You simply need to add this script to an empty GameObject that will stay in all your scenes. Save this file as LocalizationManager.cs

 

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Xml.Linq;
 
public class LocalizationManager : MonoBehaviour
{
    public static LocalizationManager Instance { get { return instance; } }
    public int currentLanguageID = 0;
    [SerializeField]
    public List<TextAssetlanguageFiles = new List<TextAsset>();
    public List<Languagelanguages = new List<Language>();
 
    private static LocalizationManager instance;   // GameSystem local instance
 
    void Awake()
    {
        instance = this;
        DontDestroyOnLoad(this);
        // This will read  each XML file from the languageFiles list<> and populate the languages list with the data
        foreach (TextAsset languageFile in languageFiles)
        {
            XDocument languageXMLData = XDocument.Parse(languageFile.text);
            Language language = new Language();
            language.languageID = System.Int32.Parse(languageXMLData.Element("Language").Attribute("ID").Value);
            language.languageString = languageXMLData.Element("Language").Attribute("LANG").Value;
            foreach (XElement textx in languageXMLData.Element("Language").Elements())
            {
                TextKeyValue textKeyValue = new TextKeyValue();
                textKeyValue.key = textx.Attribute("key").Value;
                textKeyValue.value = textx.Value;
                language.textKeyValueList.Add(textKeyValue);
            }
            languages.Add(language);
        }
    }
    // GetText will go through each language in the languages list and return a string matching the key provided 
    public string GetText(string key)
    {
        foreach(Language language in languages)
        {
            if (language.languageID == currentLanguageID)
            {
                foreach(TextKeyValue textKeyValue in language.textKeyValueList)
                {
                    if (textKeyValue.key == key)
                    {
                        return textKeyValue.value;
                    }
                }
            }
        }
        return "Undefined";
    }
}
// Simple Class to hold the language metadata
[System.Serializable]
public class Language
{
    public string languageString;
    public int languageID;
    public List<TextKeyValuetextKeyValueList = new List<TextKeyValue>();
}
// Simple class to hold the key/value pair data
[System.Serializable]
public class TextKeyValue
{
    public string key;
    public string value;
}

 

Some details about what is going on in this script : 

 

The class use DontDestroyOnLoad(this);  so the GameObject holding the script will stay in scene even after you load a new scene. You can add your localization gameobject on the first scene you load or use any other mean of scene management that you already have.

 

Using [SerializeField] over the languageFile List<> will embed that list in Unity inspector so you can easily drag and drop your XML file in there inside the editor. The script uses [System.Serializable] to embed the language sub classes in Unity inspector to easily visualise the data and help debuging. 

 

The variable currentLanguageID will define which language file to use. You can set this value any way you want, it has to match the ID value from the XML file you want to use. Here it is set to 0 so it will use the English.xml file since its ID attribute is set to 0. You could detect the system language to set this value to the system current language and use a menu / option that the player can change manually. 

 

The script in the inspector. You can drag and drop your xml file in the Language File List<> 

 

Localized UI Text component

You will also need a second script that you add to your GameObject with a UI Text component. Save this file as LocalizationUIText.cs

 

using UnityEngine;
using UnityEngine.UI;
 
[RequireComponent(typeof (Text))]
public class LocalizationUIText : MonoBehaviour
{
    public string key;

    void Start()
    {
        // Get the string value from localization manager from key 
        // and set the text component text value to the  returned string value 
        GetComponent<Text>().text = LocalizationManager.Instance.GetText(key);
    }
}

 

This script can be added to any GameObject with a Text component on it. It can be the Text Component of a button or just a label or whatever. This script simply calls GetText() from the LocalizationManager and populate the Text component text value with the string returned.

 

The [RequireComponent(typeof (Text))] line will add a Text Component to the GameObject to which you added the script, if there is not one already attached to it.

 

Add the script to a GameObject with a UI Text component. Set Key to the value you want to use in the XML file.

 

You can now specify the Key to use for this particular Text component text value. The text value will be automaticly changed by the localization script.

 

You can also use the LocalizationManager in other scripts, if you need to set a string in UI to different value programmaticaly, you can simply call the GetText() method with the Key you want to use.

 

string localizedString = LocalizationManager.Instance.GetText("START_NEW_GAME");

 

This will return the string "Start New Game" in the language specified by currentLanguageID from LocalizationManager.

 

To conclude

Well thats pretty much it. This is a very simple way to handle localization I think, there is ways to make extremely flexible localization system that will also handle images or sound, but I wanted to share a simple way to localize text, while still using an extenal file format that you can easily modify by hand. In The Shadows uses something similar to this altho I do handle image localization as well, but that was out of scope for a simple tutorial. Maybe this example will be useful to you or give you ideas for your own system.

 

 

Discussion

dotsquid
Hi. Thanks for sharing.Wouldn't it be more efficient to use a Dictionary instead of the the List of TextKeyValues? Your implementation has O(n) complexity. While the access to Dictionary has amortized O(1) complexity due to hash table inside the Dictionary.I'm a C++ guy and just can't miss such things silently :)
nt
You are right, Dictionary would be more efficient, for the small quantity of text I have in the game it's not noticeable tho. It would become slower the more key pair in the List so it could have a noticeable impact for a game that has a lot of text. I'll update it at some point!
Victor
Hey guy, I loved that system, it's perfect and ~almost~ suite my needs. I was wonder here, "How can I change the language in runtime?"I created another c# script named "profilesManager", and create three methods, each one refering to a language, "English", "Spanish" and "Portuguese". I am reffering to the GameObject containing the LocalizationManager, and trying with "GetComponent" access its "currentLanguageID" value so I can change it in runtime through a button set to each above languages. The problem is that during runtime the "currentLanguageID" value change, but the language continue the same... Is that because this script don't accept in-runtime change? I would like to ask you some help to overcome that little problem. Oh, and I tried to put the code I am using to change the value put I get a server error when trying to send the message.
nt
@Victor you would need to add some sort of update method to the scripts that call the GetText method, in the case of my example, the LocalizationUIText.cs I could add a method that check for a change of currentLanguageID and update itself, or maybe add a method in the manager that you call when you change the current language then loop through all script that access the ID to refresh them, using FindObjectsOfType(typeof(LocalizationUIText)). There could be multiple way of doing the same thing. One way or another, the scripts that fetch the data from the manager are the one that need to be refreshed since they only set them self up on Start(), the Localization loads all languages already on Awake() so it''s there, it''s just a question of updating the other script to call GetText() again.
nt
@Victor Also about the website error, I need to rewrite the comment system, it's really rudimentary and only accepts plain text with simple cleanup of "illegal" characters. I remove a lot of characters that I shouldn't and I guess some stuff is not handled properly. The website is build using a custom CMS so it's missing a lot of basic feature still : )
Victor
Hey guy, thanks the reply. I will try it here.
mochadwi
Onegai~

Would you mind provide us the tutorial to localize an image and sounds, please?

We would love to see how it works!
Hi,
hi tested this tutorial..but i alway get Undefinited values from the getText method...
Managrull
Will this work for android/iOS builds?
nt
@Managrull it should
Mark
I commented out the DontDestroyOnLoad(this); in the Awake LocalizationManager.cs script and then added currentLanguageID = PlayerPrefs.GetInt(''Language'', 0); above the DontDestroyOnLoad(this); line.

Then made this as I change languages through an UI button that then just call the btnLanguage with the int value of the language id, and then reload the scene afterwards. Works fine for small projects, although it might be a little dirty to it this way.

public void btnLanguage(int i)
{
PlayerPrefs.SetInt(''Language'', i);
SceneManager.LoadScene(0);
}

Leave a reply

Name ( required )
E-mail ( required, won't be published )
Wesite