Monthly Archives: November 2014

String localization for XAML and C# using dynamically implemented interface

In this post I will describe a solution for easy localized strings management in XAML or C#. Precisely, we will use the usual recommended material for manipulating localized strings in .NET: resx files and the ResourceManager class. However, for XAML manipulation, we will add a “type layer” on top of this. We will see that having typed resources can be very useful. The “type layer” is basically an interface where string properties contain the localized strings, then in XAML or C# code the translations are accessed by using directly these properties. To avoid painful repetitions, the implementation of the interface is dynamically generated using some very simple MSIL. To conclude, we will write simple unit tests that check that the translation files (.resx) contains all the localized strings for all supported languages.

First, let us recall that it is really important that your localized strings are not dispersed in the source code of your application. Using a code snippet of the following form is a bad practice.

//don't do this
TextBox.Text = LocalizeUtil.Localize("Hello","Bonjour","Bon dia");

Indeed, it is very important that you keep grouped all the translations for a given language in one file. Then, you could rework your translations on your own or with a professional without having to grep the entire code base.

Fortunately, .NET comes with all the material you need to handle Culture-Specific resources with the ResourceManager. Say you support english (default) and french languages then you have two .resx files which contains key/value string entries. Such files are named LocalizedStrings.resx and LocalizedStrings.fr-FR.resx they may contain, among others, the entry SayHello (“Hello!” in english and “Bonjour !” in french). Finally, you only have to initialize the ResourceManager and getting the localized strings as follows.

var rem = new ResourceManager("LocalizedStrings", Assembly.GetExecutingAssembly());
Console.WriteLine(rem.GetString("SayHello"));

It is important to note that the right file (*.resx or *.fr-FR.resx) is automatically chosen using the Tread.CurrentThread.CurrentUICulture.

When manipulating XAML for the UI of your app, creating a type for the localized string is recommended. Indeed, in XAML you can bind to properties but you cannot (at least not easily) bind to methods. So the recommended method from the MSDN is to create a LocalizedStrings class whose members are the localized strings.

So we can create the LocalizedStrings class

public class LocalizedStrings
{
    private readonly ResourceManager _rem
    public LocalizedStrings(ResourceManager rem)
    {
        _rem = rem;
    }

    public string SayHello
    {
       { get {return _rem.GetString("SayHello"); }
    }

    public string SayHelloAgain
    {
       { get {return _rem.GetString("SayHelloAgain"); }
    }

    /* A lot of properties more..... */
}

While the instance is retrieved using a Singleton-like pattern (there is no reason to mock this for testing so it makes sense to use a singleton here).

public class StringLocalizer
{
    private static LocalizedStrings _localizedStrings;

    public static LocalizedStrings Strings
    {
        get
        {
            if (_localizedStrings == null)
            {
                  _localizedStrings = new LocalizedStrings(new ResourceManager("LocalizedStrings", Assembly.GetExecutingAssembly())));
             }
             return _localizedStrings;
        }
     }
}

We can use easily the LocalizedStrings in the xaml after adding the StringLocalizer has an application resource.

<!-- set the localizer has a resource -->
 <Application.Resources>
   <local:StringLocalizer xmlns:local ="clr-namespace:appNamespace"
                           x:Key="Localizer" />
 </Application.Resources>
 <!-- in the window or user control -->
 <Button DockPanel.Dock="Left"
         Command="{Binding Next}"
         ToolTip="{Binding Strings.SayHello, Source={StaticResource Localizer}}">

Obviously we can use the same class in the plain old C#

Console.WriteLine(StringLocalizer.Strings.SayHello);

Using this “type layer” is not only mandatory for XAML it is also very useful because we benefit from the static typing. Indeed, typing help us to create localized strings list that do not contain tons on unused entries. Even if this is not a matter of life or death, it is a good thing to remove unused localized strings from your dictionaries, because they going to cost you some effort or money when you will make new languages available or if you want to review the terminology used in your application. The only thing you have to do is to search for unused member of the class (the plugin Resharper does this well even in XAML).

As shown in the image below the XAML-intellisense of Resharper shows us all members of the interface which is handy to reuse localized strings.

The plugins resharper shows all properties of the LocalizedStrings class

The plugins resharper shows all properties of the LocalizedStrings class

Still there is one thing which is cumbersome, every-time you create a new entry you have to type the same logic for the property: _rem.GetString(“NameOfTheProperty”). Then, it’s time to be nerdy and find a way to do this automatically! Let us replace the LocalizedStrings class by and interface ILocalizedStrings whose implementation is dynamically generated. You can emit dynamically new type in .NET using the ILGenerator. So this is the implementation of the new StringLocalizer with some help from the .NET IL guru Olivier Guimbal.

So here it is what the interface looks like

public interface ILocalizedStrings
{
    void SetResMan(ResourceManager man);

    string SayHello{ get; }
    string SayHelloAgain{ get; }
    /* A lot of properties more..... */
}

And here is the StringLocalizer

public class StringLocalizer
{
    private static class InstanceGenerator
    {
        static readonly Dictionary<Type, ILocalizedStrings> Implementations = new Dictionary<Type, ILocalizedStrings>();
        static int _implCount = 0;

        static ModuleBuilder _moduleBuilder;
        static ModuleBuilder ModuleBuilder
        {
            get
            {
                if (_moduleBuilder != null)
                    return _moduleBuilder;
                var assemblyName = new AssemblyName { Name = "Services" };
                AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave);
                _moduleBuilder = assemblyBuilder.DefineDynamicModule("ServicesModule", "ServicesModule.dll");
                return _moduleBuilder;
            }
        }

