Localisation of text and graphics in Unity3D using C#

//Localisation of text and graphics in Unity3D using C#

Hi, this will be the first of my tutorial section where I will address a number of topics around the development of games in Unity3D. First off the bat though, let me just say that I am a self-taught indie dev, I havent worked in a big game studio, so this is just me sharing the ways that I have found to get around certain things that have come up in my development of my own games. It may not necessarily be the correct or best practise method, but it works for me, and if it works for you too, then that’s awesome! So, today’s topic is localisation. This is something that seems to be often overlooked in games either entirely, or thrown in at the last minute and then requires a lot of rework to integrate. I wanted to make sure that I could localise Xenomorph right off the bat, so this is the approach that I took to it.

Considerations

Ok, so there were a few considerations that I wanted make sure that I fully covered in my approach.

  1. It had to be simple to implement and easy to expand upon
  2. It must be able to automatically change all the text in the game (interface, dialog, menus etc)
  3. It must be easy to add new languages
  4. It must support changing of art assets (signposts, directions, any written text etc)
  5. It must be in a format easy to have translated

So with that in mind, this is how I decided to approach it.

Approach

I figured the easiest way to achieve the above is to store the actual text in a simple text file, that way I can just send the text file to a translator and they can send me back the translated file, simples. This approach does however have one very serious limitation. It relies on you referencing the text you want from a specific line number. For example, Line 10 in your text file might be “Quit Game”. If someone changes the line numbering by inserting a line somewhere in the file or a simple carriage return, then this will throw out your whole system. As its only me developing my games, I have good attention to detail and discipline, so I am not worried about that 😉 I am however concerned that a translator may muck up the lines, so that is something for me to bear in mind, or adapt the system to be able to cope with that in the future. So my text and art assets will be developed in the following way: Text: Have a number of text files that relate to whatever text is needed in game. I went with individual text files for Dialog, Menu’s and UI (interface). That way when developing the game I can just append new lines to the text file in a logical way, rather than it all dumped as a mess in one file. You can use as many or as little files as you want, that is up to you. Art: My game is a 2D game that use a sprite atlas for each level. This means that I can create seperate Atlases for each language. I actually maintain a photoshop layered file that has the base texture atlas, and I add each language as a seperate layer that can be switched on and off.

Design

Ok, now we have the approach and decided what to make, here is how I actually developed it. Firstly, we need a language manager script, and this should be persistent across the whole game ideally. This script is going to let the game know which language we are currently using. This is nice and simple, and I use an enum to create the languages I want.

public class LanguageManager : MonoBehaviour {

    public enum Languages { English, Spanish, French, German, Russian, Italian }
    [Header("Game Language")]
    public Languages language;
}

Ok, there you can see that we just set a simple enum up and then make a public reference to it, so that we can set the language in the inspector within Unity. So lets look at setting up text first, we will come back to graphics later on. In order to setup a system for reading text, we need to do the following:

  1. Create our default text files, these will be in English for me.
  2. Create a TextAsset to load these files into Unity from our Resources folder. Important, the text assets will be stored in the Resources folders, I went with /Resources/Languages/
  3. Check which language we are currently using, and load the corresponding file
  4. Create a public method to read from that file wherever we need text in our game.

Lets setup what we need: First we need a string that tells us the name of the file for each language we have:

    public string englishDialog;
    public string frenchDialog;
    public string germanDialog;

In the inspector, you can now simply give the name of your text file (without the .txt extension, it doesnt need it in Unity). So for example, if your filename was called “English_Dialog.txt” then you would enter English_Dialog into the inspector in Unity. You get the idea… Then we need to setup a way of reading those files into Unity, and for that I am using a StreamReader which you need to use a scecific namespace for

using System.IO;
StreamReader fileToOpen;

This allows you to read the text from the files in your resources folder. Now we need to create something to load them into, and for that we will use Unity TextAssets. Again, we need to create a reference to them and then load the text into them.

