Running with Code Like with scissors, only more dangerous


Localizing a WPF Application with ResX Files

Posted by Rob

As I mentioned during my rant earlier this week, localization is fairly difficult with WPF.  As it stands right now, the only real tool support Microsoft offers for localization is a tool called LocBaml.exe, which they clearly mark as "not production-ready."  Samples available online describe how to utilize it, but there are significant drawbacks to its current implementation.  First, since LocBaml generates CSV files, it is difficult or impossible to localize most anything other than simple strings; images, for instance, are impossible to embed into a CSV file.  ResX, an XML-based file format, has not only been incredibly strong on this front, but in fact is an excellent choice because of its rich tool support available in the Visual Studio IDE.

I won't go into the depths of how LocBaml is used for localizing a WPF application; the linked sample on CodeProject and the MSDN site should give you a fairly strong idea of how it's implemented.  But here is a bulleted list of its major drawbacks.

Drawbacks to using LocBaml for WPF Localization:

  • Localization is done per-XAML-file and not per resource such as string or image.
  • Localized resources are not strongly-typed; rather, they are stored as MemoryStreams:
    A resource assembly loaded into Reflector
  • Only string resources can be easily localized.
  • Localization takes place out of the IDE and away from the localized task; generating the CSV file is done after the initial assembly is compiled and then the satellite assemblies are generated later.
  • Assemblies with strong name signatures may need to be delay-signed, waiting until after the resource modules have been generated.  (I'm not 100% certain about this).

Bending XAML to Your Localization Will

It turns out that XAML already has the perfect tool set for your application: Attached Properties.  By setting attached properties with specific IDs, we can generate markup that cleanly pulls in localization resources.  Here's a sample XAML file that demonstrates this approach:

   1:  <Window x:Class="LocalizationTestTake3.Window1"
   2:      xmlns=""
   3:      xmlns:x=""
   4:      xmlns:loc="clr-namespace:LocalizationTestTake3"
   5:      Title="Window1" Height="300" Width="300">
   6:      <Grid>
   7:          <Button Height="45" Margin="55,82,75,0" loc:LocalizationProvider.ContentID="HelloWorld" Name="button1" VerticalAlignment="Top"></Button>
   8:          <Button Height="23" Margin="94,178,109,0" loc:LocalizationProvider.ContentID="English" Name="button2" VerticalAlignment="Top" Click="button2_Click"></Button>
   9:          <Button Height="23" Margin="94,0,109,24" loc:LocalizationProvider.ContentID="Spanish" Name="button3" VerticalAlignment="Bottom" Click="button3_Click"></Button>
  10:          <TextBox Height="23" Margin="55,14,59,0" loc:LocalizationProvider.TextID="HowAreYou" Name="textBox1" VerticalAlignment="Top" />
  11:      </Grid>
  12:  </Window>

In this example, you can see the attached properties defined by the LocalizationProvider class.  The IDs specified in the ContentID and TextID properties are actually defined within my .resx files.  The neat thing about this approach is that it includes full designer support, in both Visual Studio 2008 and Blend 2.5:


The application is able to switch at runtime by changing the Thread.CurrentThread.CurrentCulture property and then calling LocalizationProvider.UpdateAllControls().  Here's part of the class, and then I'll discuss it:

   1:      public static class LocalizationProvider 
   2:      {
   3:          private static List<DependencyObject> _contentObjects;
   5:          static LocalizationProvider()
   6:          {
   7:              _contentObjects = new List<DependencyObject>();
   8:          }
  10:          public static void UpdateAllObjects()
  11:          {
  12:              foreach (DependencyObject obj in _contentObjects)
  13:              {
  14:                  ResourceManager res = Resources.ResourceManager;
  15:                  ContentControl cctrl = obj as ContentControl;
  16:                  if (cctrl == null)
  17:                      throw new InvalidCastException(string.Format("Type '{0}' does not derive from type 'ContentControl'.", obj.GetType().FullName));
  19:                  string key = obj.GetValue(ContentIDProperty) as string;
  21:                  string resourceValue = res.GetString(key, CultureInfo.CurrentCulture);
  23:                  cctrl.SetValue(ContentControl.ContentProperty, resourceValue);
  24:              }
  25:          }
  27:          #region ContentID property
  28:          public static string GetContentID(DependencyObject obj)
  29:          {
  30:              return (string)obj.GetValue(ContentIDProperty);
  31:          }
  33:          public static void SetContentID(DependencyObject obj, string value)
  34:          {
  35:              obj.SetValue(ContentIDProperty, value);
  36:          }
  38:          private static void OnContentIDChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
  39:          {
  40:              if (obj == null)
  41:                  throw new ArgumentNullException("obj");
  43:              ResourceManager res = Resources.ResourceManager;
  44:              ContentControl cctrl = obj as ContentControl;
  45:              if (cctrl == null)
  46:                  throw new InvalidCastException(string.Format("Type '{0}' does not derive from type 'ContentControl'.", obj.GetType().FullName));
  49:              string resourceValue = null;
  50:              try
  51:              {
  52:                  resourceValue = res.GetString(e.NewValue as string, CultureInfo.CurrentCulture);
  53:              }
  54:              catch (Exception ex) { }
  56:              if (resourceValue != null)
  57:              {
  58:                  cctrl.SetValue(ContentControl.ContentProperty, resourceValue);
  59:                  if (!_contentObjects.Contains(obj))
  60:                      _contentObjects.Add(obj);
  61:              }
  63:          }
  65:          public static DependencyProperty ContentIDProperty =
  66:              DependencyProperty.RegisterAttached("ContentID", typeof(string), typeof(LocalizationProvider),
  67:              new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsArrange, new PropertyChangedCallback(OnContentIDChanged)));
  68:          #endregion
  69:      }

