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

Update

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:

  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <localizableStrings>
  3.  <meta>
  4.        <gameName>Trust in Me</gameName>
  5.        <version>1.0</version>
  6.      <author>Kaspar Manz</author>
  7.        <language>multilingual</language>
  8.       <grouping>multi</grouping>
  9.  </meta>
  10.   <group id="1">
  11.      <string id="1">
  12.             <text lang="de">Das ist der Text auf Deutsch.</text>
  13.          <text lang="en">This is the same string in English.</text>
  14.            <audio lang="de">audio/de/1/1.ogg</audio>
  15.             <audio lang="en">audio/en/1/1.ogg</audio>
  16.         </string>
  17.         <string id="2">
  18.             <text lang="de">Willkommen zu Trust in Me.</text>
  19.             <text lang="en">Welcome to Trust in Me.</text>
  20.        </string>
  21.     </group>
  22. </localizableStrings>

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:

  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <localizableStrings>
  3.  <meta>
  4.        <gameName>Trust in Me</gameName>
  5.        <version>1.0</version>
  6.      <author>Kaspar Manz</author>
  7.        <language>en</language>
  8.         <grouping>menu</grouping>
  9.   </meta>
  10.   <group>
  11.       <string id="button-load">
  12.           <text>Load Game</text>
  13.      </string>
  14.         <string id="button-save">
  15.           <text>Save Game</text>
  16.      </string>
  17.         <string id="button-delete">
  18.             <text>Delete Game</text>
  19.        </string>
  20.     </group>
  21. </localizableStrings>

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:

  1. using System;
  2. using UnityEngine;
  3. using System.IO;
  4. using System.Xml;
  5.  
  6.  
  7. public class XMLStringReader : MonoBehaviour
  8. {
  9.   public string localizedStringsFile;
  10.     string language;
  11.    string grouping;
  12.    XmlDocument root;
  13.  
  14.  void Start ()
  15.   {
  16.       // Get the file into the document.
  17.      root = new XmlDocument ();
  18.      root.Load (localizedStringsFile);
  19.  
  20.      language = root.SelectSingleNode ("localizableStrings/meta/language").InnerText;
  21.      grouping = root.SelectSingleNode ("localizableStrings/meta/grouping").InnerText;
  22.  
  23.     }

The following code is used to retrieve the strings.

  1. /<strong>
  2.  * <summary>Retrieves the string by its ID. Only works when the referenced file
  3.  * is both monolingual and monogrouped. Will throw a NullReference Exception
  4.  * when the file doesn't meet the requirements.</summary>
  5.  * <param name="id">The ID of the requested string.</param>
  6.  * <returns>A string with the required text.</returns>
  7.  */
  8. public string GetText (string id)
  9. {
  10.   try {
  11.       if (language == "multilingual" || grouping == "multi") {
  12.            throw new NullReferenceException ("The referenced file is" + "multilangual and/or multigrouped.");
  13.      } else {
  14.            string n = root.SelectSingleNode ("localizableStrings" + "/group/string[@id='" + id + "']/text").InnerText;
  15.             return n;
  16.       }
  17.  
  18.    } catch (NullReferenceException ex) {
  19.       string s = "Missing string (" + ex.ToString () + ")";
  20.       return s;
  21.   }
  22. }
  23.  
  24. /</strong>
  25.  * <summary>Retrieves the string by its ID and its level. Assumes the second
  26.  * parameter to be the level string, but uses it as a language string if it 
  27.  * fails the first time.</summary>
  28.  * <param name="id">The ID of the requested string.</param>
  29.  * <param name="level">The name of the group in which the string is placed.</param>
  30.  * <returns>A string with the required text.</returns>
  31.  */
  32. public string GetText (string id, string level)
  33. {
  34.  try {
  35.       /*TODO Write a routine that silently ignores the level string
  36.        * when it's the same of the grouping.
  37.          */         
  38.        string n = root.SelectSingleNode ("localizableStrings" + "/group[@id='" + level + "']" + "/string[@id='" + id + "']" + "/text").InnerText;
  39.      return n;
  40.  
  41.    } catch (NullReferenceException exa) {
  42.      // Apparently, there was nothing there ... well, so
  43.         // we simply try and see whether we can use the last string as 
  44.         // a language code.
  45.  
  46.        try {
  47.           string n = root.SelectSingleNode ("localizableStrings" + "/group/string[@id='" + id + "']" + "/text[@lang='" + level + "']").InnerText;
  48.           return n;
  49.       } catch (NullReferenceException exb) {
  50.          string s = "Missing string (" + exb.ToString () + ")";
  51.          return s;
  52.       }
  53.   }
  54. }
  55.  
  56. /**
  57.  * <summary>Retrieves the string by its ID, its level and its language.</summary>
  58.  * <param name="id">The ID of the requested string.</param>
  59.  * <param name="level">The name of the group in which the string is placed.</param>
  60.  * <param name="lang">The language the string is required in.</param>
  61.  * <returns>A string with the required text.</returns>
  62.  */
  63. public string GetText (string id, string level, string lang)
  64. {
  65.    try {
  66.       string xPath = "localizableStrings";
  67.      xPath += "/group[@id='" + level + "']";
  68.       xPath += "/string[@id='" + id + "']";
  69.         xPath += "/text[@lang='" + lang + "']";
  70.       string n = root.SelectSingleNode (xPath).InnerText;
  71.         return n;
  72.   } catch (NullReferenceException ex) {
  73.       string s = "[This string has either not been implemented or needs " + "to be translated.]";
  74.         return s;
  75.   }
  76. }

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.
Social Tags:
XML

Comments

Thank you, really useful

Hi,

I would like to load an XML file trough a GUI.

The XML file has to load differens links like this :

Example :

Id 1 will open links 1 in a new internet windows /Id 2 will open links 2 in a new internet windows / Id 3 will open links 3 in a new internet windows …

Can you give me some help to do this ?

I’m working in C#

Many thanks for your help !

Hey Michael,

you might want to visit the Unity Answers Forum, where you should find an answer to your question.

problema version=3D”1.0”
Hola a todos.

Tengo un proceso que descarga automatica de archivos anexos XML y PDF, el problema que tengo que al descargar el XML automaricamente remplaza algunos caracteres desconocidos que marcan error al intentar cargar el XML en un XmlDocument, por ejemplo: lo remplaza por el siguiente texto y genera el error que les menciono.

si alguien pudiera ayudarme al respecto les agradeceria bastante. Gracias.

If you’re involved in localization projects and are interested in tools that can help you better manage the string translation process, you might want to check out the online platform https://poeditor.com/ It has API and Translation Memory, and the work environment is collaborative.

Add new comment