TextAsset dialogLoaded;
dialogLoaded = Resources.Load<TextAsset>("Language/" + englishDialog);

Ok, that loads our text from file into the Asset, but now we need to read it into a string array so that we can read the text anywhere from our game. I went with an array, because then you can split the text file out line by line into an array, then simply call the line number of the file from the array and it returns the text you want.

string[] dialogLines; 
string dialogString = dialogLoaded.ToString();
dialogLines = dialogString.Split("\n"[0]);

As you can see, this basically takes the text file, splits it line by line and then saves it into our final text array dialogLines. No whenever you want to ready localised text into your game, simply use a nice simple method like this:

    public string ReadDialog(int lineNumber)
    {
        return dialogLines[lineNumber -1];
    }

You have to minus 1 from the line you want to read becuase arrays start from 0, the text file will start from line 1. So in order to put this all together now as a coherent script, here is my Language manager that caters for 4 different files I want, Dialog, Menus, Messages and Interface. It also allows me to use English, French, German, Spanish, Russian and Italian. All I have to do is create the following text files:

  • Dialog_English.txt
  • Messages_English.txt
  • Interface_English.txt
  • Levels_English.txt

I place those in my Resources/Language folder and then I can just send those files for translation, and rename them as needed for each language. Here is my completed script that gives me public methods to read from each file individually:

using UnityEngine;
using System.Collections;
using System;
using System.IO;

public class LanguageManager : MonoBehaviour {

    public enum Languages { English, Spanish, French, German, Russian, Italian }
    [Header("Game Language")]
    public Languages language;

    [Header("English File Names")]
    public string englishDialog;
    public string englishInterface;
    public string englishMessages;
    public string englishLevels;

    [Header("French")]
    public string frenchDialog;
    public string frenchInterface;
    public string frenchMessages;
    public string frenchLevels;

    [Header("German")]
    public string germanDialog;
    public string germanInterface;
    public string germanMessages;
    public string germanLevels;

    [Header("Spanish")]
    public string spanishDialog;
    public string spanishInterface;
    public string spanishMessages;
    public string spanishLevels;

    [Header("Russian")]
    public string russianDialog;
    public string russianInterface;
    public string russianMessages;
    public string russianLevels;

    [Header("Italian")]
    public string italianDialog;
    public string italianInterface;
    public string italianMessages;
    public string italianLevels;

    StreamReader fileToOpen;
    TextAsset dialogLoaded;
    TextAsset interfaceLoaded;
    TextAsset messagesLoaded;
    TextAsset levelsLoaded;

    string[] dialogLines;
    string[] interfaceLines;
    string[] messagesLines;
    string[] levelLines;

    void Start()
    {
        ResetLanguage();
    }

