Language Localization in Unity

One of the main ways you interact with the player in your games is through language. This post is focusing on written text, but could easily be adapted to spoken words and audio files.

If you are writing a game, for a jam or just for fun, you may not be all that concerned with language localization. I know I never was. Then, I realized just how easy it is implement. I don’t think I’ll ever write a game without it again.

The way I’ve chosen to implement localization, and there are many ways, also gives me some added benefits that I find extremely useful. I create the base for the whole system, a localization class that is instantiated as a singleton. Immediatly, this gives a huge benefit, in that I can access the entire localization system from anywhere in my code statically, without any references to it. So there are no variables to pass and track, no searching for components or game objects to find it. It’s just there an ready for use.

There is a sneaky side benefit to this as well. In the instantiation code for the singleton class, I can do a single Unity search for the object that does the actual feedback to the user, and store it. So now, I can access the localization system and the feedback system from the same singleton. One function call can get localized text and display it to the user.

The basis for how this all works, is that I generate multiple language files that pair keys to text. I’m currently using JSON formatting on the files, because it’s easy to deserialize in C#. You could just as easily use CSV, or XML if you are more comfortable.

They keys can be anything that is comparable. I typically use strings, as I find it easy to make them meaningful for context. I also tend to make a static class structures to utilize as a reference in the code, so I don’t have to memorize the keys, or keep looking back at the language files. You could just as easily use integers or an enum for keys.

My typical language file would look something like the following. I identify the locale as a key. There are plenty of ways to do this, I’ve chosen to use the IETF language tag. I’ve also added a localeName field to represent a plane text name for the language, that would, of course, be in the language of the file. Finally, there is a list of key/value pairs that link the (in this case) string keys to the actual text values to be displayed to the user.

{
"locale":"en-us",
"localeName":"US English",
"items":
[
{"key":"game.title","value":"My Test Game"},
{"key":"object.interact","value":"You interacted with an object"}
]
}

Deserializing (loading) JSON into a class structure in C# is fairly easy for this simple of a structure. There are a couple classes needed. The first class is the base that is going to hold all data from the file. It has fields that correspond to the keys from the JSON file (local, localName, and items). The second class is the one that will hold each item from the list of items in the JSON file. It needs to have corresponding fields (key and value). There’s nothing special about these field names. They simply need to match what you use in the JSON file. The key here is the “[System.Serializable]” attribute applied to each class. This lets the C# serialization code do the work for you.

[System.Serializable]
public class LocalizationData
{
    public string locale;
    public string localeName;
    public LocalizationItem[] items;
}

[System.Serializable]
public class LocalizationItem
{
    public string key;
    public string value;
}

The next thing we’ll need is some code to read the file and build the objects. There are a few tricks here that I’ll walk through. The first highlighted line has a couple things going on. First, I’m using Path.Combine to to concatenate path elements into a valid path for the operating system to use. The first argument is also interesting. Application.streamingAssetsPath is a special path in your project. Create a folder in your Assets folder named StreamingAssets. Spell it exactly like that, capitalization included. Now, when your game is executing Application.streamingAssetsPath will always refer to the full path of that folder. You don’t need to worry about where it actually is on the operating system. In the highlighted line below, I’m also appending a folder named Languages where all the language files are at.

The second highlighted line is what actually converts the JSON formatted text data that is held in fileData into a LocalizationData object. This converts contents of the JSON text file into the object structure we created earlier. The items field is an array of type LocalizationItem. That’s not the best format for the data, and the way we want to use it, so we’ll convert it to a Dictionary<string, string>.

   private Dictionary<string, string> stringSet = new Dictionary<string, string>();
 
   private void LoadLanguage(string filename)
    {
        string fullPath = Path.Combine(Application.streamingAssetsPath, "Languages", filename);

        if (File.Exists(fullPath))
        {
            string fileData = File.ReadAllText(fullPath);
            LocalizationData data = JsonUtility.FromJson<LocalizationData>(fileData);

            locale = data.locale;
            language = data.localeName;

            stringSet.Clear();

            for (int i = 0; i < data.items.Length; i++)
            {
                stringSet.Add(data.items[i].key, data.items[i].value);
            }
        }
        else
        {
            Debug.LogError("File doesn't Exist: " + fullPath);
        }
    }

Now that the language file is loaded into a dictionary, getting language specific versions of strings is simple.

    public string GetLocalizedString(string key)
    {
        string retVal;
        if(stringSet.TryGetValue(key, out retVal))
        {
            return retVal;
        }
        return "";
    }

Now, this works just fine the way it is, but as I said, I like to create an object tree to make using the localization easier. No need to remember the keys, they are all tied to objects.

public static class TextKey 
{
    
    public static class Game
    {
        public static readonly string Title = "game.title";
    }

    public static class Object
    {
        public static readonly string Interact = "object.interact";
    }
}

Pop it all together in a singleton, and you can get language localized strings quick and easy, from anywhere in your code. The following will print text associated with the game.title key to the debug log. In my case, it will print, “My Test Game”.

Debug.Log(Localization.instance.GetLocalizedString(TextKey.Game.Title));

One added benefit… All displayed text is in one place, not scattered all over the code. If some piece of texts needs to change, I don’t need to scour the code looking for it. It’s a simple change in one file per language. Couldn’t be simpler.

Leave a Reply

Your email address will not be published. Required fields are marked *