This part of the class tracks objects that use the ContentID attached property; this property is set on controls that use the Content property (any controls that inherit from ContentControl).  I'll explain why this is important in a little while.

First, we declare the ContentIDProperty as a DependencyProperty.  We tell the framework that it affects the arrangement of controls to which it is attached, and that it should call the method OnContentIDChanged when it is changed for a control.  That's down at the bottom.  We also then provide the methods for GetContentID and SetContentID - these are like extension methods for the specific dependency property we're creating.  (For more information about how a dependency property is created, see the related MSDN article).  The meat and potatoes of the class is in the OnContentIDChanged method - it looks at the new value, searches for a resource with the given ID, and then sets the control's Content dependency property to the value of the resource.  (Note: the current implementation also only works with strings.  However, changing the type to Object and then pulling the value out would work for other types of resources as well).  It then saves the DependencyObject to a list of controls that have been localized.

The UpdateAllControls iterates over each localized control and rebinds its value based on its type, using much the same logic as the initial method.


There are a few difficulties introduced by using this method:

  • If you set the Content property of a control (by specifying internal text or by using the attribute markup syntax), you run the risk of overriding the attached property that we defined here.  For example, this markup:

    <Button Height="45" Margin="55,82,75,0" loc:LocalizationProvider.ContentID="HelloWorld" Name="button1" VerticalAlignment="Top">Hey there, world!</Button>

    would override the localized text.  This is because of the order the XAML parser loads and binds dependency properties -- remember that the Content property is also a dependency property.  I've tried a lot of different techniques for fixing this, but thus far can't find any.

  • It requires the xmlns: namespace declaration in each XAML file in which it's used.
  • Different controls and different dependency properties require different localization ID properties.  For example, TextBlocks and Runs, which have a Text property but not a Content property, need to use a different localization handler.  That involves changing the UpdateAllControls method to account for new dependency property callback types as well.
  • For very large applications, updating all controls may take longer.
  • The Text property, which is not defined on a common base class, uses reflection to set the dependency property.  The way that the set method is invoked is slower than calling the method or property setter directly.
  • Because ResX files are bound per-assembly, it can be difficult to centralize the LocalizationProvider class.  However, if it is included as an internal class in each assembly in which it is used, it can be worked around.  Another option is to use base classes, although since it's a static class, this could create a more difficult implementation.


For all of the potential difficulties, we like this approach better for a number of reasons:

  • It provides designer support.  You can even set the dependency properties in Blend.
  • It has richer tool support and an established file format.
    • Existing localization investments don't go away.
  • It doesn't rely on binding syntax.
    • Binding syntax is fine, and is described in the CodeProject article linked above as a convenient way to use ResX files.  However, they require editing in XAML directly; this does not.
  • It allows the runtime, dynamic modification of the UI culture.

I'll be posting a more complete sample project soon, so if this interests you, stay tuned!