    public void ResetLanguage()
    {
        dialogLines = new string[0];
        interfaceLines = new string[0];
        messagesLines = new string[0];
        levelLines = new string[0];

        if (language == Languages.English)
        {
            dialogLoaded = Resources.Load<TextAsset>("Language/" + englishDialog);
            interfaceLoaded = Resources.Load<TextAsset>("Language/" + englishInterface);
            messagesLoaded = Resources.Load<TextAsset>("Language/" + englishMessages);
            levelsLoaded = Resources.Load<TextAsset>("Language/" + englishLevels);

            string dialogString = dialogLoaded.ToString();
            dialogLines = dialogString.Split("\n"[0]);

            string interfaceString = interfaceLoaded.ToString();
            interfaceLines = interfaceString.Split("\n"[0]);

            string messageString = messagesLoaded.ToString();
            messagesLines = messageString.Split("\n"[0]);

            string levelString = levelsLoaded.ToString();
            levelLines = levelString.Split("\n"[0]);
        }

        if (language == Languages.French)
        {
            dialogLoaded = Resources.Load<TextAsset>("Language/" + frenchDialog);
            interfaceLoaded = Resources.Load<TextAsset>("Language/" + frenchInterface);
            messagesLoaded = Resources.Load<TextAsset>("Language/" + frenchMessages);
            levelsLoaded = Resources.Load<TextAsset>("Language/" + frenchLevels);

            string dialogString = dialogLoaded.ToString();
            dialogLines = dialogString.Split("\n"[0]);

            string interfaceString = interfaceLoaded.ToString();
            interfaceLines = interfaceString.Split("\n"[0]);

            string messageString = messagesLoaded.ToString();
            messagesLines = messageString.Split("\n"[0]);

            string levelString = levelsLoaded.ToString();
            levelLines = levelString.Split("\n"[0]);
        }


        if (language == Languages.German)
        {
            dialogLoaded = Resources.Load<TextAsset>("Language/" + germanDialog);
            interfaceLoaded = Resources.Load<TextAsset>("Language/" + germanInterface);
            messagesLoaded = Resources.Load<TextAsset>("Language/" + germanMessages);
            levelsLoaded = Resources.Load<TextAsset>("Language/" + germanLevels);

            string dialogString = dialogLoaded.ToString();
            dialogLines = dialogString.Split("\n"[0]);

            string interfaceString = interfaceLoaded.ToString();
            interfaceLines = interfaceString.Split("\n"[0]);

            string messageString = messagesLoaded.ToString();
            messagesLines = messageString.Split("\n"[0]);

            string levelString = levelsLoaded.ToString();
            levelLines = levelString.Split("\n"[0]);
        }


        if (language == Languages.Spanish)
        {
            dialogLoaded = Resources.Load<TextAsset>("Language/" + spanishDialog);
            interfaceLoaded = Resources.Load<TextAsset>("Language/" + spanishInterface);
            messagesLoaded = Resources.Load<TextAsset>("Language/" + spanishMessages);
            levelsLoaded = Resources.Load<TextAsset>("Language/" + spanishLevels);

            string dialogString = dialogLoaded.ToString();
            dialogLines = dialogString.Split("\n"[0]);

            string interfaceString = interfaceLoaded.ToString();
            interfaceLines = interfaceString.Split("\n"[0]);

            string messageString = messagesLoaded.ToString();
            messagesLines = messageString.Split("\n"[0]);

            string levelString = levelsLoaded.ToString();
            levelLines = levelString.Split("\n"[0]);
        }


        if (language == Languages.Russian)
        {
            dialogLoaded = Resources.Load<TextAsset>("Language/" + russianDialog);
            interfaceLoaded = Resources.Load<TextAsset>("Language/" + russianInterface);
            messagesLoaded = Resources.Load<TextAsset>("Language/" + russianMessages);
            levelsLoaded = Resources.Load<TextAsset>("Language/" + russianLevels);

            string dialogString = dialogLoaded.ToString();
            dialogLines = dialogString.Split("\n"[0]);

            string interfaceString = interfaceLoaded.ToString();
            interfaceLines = interfaceString.Split("\n"[0]);

            string messageString = messagesLoaded.ToString();
            messagesLines = messageString.Split("\n"[0]);

            string levelString = levelsLoaded.ToString();
            levelLines = levelString.Split("\n"[0]);
        }

        if (language == Languages.Italian)
        {
            dialogLoaded = Resources.Load<TextAsset>("Language/" + italianDialog);
            interfaceLoaded = Resources.Load<TextAsset>("Language/" + italianInterface);
            messagesLoaded = Resources.Load<TextAsset>("Language/" + italianMessages);
            levelsLoaded = Resources.Load<TextAsset>("Language/" + italianLevels);

            string dialogString = dialogLoaded.ToString();
            dialogLines = dialogString.Split("\n"[0]);

            string interfaceString = interfaceLoaded.ToString();
            interfaceLines = interfaceString.Split("\n"[0]);

            string messageString = messagesLoaded.ToString();
            messagesLines = messageString.Split("\n"[0]);

            string levelString = levelsLoaded.ToString();
            levelLines = levelString.Split("\n"[0]);
        }
    }

