Reading Strings out of an XML file using C# in Unity 3D

You are here


The code for this project has seen extensive changes and has since been migrated to GitHub. Read more about it over here – and then go forth and fork it. It is released under a Creative Commons License, so you are free to build upon it.

Since I already played around with XML in C#, this part of the project was easier to do than before.

What is it supposed to do?

Basically, I could simply hard-code most of my strings used in the game directly into the code – no one would notice the difference anyway. But obviously, this is not a very good idea, both because editing strings and later translating them becomes a pain.

Creating some data that would allow me to get strings out of an XML file would solve this problem – and, if the code is good enough, be reusable in later games.

It would allow me to edit text independently of the game code and add translations on a later date.

Preliminary format of the XML

The XML file has the following (as of now experimental) format:

Trust in Me 1.0 Kaspar Manz multilingual multi Das ist der Text auf Deutsch. This is the same string in English. Willkommen zu Trust in Me. Welcome to Trust in Me.

In order to allow for different distribution formats, parts of this file can change, as reflected within the <meta> tag.

The file above could contain all strings in all languages for all levels and screens. The format allows for splitting these files up, i. e. into single level files (one file for each level) or single language files (one file for each available language) or a combination thereof.

A a mono-grouped, monolingual file would then look like this:

Trust in Me 1.0 Kaspar Manz en menu Load Game Save Game Delete Game

Comparing both examples, you can see certain features:

  • The XML format does no refer to “levels”, but to “groups”, in order to take into account that also menu and option screens can have their own groups.
  • Whenever either language or grouping have been defined within the meta tag, the attributes aren’t necessary anymore later on.
  • All ids are strings, in order to allow for more understandable coding and to reduce human error while matching the ID and the actual string in the code.
  • The format should also allow for audio files to be matched with the strings. This function is not yet implemented, though.

The format has yet to be formalised in a RelaxNG schema or a DTD.

How to get the strings out of the file

Again, first I thought I could do stuff with a XMLReader – before I realised that I didn’t want to program a glorified typewriter. An anonymous commenter pointed me to LINQ, which sounded great, but isn’t available in Unity, since it uses an older version of the framework.

What’s available is XmlDocument, though – and how comfortable it is. Using simple XPath queries, the document can be easily traversed and the necessary strings can be pulled from it.

The following code prepares the necessary variables:

using System; using UnityEngine; using System.IO; using System.Xml;

public class XMLStringReader : MonoBehaviour { public string localizedStringsFile; string language; string grouping; XmlDocument root;

void Start ()
    // Get the file into the document.
    root = new XmlDocument ();
    root.Load (localizedStringsFile);

    language = root.SelectSingleNode ("localizableStrings/meta/language").InnerText;
    grouping = root.SelectSingleNode ("localizableStrings/meta/grouping").InnerText;


The following code is used to retrieve the strings.

/** *

Retrieves the string by its ID. Only works when the referenced file * is both monolingual and monogrouped. Will throw a NullReference Exception * when the file doesn’t meet the requirements. * The ID of the requested string. * A string with the required text. */ public string GetText (string id) { try { if (language == “multilingual” || grouping == “multi”) { throw new NullReferenceException (“The referenced file is” + “multilangual and/or multigrouped.”); } else { string n = root.SelectSingleNode (“localizableStrings” + “/group/string[@id=’” + id + “’]/text”).InnerText; return n; }

} catch (NullReferenceException ex) {
    string s = "Missing string (" + ex.ToString () + ")";
    return s;


/** *

Retrieves the string by its ID and its level. Assumes the second * parameter to be the level string, but uses it as a language string if it * fails the first time. * The ID of the requested string. * The name of the group in which the string is placed. * A string with the required text. / public string GetText (string id, string level) { try { /TODO Write a routine that silently ignores the level string * when it’s the same of the grouping. */
string n = root.SelectSingleNode (“localizableStrings” + “/group[@id=’” + level + “’]” + “/string[@id=’” + id + “’]” + “/text”).InnerText; return n;

} catch (NullReferenceException exa) {
    // Apparently, there was nothing there ... well, so
    // we simply try and see whether we can use the last string as 
    // a language code.

    try {
        string n = root.SelectSingleNode ("localizableStrings" + "/group/string[@id='" + id + "']" + "/text[@lang='" + level + "']").InnerText;
        return n;
    } catch (NullReferenceException exb) {
        string s = "Missing string (" + exb.ToString () + ")";
        return s;


/** *

Retrieves the string by its ID, its level and its language. * The ID of the requested string. * The name of the group in which the string is placed. * The language the string is required in. * A string with the required text. */ public string GetText (string id, string level, string lang) { try { string xPath = “localizableStrings”; xPath += “/group[@id=’” + level + “’]”; xPath += “/string[@id=’” + id + “’]”; xPath += “/text[@lang=’” + lang + “’]”; string n = root.SelectSingleNode (xPath).InnerText; return n; } catch (NullReferenceException ex) { string s = “[This string has either not been implemented or needs ” + “to be translated.]”; return s; } }

The code may not be really beautiful right now and could be written more clearly and compact. It was more an exercise in overloading methods and using try/catch structures …

The method overloading allows for coding, since only the necessary parameters have to be given (at least, that’s the idea).

Ideas that got trashed along the way

For a brief moment I considered changing that all to a Drupal-style translation system, in which the actual strings in the original language would be string IDs, in order to allow coding along the lines of Button(rect, t(Load Game)); where the ID string could be used as a fallback when the language wasn’t available.

I decided against it, because of the following reasons:

  • Text is, again, distributed over actual game code and an XML file: whenever the wording of the original has to be changed, it has to be changed in the code. This defeats the original purpose of the code.
  • Whenever the original wording is changed in the game code, the translations break (this is also a problem in Drupal, even though strings are handled in a database).
  • Unsanitised input – depending on the text, it could wreak havoc both in the code as well as the XML.

What still has to be done

  • The strings aren’t sanitised – bad things could happen if someone tried to sneak in some strings on his own, I guess.
  • The method using two parameters has to be rewritten – it seems to be too clumsy right now.