Friday, April 20, 2012

Localization with C# Extensions

In this short post I am going to describe how I handle localization for different platforms. Actually each mobile platform comes with its own approach to this topic. But when you develop cross platform you probably don't want to complicate things by using the native solutions.
Therefore I came up with a very simple implementation that is just enough for my needs. Basically, in my approach localization just means to replace a text string with a text string for the local language. I do this by using three things:
  • An xml file with the localized strings,
  • a singleton class to handle the strings and
  • an Extension to the C# String class.
Insinde the xml file I declare a Text node with a name attribute, which is the identifier for the text. For me the identifier already is the default text. If you work with a bigger project it might be better to use a pure placeholder text instead. Inside the Text node there are Item nodes. Each Item node has an attribute language and value. As language identifier I use the  ISO 639-1 two-letter code which you can obtain easily in dot Net. The value is obviously the translated text. This is an example of my xml file:
<?xml version="1.0" encoding="utf-8" ?>
<TgexLocalization>
  <Text name="Score">
    <Item language="de" value="Punkte"></Item>
  </Text>
  <Text name="Multiplier">
    <Item language="de" value="Multiplikator"></Item>
  </Text>
</TgexLocalization>
The localization file and data is then handled by a singleton class, the Localizer. Before using the Localizer the Initialize function has to be called once, giving the xml file filename as parameter. The Localizer just has a dictionary of a string as key and string as value. When it parses the xml file it will just add entries to the dictionary when a Text node with an Item of the current language ID is found. In the above example, the dictionary would only be filled if the local system is German. In this case the dictionary would get two entries.
To get a localized text, you can now simple call
Localizer.Instance.GetText("Score");
If your system is not German, it would just return the same string that you passed as paramater, "Score". But if your system is German it finds the entry in the dictionary and returns "Punkte".
The full Localizer.cs looks like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Text;
using System.Globalization;
using System.IO;

#if ANDROID
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
#endif

namespace Tgex
{
    public class Localizer
    {
        protected String m_tag;
        /// <summary>
        /// String tag that defines the languages (2 char)
        /// </summary>
        public String Tag
        {
            get { return m_tag; }
            set { m_tag = value; }
        }

        Dictionary<string, string> m_strings = new Dictionary<string, string>();

        // create singleton
        static readonly Localizer instance = new Localizer();
        /// <summary>
        /// Get instance of singleton.
        /// </summary>
        public static Localizer Instance
        {
            get
            {
                return instance;
            }
        }
        /// <summary>
        /// Explicit static constructor to tell C# compiler
        /// not to mark type as beforefieldinit
        /// </summary>
        static Localizer()
        {
        }
        /// <summary>
        /// Constructor
        /// </summary>
        Localizer()
        {
            m_tag = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
        }

        public void Initialize(string filename)
        {
#if ANDROID
   var reader = new StreamReader(Game.Activity.Assets.Open (filename));
            XDocument xmlFile = XDocument.Load(reader);
#else

            XDocument xmlFile = XDocument.Load(filename);
#endif
            XElement root = xmlFile.Element("TgexLocalization");

            // read strings
            var texts = root.Descendants("Text");
            if (texts != null)
            {
                texts.ForEach(text =>
                {
                    var items = text.Elements("Item");
                    if (items != null)
                    {
                        XElement locStringXml = items.FirstOrDefault(item => item.Attribute("language").Value.ToString() == m_tag);
                        if (locStringXml != null)
                        {
                            m_strings.Add(text.Attribute("name").Value.ToString(), locStringXml.Attribute("value").Value.ToString());
                        }
                    }
                });
            }
        }

        public String GetText(string name)
        {
            if (m_strings.ContainsKey(name))
            {
                return m_strings[name];
            }
            return name;
        }
    }
}

To make the access simpler I use C# Extensions. I love this feature of C# and my Extensions class is actually very huge - I will probably post some of my most useful code in other blog posts. For now, I just show an extension to the String class. I define an extension function Localize(), that makes the call to the Localizer class. By that, I can localize a string by writing:
"Score".Localize();
The Extensions.cs class for that looks like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Tgex
{
    public static class Extensions
    {
        public static String Localize(this String text)
        {
            return Localizer.Instance.GetText(text);
        }
        // helper so that you can use ForEach on any IEnumerable
        public static void ForEach<T>(this IEnumerable<T> values, Action<T> action)
        {
            foreach (var v in values)
            {
                action(v);
            }
        }
    }
}
Even though this only translates text, you can use it also for resource strings. I use it for texture names, so I can also easily localize buttons with text on it.
This is already all that I need for localization of my games. I hope this little and simple approach might be useful for some cross platform developers.

5 comments:

  1. Hey Timo, Mind if i make a Github project from this? got some ideas to generalize this as well... is a solid cross-dev solution for the mono tooling... :)

    ReplyDelete
    Replies
    1. Sure, you are welcome to create a Github project from this :)

      Delete
  2. btw: there's a small error in this:

    texts.ForEach on line #74 is a helper that isnt included i presume...

    changing this to

    foreach (XElement text in texts) { code here } fixes this but i thought i'd share the issue with ya ;)

    ReplyDelete
    Replies
    1. Yes right, thanks for the hint. I am already so much used to this, that I did not notice. Inside the Extensions class I use:
      public static void ForEach(this IEnumerable values, Action action)
      {
      foreach (var v in values)
      {
      action(v);
      }
      }

      I will add it above.

      Delete
  3. Have you tried this online localization tool: https://poeditor.com? It doesn’t need downloading and your project, if imported in .po format, can be exported in .mo, after doing the necessary changes or translations.

    ReplyDelete