    public string ReadDialog(int lineNumber)
    {
        return dialogLines[lineNumber -1];
    }

    public string ReadInterface(int lineNumber)
    {
        return interfaceLines[lineNumber - 1];
    }

    public string ReadMessages(int lineNumber)
    {
        return messagesLines[lineNumber - 1];
    }

    public string ReadLevels(int lineNumber)
    {
        return levelLines[lineNumber - 1];
    }

}

There you go, so in closing if my Messages_Engligh.txt looks like this:

Access Denied
Access Granted
Toggle Lights
Open Door
Close Door
Lock Door
Unlock Door
Enter Hatch
Open Doors
Close Doors
Lock Doors
Unlock Doors
Unlocked

And I ran this code anywhere in my game :

string myText = LanguageManager.ReadMessages(2);

It would return “Access Granted” I hope that makes sense! So now lets move on to the next part.

Localising Artwork

Ok, localising artwork is also relatively simple when you have things setup in a simple way. My levels use a single tileset for the floors (its a top down game) and therefore, any text is likely to be written there. Here is a screenshot as an example. screen1 What I needed to do, was create seperate PNG files for each language, and load them in seperately as I load the level, based on the language chosen. First off, I create a LevelManager script and set the following variables.

    public GameObject floorTiles;
    public string englishTiles;
    public string spanishTiles;
    public string germanTiles;
    public string frenchTiles;
    public string italianTiles;
    public string russianTiles;

Once done, simply give the name of the PNG tilesets in the inspector on the LevelManager. One again, you do not need to give the extension, so for example it might be… Level1_Tileset_English Now, we just need to first get the language we are currently using, then get a reference to the material used for the floor tiles and swap out the tileset based on the language. For this, I call the following method during the Awake() function when loading a level.

void SetLanguage()
    {
        Debug.Log("Initialising Localisation for level Tiles");

        if(_languageManager.language == LanguageManager.Languages.English)
        {
            floorTiles.GetComponent<MeshRenderer>().material.SetTexture("_MainTex", Resources.Load<Texture>("Artwork/Tilesets/" + englishTiles));
        }

        if (_languageManager.language == LanguageManager.Languages.Spanish)
        {
            floorTiles.GetComponent<MeshRenderer>().material.SetTexture("_MainTex", Resources.Load<Texture>("Artwork/Tilesets/" + spanishTiles));
        }

        if (_languageManager.language == LanguageManager.Languages.French)
        {
            floorTiles.GetComponent<MeshRenderer>().material.SetTexture("_MainTex", Resources.Load<Texture>("Artwork/Tilesets/" + frenchTiles));
        }

        if (_languageManager.language == LanguageManager.Languages.German)
        {
            floorTiles.GetComponent<MeshRenderer>().material.SetTexture("_MainTex", Resources.Load<Texture>("Artwork/Tilesets/" + germanTiles));
        }

        if (_languageManager.language == LanguageManager.Languages.Italian)
        {
            floorTiles.GetComponent<MeshRenderer>().material.SetTexture("_MainTex", Resources.Load<Texture>("Artwork/Tilesets/" + italianTiles));
        }

        if (_languageManager.language == LanguageManager.Languages.Russian)
        {
            floorTiles.GetComponent<MeshRenderer>().material.SetTexture("_MainTex", Resources.Load<Texture>("Artwork/Tilesets/" + russianTiles));
        }

        Debug.Log("Language set to " + _languageManager.language +", Successfully loaded texture : " + floorTiles.GetComponent<MeshRenderer>().material.GetTexture("_MainTex").ToString());
    }

Simplifying the code

Ok, we have our code but its a little too wet, and could certainly be simplified. Rather than using an if statement for each different language, lets just setup a simple set of strings to use as the file prefix, and then pull the language in from the enum instead. Lets say for example that our dialog file is called English_Dialog, French_Dialog, Spanish_Dialog… instead of using if statements, lets just use one line of code to read the file based on the language.