        public static ILocalizedStrings CreateImplementation()
        {
            Type interfaceType = typeof(ILocalizedStrings);
            if (!interfaceType.IsInterface)
                throw new ArgumentException(interfaceType + " is not an interface type");

            ILocalizedStrings found;
            if (Implementations.TryGetValue(interfaceType, out found))
                return found;

            var module = ModuleBuilder;
            var tb = module.DefineType(interfaceType.FullName + "Impl" + (_implCount++), TypeAttributes.Public | TypeAttributes.BeforeFieldInit, typeof(object), new[] { interfaceType });

            FieldBuilder fld = tb.DefineField("_resourceManager", typeof(ResourceManager), FieldAttributes.Private);

            var setter = tb.DefineMethod("SetResMan", MethodAttributes.Public | MethodAttributes.Virtual, CallingConventions.HasThis, typeof(void), new Type[] { typeof(ResourceManager) });
            MethodInfo setMethodInfo = typeof(ILocalizedStrings).GetMethod("SetResMan");

            var setterIl = setter.GetILGenerator();
            setterIl.Emit(OpCodes.Ldarg_0);
            setterIl.Emit(OpCodes.Ldarg_1);
            setterIl.Emit(OpCodes.Stfld, fld);
            setterIl.Emit(OpCodes.Ret);

            tb.DefineMethodOverride(setter, setMethodInfo);

            var props = interfaceType.GetProperties();

            foreach (var pi in props)
            {
                if (pi.PropertyType != typeof(string))
                    throw new ArgumentException("Only string properties are supported");

                if (pi.CanWrite)
                    throw new ArgumentException("Property must be read-only: " + interfaceType + " -> " + pi.Name);

                MethodInfo methodInfo = pi.GetGetMethod();
                var mb = CreateOverride(tb, methodInfo);

                var il = mb.GetILGenerator();
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldfld, fld);
                il.Emit(OpCodes.Ldstr, pi.Name);
                il.Emit(OpCodes.Call, typeof(ResourceManager).GetMethod("GetString", new[] { typeof(string) }));
                il.Emit(OpCodes.Ret);

                PropertyBuilder pb = tb.DefineProperty(pi.Name,
                       PropertyAttributes.HasDefault,
                        CallingConventions.HasThis, methodInfo.ReturnType,
                        methodInfo.GetParameters().Select(p => p.ParameterType).ToArray());
                pb.SetGetMethod(mb);
            }

            found = (ILocalizedStrings)Activator.CreateInstance(tb.CreateType());
            Implementations.Add(interfaceType, found);
            return found;
        }

        static MethodBuilder CreateOverride(TypeBuilder tb, MethodInfo mi)
        {
            var mb = tb.DefineMethod(mi.Name,
                        MethodAttributes.Public
                        | MethodAttributes.HideBySig
                        | MethodAttributes.NewSlot
                        | MethodAttributes.Virtual
                        | MethodAttributes.Final,
                        CallingConventions.HasThis, mi.ReturnType, mi.GetParameters().Select(p => p.ParameterType).ToArray());
            tb.DefineMethodOverride(mb, mi);

            return mb;
        }
    }

    private static ILocalizedStrings _localizedStrings;

    public static ILocalizedStrings Strings
    {
        get
        {
            if (_localizedStrings == null)
            {
                _localizedStrings = (ILocalizedStrings)InstanceGenerator.CreateImplementation();
                _localizedStrings.SetResMan(new ResourceManager("LocalizedStrings", Assembly.GetExecutingAssembly())));
            }
         return _localizedStrings;
        }
    }
}
Tests are failing because some properties of the ILocalizedStrings are missing in the resx.

Tests are failing because there are some unused entries in the resx.

To conclude let us write unit tests to ensure that all properties in the interface ILocalizedStrings are defined on all .resx files and, reciprocally, to make sure that all keys in the resx files are properties of the interface. This will help us to track non translated entries and to remove useless keys in the resx dictionaries. We basically parse the .resx files to retrieve all keys and use reflexion to get all public properties of the interface. The test fail when problematic entries are discovered and printed in the test console.

[TestClass]
public class TranslationTests
{

   private TestContext _testContextInstance;
   public TestContext TestContext
   {
       get
       {
           return _testContextInstance;
       }
       set
       {
           _testContextInstance = value;
       }
   }

   private void TestPropertiesAndResxKeyAreEqual(string filePath)
   {
       XDocument xDoc = XDocument.Load(filePath);

       HashSet<string> resxKeys = new HashSet<string>(xDoc.Descendants("data").Select(c => c.Attribute("name").Value));
       HashSet<string> interfaceProp =
           new HashSet<string>(
               typeof (ILocalizedStrings).GetProperties(BindingFlags.Public | BindingFlags.Instance)
                   .Select(p => p.Name));

       bool fail = false;
       foreach (var r in resxKeys)
       {
           if (!interfaceProp.Contains(r))
           {
               _testContextInstance.WriteLine(string.Format("The key {0} exists in resx but not in interface", r));
               fail = true;

           }
       }

       foreach (var property in interfaceProp)
       {
           if (!resxKeys.Contains(property))
           {
               _testContextInstance.WriteLine(string.Format("The key {0} exists in interface but not in resx", property));
               fail = true;
           }
       }
       if (fail)
       {
           Assert.Fail();
       }

   }

   [TestMethod]
   public void TestEnglishResx()
   {
       const string path = @"../../../MyApp/LocalizedStrings.resx";
       TestPropertiesAndResxKeyAreEqual(path);
   }

   [TestMethod]
   public void TestFrenchResx()
   {
       const string path = @"../../../MyApp/LocalizedStrings.fr-FR.resx";
       TestPropertiesAndResxKeyAreEqual(path);
   }
}