XAML Playground
about XAML and other Amenities

Simplify localization with a T4 template

2010-11-25T23:07:04+01:00 by Andrea Boschin

If you evere tryied to localize a Silverlight application, you have found for sure that it is not a straighforward task. Like all the other .NET tecnologies the Silverlight projects has resource files and you can create satellite assemblies to contains culture specific strings. But the problem is that it is not so easy to bring this string to the interface.

When you create a resx file Visual Studio generates a code file with a class ready to be used to read the strings. You can also choose to make this class public, so it is possible to use it across different projects. Unfortunately this class has an internal constructon so there is no way to add an instance to the App.xaml resources and use DataBinding to reference the strings in the user interface.

Someone suggest to manually change the ctor of the class but to me it is very frustrating to correct this simple bug every time I add a new resource. So it is required to create a proxy class, that wraps the static one. Every property will call the correspondent static property and then the proxy class will be used for the DataBinding. When you are developing a simple application this is a simple task, but what about when you have an huge number of strings?

With this in mind I've worked to a T4 template that it is able to read a resx file and automatically produces the proxy class. If you do not know it, the T4 template is a very powerful tool that exists since Visual Studio 2008. With it you can write code to generate code inside Visual Studio, and it is exactly what I'm searching for.

Here is the template:

   1: <#@ template debug="false" hostspecific="true" language="C#" #>
   2: <#@ output extension=".cs"  encoding="UTF8" #>
   3: <#@ assembly name="System.Core" #>
   4: <#@ assembly name="System.Xml" #>
   5: <#@ assembly name="System.Xml.Linq" #>
   6: <#@ import namespace="System.IO" #>
   7: <#@ import namespace="System.Text.RegularExpressions" #>
   8: <#@ import namespace="System.Linq" #>
   9: <#@ import namespace="System.Xml" #>
  10: <#@ import namespace="System.Xml.Linq" #>
  11: //------------------------------------------------------------------------------
  12: // <auto-generated>
  13: //     This code was generated by a tool.
  14: //
  15: //     Changes to this file may cause incorrect behavior and will be lost if
  16: //     the code is regenerated.
  17: // </auto-generated>
  18: //------------------------------------------------------------------------------
  19:  
  20: <#
  21: string appName = "ResXProxy Generator Template";
  22: string version = "1.0.3977.0";
  23: string ns = (string)System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("NamespaceHint");
  24: string resxFileName = Path.ChangeExtension(Host.TemplateFile, ".resx");
  25: string resxClassName = Path.GetFileNameWithoutExtension(Host.TemplateFile);
  26: string proxyClassName = string.Format("{0}ResourceProxy", resxClassName);
  27: XDocument document = XDocument.Parse(File.ReadAllText(resxFileName));
  28: #>
  29: namespace <#=ns#>
  30: {
  31:     using System.Globalization;
  32:     using System.Windows.Markup;
  33:  
  34:     /// <summary>
  35:     /// Represent a proxy class for "<#= resxClassName #>" resources
  36:     /// </summary>
  37:     [System.Diagnostics.DebuggerStepThroughAttribute()]
  38:     [System.CodeDom.Compiler.GeneratedCode("<#= appName #>", "<#= version #>")]
  39:     public class <#= proxyClassName #>
  40:     {
  41:         /// <summary>
  42:         /// Initializes the "<#= proxyClassName #>" class
  43:         /// </summary>
  44:         public <#= proxyClassName #>()
  45:         {}
  46:     
  47:         /// <summary>
  48:         /// Gets the current culture
  49:         /// </summary>
  50:         [System.CodeDom.Compiler.GeneratedCode("<#= appName #>", "<#= version #>")]
  51:         public CultureInfo CurrentCulture 
  52:         { 
  53:             get { return CultureInfo.CurrentCulture; } 
  54:         }
  55:  
  56:         /// <summary>
  57:         /// Gets the current UI Culture
  58:         /// </summary>
  59:         [System.CodeDom.Compiler.GeneratedCode("<#= appName #>", "<#= version #>")]
  60:         public CultureInfo CurrentUICulture 
  61:         {
  62:             get { return CultureInfo.CurrentUICulture; } 
  63:         }
  64:         
  65:         /// <summary>
  66:         /// Gets the current Xml Language property
  67:         /// </summary>
  68:         [System.CodeDom.Compiler.GeneratedCode("<#= appName #>", "<#= version #>")]
  69:         public XmlLanguage Language 
  70:         { 
  71:             get { return XmlLanguage.GetLanguage(CultureInfo.CurrentUICulture.Name); } 
  72:         }
  73: <# foreach(var item in document.Element("root").Elements("data")) 
  74:    { 
  75:         string name = EscapeName(item);
  76:     
  77:         if (item.Attributes("type").Count() == 0)
  78:         {
  79: #>
  80:  
  81:         /// <summary>
  82: <# if (item.Elements("comment").Count() == 1) { #>
  83:         /// <remarks><#= item.Element("comment").Value #></remarks>
  84: <# } #>
  85:         /// Gets the "<#= name #>" Property
  86:         /// </summary>
  87:         [System.CodeDom.Compiler.GeneratedCode("<#= appName #>", "<#= version #>")]
  88:         public string <#= name #> 
  89:         { 
  90:             get { return <#= resxClassName + "." + name #>; } 
  91:         }
  92: <# 
  93:         }
  94: }
  95: #>
  96:     }
  97: }<#+
  98: public string EscapeName(XElement item)
  99: {
 100:     string name = item.Attribute("name").Value;
 101:     return Regex.Replace(name, "[^a-zA-Z0-9_]{1,1}", "_");
 102: }
 103: #>

Using the template is really easy. First of all you have to configure the Silverlight application to be localizable ( you can find how to do)  and then create the required resx files. Finally you have to add the template using the same name of the main resource file but using the .tt extension. E.g. if your resx is called AppString.resx you have to call the template AppStrings.tt. Nothing else. Once you have added the template it automatically generates a class called AppStringResourceProxy you can easily reference from a XAML file and bind to the UI. Remember that every time you change the resx file you have to select "Run custom tool" from the context menu of the T4 template to generate the code again.

The generated class does not contains only the string properties but also expose a Language property. You can bind this property to the corresponding Language property of the page (the UserControl) and override the default culture that is usually set to "en-US". Doing this you will have also the Dates and numeric values converted to the right culture.

   1: <UserControl Language="{Binding Language, Source={StaticResource AppString}}" />

The tamplate works fine for Silverlight and Windows Phone. I've never tryied but I think it works also with WPF. Hope this helps.

Download Sample: Silverlightplayground.Localization.zip (96,3 KB)

Comments (2) -

November 29. 2010 13:22

As far as I know, this workaround isn't needed for WPF, because WPFs xaml loader doesn’t try to construct an instance of the generated resource class (so no problem with the internal constructor). Unfortunately Silverlight tries to instantiate this class, which is needless as the localization strings are just static properties.

Thomas Mutzl

November 29. 2010 13:27

Thanks for your comment. I usually work with Silverlight and do not use WPF so much. So probably you are right. Smile

Andrea Boschin