public string dialogPrefix = "Dialog";

dialogLoaded = Resources.Load<TextAsset>("Language/" + dialogPrefix + "_" + language.ToString());

Now you can see that we simply create a standard prefix string, pass in the underscore and then the language as a string. We can now simplify our final code to this:

using UnityEngine;
using System.Collections;
using System;
using System.IO;

public class LanguageManager : MonoBehaviour {

    public enum Languages { English, Spanish, French, German, Russian, Italian }
    [Header("Game Language")]
    public Languages language;

    [Header("Prefix Strings")]
    public string dialogPrefix = "Dialog";
    public string levelPrefix = "Levels";
    public string messagesPrefix = "Messages";
    public string interfacePrefix = "Interface";

    StreamReader fileToOpen;
    TextAsset dialogLoaded;
    TextAsset interfaceLoaded;
    TextAsset messagesLoaded;
    TextAsset levelsLoaded;

    string[] dialogLines;
    string[] interfaceLines;
    string[] messagesLines;
    string[] levelLines;

    void Start()
    {
        ResetLanguage();
    }

    public void ResetLanguage()
    {
        dialogLines = new string[0];
        interfaceLines = new string[0];
        messagesLines = new string[0];
        levelLines = new string[0];

        dialogLoaded = Resources.Load<TextAsset>("Language/" + dialogPrefix + "_" + language.ToString());
        interfaceLoaded = Resources.Load<TextAsset>("Language/" + interfacePrefix + "_" + language.ToString());
        messagesLoaded = Resources.Load<TextAsset>("Language/" + messagesPrefix + "_" + language.ToString());
        levelsLoaded = Resources.Load<TextAsset>("Language/" + levelPrefix + "_" + language.ToString());

        string dialogString = dialogLoaded.ToString();
        dialogLines = dialogString.Split("\n"[0]);

        string interfaceString = interfaceLoaded.ToString();
        interfaceLines = interfaceString.Split("\n"[0]);

        string messageString = messagesLoaded.ToString();
        messagesLines = messageString.Split("\n"[0]);

        string levelString = levelsLoaded.ToString();
        levelLines = levelString.Split("\n"[0]);

    }

    public string ReadDialog(int lineNumber)
    {
        return dialogLines[lineNumber -1];
    }

    public string ReadInterface(int lineNumber)
    {
        return interfaceLines[lineNumber - 1];
    }

    public string ReadMessages(int lineNumber)
    {
        return messagesLines[lineNumber - 1];
    }

    public string ReadLevels(int lineNumber)
    {
        return levelLines[lineNumber - 1];
    }

}

And the same with the tileset script:

    void SetLanguage()
    {
        Debug.Log("Initialising Localisation for level Tiles");
        floorTiles.GetComponent<MeshRenderer>().material.SetTexture("_MainTex", Resources.Load<Texture>("Artwork/Tilesets/" + tilePrefix +"_" + LanguageManager.language.ToString()));
        Debug.Log("Language set to " + Ref.languageManager.language +", Successfully loaded texture : " + floorTiles.GetComponent<MeshRenderer>().material.GetTexture("_MainTex").ToString());
    }

And thats it really, simply call that function when loading the level and there you have it, localised text and images. I hope this has been useful to someone, like I said, its a very simple approach that problably only suits an indie dev, but it certainly does work. Thanks for reading 🙂

James Stone
Indie Game Dev at NerdRage Studios
Indie Game Dev @ NerdRage Studios Ltd.

Self taught game developer, left the IT industry after 20 years in corporate IT management and now out on his own, making games like Xenomorph, Eggstraction and VRbloX. From the UK, currently living just outside Shanghai in China.

Huge retro gaming nerd and collector of stuff which was rubbish in the day, but thanks to eBay and feelings of nostalgia, is now considered worth something ;)

@EpicNerdRage
2016-12-30T04:02:51+00:00

Leave A Comment