Completeness Testing of Localised RESX Files

It happened once upon a time in a project in a far off galaxy. I was asked to develop a concept for Internationalisation and Localisation for a customer. With the simple stuff over, I started considering how we could ensure that what returned from the localisation and translation process matched what we had given them and that no string elements were missing.

It turns out to be quite simple. Most translation systems provide the ability to mark the translated text during the first pass from a Standard English used in the development phase to some new language. The new translation can be optionally prefixed with some sort of string to make sure that the text has been translated. When not then the string appears un-translated in the GUI and is immediately obvious. Well what if we apply this trick when testing the RESX files that is returned to us. First we define a prefixing that does not clash with what the translators will be using. Then we prefix all our string in the base RESX – this is usually the one which contains the code behind in the C# or VB.Net project file.

All is well and good. We have our prefixed base resources and we have our translated resources that have been returned. All we must do now is check that all returned strings do not contain our prefix. Of course if the translated sting still contains the prefix used by the translators we still might have a problem if is returned to use. However, if we also know this prefix string we can also check that the translator has completed their job properly.

Just how cool can you get?

Okay, I understand that you are only interested in the code that does the checking. But first a little background. To successfully translate all language translations and string automatically we require knowing a number of things upfront; firstly, we need to know what languages we are supporting. These locales or root languages are placed in a static string list within our test. Secondary, what we also need to know are any translation prefixes that will be used by the translators as well as our own one. Finally, we need a way of reading out all the resource string from code behind of the base RESX file.

As to the test, it will fail if the string found for the specific language is prefixed with either our defined prefix or that defined by the translators. Is this simple – yes?

The final hurdle that we have is reading the properties from the base code behind. For that we have reflection. Okay I know this is slow, but in this case justified – the machine will always be quicker than if the complete app or RESX is eyeballed.

Here is the code:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Resources;
using System.Threading;
using ConsoleI18NApplication.Properties;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace
TestProject1
{
  /// <summary>
  /// Some test class.
  /// </summary>
  [TestClass]
  public class UnitTest1
  {
    /// <summary>
    /// The cultures we support.
    /// </summary>
    private static readonly IList<string> Cultures = new
          List<string> { “en-US”, “en”, “es”, “fr”, “it”, “de”, “ja” };

    /// <summary>
    /// Invariant resource strings.
    /// </summary>
    private static readonly Dictionary<string, string> BaseDetails = new
          Dictionary<string, string>();

    /// <summary>
    /// A list of properties in the code-behind.
    /// </summary>
    private static readonly List<PropertyInfo> PropertyInfos = new 
         
List<PropertyInfo>(
typeof(Resources).GetProperties(BindingFlags.Public | BindingFlags.Static));

    /// <summary>
    /// The invariant culture.
    /// </summary>
    private static CultureInfo baseCulture;

    /// <summary>
    /// Gets or sets the test context which provides
    /// information about and functionality for the current test run.
    /// </summary>
    /// <value>
    /// The test context.
    /// </value>
    public TestContext TestContext { get; set; }

    #region
Additional test attributes

    /// <summary>
    /// The strings class initialize.
    /// </summary>
    /// <param name=”testContext”>
    /// The test context.
    /// </param>
    [ClassInitialize]
    public static void StringsClassInitialize(TestContext testContext)
    {
      baseCulture = CultureInfo.InvariantCulture;
      Thread.CurrentThread.CurrentCulture = baseCulture;
      Thread.CurrentThread.CurrentUICulture = baseCulture;

      // load the string resource names and values from the base resource
      // file (usually the main resource assembly
      var strs = new Resources();

      foreach (var info in PropertyInfos)
      {
        if (info.Name.Equals(“ResourceManager”) || info.Name.Equals(“Culture”)) continue;

        var val = strs.GetType().GetProperty(info.Name).GetValue(strs, null);
        var str = val as string;

        // add the property name and the value to a dictionary
        if (str != null)
        {
          BaseDetails.Add(info.Name, str);
        }
      }
    }

    /// <summary>
    /// The strings class cleanup.
    /// </summary>
    [ClassCleanup]
    public static void StringsClassCleanup()
    {
    }

    /// <summary>
    /// The strings test initialize.
    /// </summary>
    [TestInitialize]
    public void StringsTestInitialize()
    {
    }

    /// <summary>
    /// The strings test cleanup.
    /// </summary>
    [TestCleanup]
    public void StringsTestCleanup()
    {
    }

    #endregion


    /// <summary>
    /// The test method 1.
    /// </summary>
    [TestMethod]
    public void DetailsTest()
    {
      try
      {
        foreach (var culture in Cultures)
        {
          var newCulture = new CultureInfo(culture);
          Thread.CurrentThread.CurrentCulture = newCulture;
          Thread.CurrentThread.CurrentUICulture = newCulture;

          var strs = new Resources();
          foreach (var info in PropertyInfos)
          {
            if (info.Name.Equals(“ResourceManager”) || info.Name.Equals(“Culture”)) continue;

            var val = strs.GetType().GetProperty(info.Name).GetValue(strs, null);
            var str = val as string;

            Assert.AreNotEqual(str, BaseDetails[info.Name],
                               string.Format(“No translation for {0} found in culture {1}.”,
                               info.Name, culture));
          }
        }
      }
      catch (CultureNotFoundException e)
      {
        Assert.Fail(e.Message);
      }
      catch (MissingSatelliteAssemblyException e)
      {
        Assert.Fail(e.Message);
      }
      catch (MissingManifestResourceException e)
      {
        Assert.Fail(e.Message);
      }
    }
  }
}

Take care to adjust the Cultures list to reflect the locales and cultures that you will support.

Oh, I almost forgot. A default parameter-less constructor is required. This is not the case where the code-behind has been automatically generated. The generated internal constructor must be manually edited and made public for reflection to work. Unfortunately, if the base RESX file is regenerated you will need to set the constructor public once again – annoying but still better than eyeballing the RESX manually. Additionally, there could be problems when the namespace for the test is other than the where the resource assembly resides – I have left that one for the reader!

That’s all folks…

Advertisements

~ by Intelligence4 on January 21, 2011.

 
%d bloggers like this: