Bookmark and Share Share...    Subscribe to this feed Feed   About Christian Moser  


Localization of a WPF application using a custom MarkupExtension

The Idea

A simple and effective way to localize application resources is to write a custom MarkupExtension that provides a localized value. The extension takes a parameter in the constructor that is the unique resource key. When the DepdendencyProperty asks for the value, the markup extension looks up the value from a generic resource provider. This gives you the flexibility to reuse the resource management and translation process that is already established within your company.

Using a custom markup extension has some advantages

  • Lightweight and flexible solution
  • Dynamic language switch at runtime
  • Works with any kind of resource providers.

Other Implementations

The idea of using a markup extension for localization is not unique by me. There are also other similar implementations out there. Here you find some links:

How to use it

The usage of the markup extension is very simple. Just replace the string you want to localize by {l:Translate resourceKey}.

 
 <TextBlock Text="{l:Translate CustomerForm.FirstName}" />
 
 

Implementation details of the translation infrastructure

The translation infrastructure consists of the folling parts

  • Translation manager
    The translation manager is a static class that manages the current language and notifies all markup extensions, to update their values when the language changes. It also provides access to translated resources. The resources itself are provided by a generic translation provider.

  • Translate markup extension
    The tanslate markup extension knows the resource key and provides the translated value. It listens to the LanguageChanged event of the translation manager and updates its value. This event handler is implemented by the weak event pattern to prevent memory leaks.

  • Translation provider
    The translation provider is a class that provides the translated resources. It has to implement the ITranslationProvider and can access any kind of resources you like. For e.g. ResX, XML or text files.

 
public class TranslateExtension : MarkupExtension
{
    private string _key;
 
    public TranslateExtension(string key)
    {
        _key = key;
    }
 
    [ConstructorArgument("key")]
    public string Key
    {
        get { return _key; }
        set { _key = value;}
    }
 
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var binding = new Binding("Value")
              {
                  Source = new TranslationData(_key)
              };
        return binding.ProvideValue(serviceProvider);
    }
}
 
 
 
public class TranslationData : IWeakEventListener, 
                  INotifyPropertyChanged, IDisposable
{
    private string _key;
 
    public TranslationData( string key)
    {
        _key = key;
        LanguageChangedEventManager.AddListener(
                  TranslationManager.Instance, this);
    }
 
    ~TranslationData()
    {
        Dispose(false); 
    }
 
 
    public void Dispose() 
    { 
        Dispose(true); 
        GC.SuppressFinalize(this); 
    } 
 
    protected virtual void Dispose(bool disposing) 
    { 
        if (disposing) 
        { 
          LanguageChangedEventManager.RemoveListener(
                    TranslationManager.Instance, this); 
        } 
    } 
 
 
    public object Value
    {
        get
        {
            return TranslationManager.Instance.Translate(_key);
        }
    }
 
    public bool ReceiveWeakEvent(Type managerType, 
                            object sender, EventArgs e)
    {
        if (managerType == typeof(LanguageChangedEventManager))
        {
            OnLanguageChanged(sender, e);
            return true;
        }
        return false;
    }
 
    private void OnLanguageChanged(object sender, EventArgs e)
    {
        if( PropertyChanged != null )
        {
            PropertyChanged( this, new PropertyChangedEventArgs("Value"));
        }
    }
 
    public event PropertyChangedEventHandler PropertyChanged;
}
 
 
 
public class TranslationManager
{
    private static TranslationManager _translationManager;
 
    public event EventHandler LanguageChanged;
 
    public CultureInfo CurrentLanguage
    {
        get { return Thread.CurrentThread.CurrentUICulture; }
        set
        {
           if( value != Thread.CurrentThread.CurrentUICulture)
           {
               Thread.CurrentThread.CurrentUICulture = value;
               OnLanguageChanged();
           }
        }
    }
 
   public IEnumerable<CultureInfo> Languages
   {
       get
       {
           if( TranslationProvider != null)
           {
               return TranslationProvider.Languages;
           }
           return Enumerable.Empty<CultureInfo>();
       }
    }
 
    public static TranslationManager Instance
    {
        get
        {
            if (_translationManager == null)
                _translationManager = new TranslationManager();
            return _translationManager;
        }
    }
 
    public ITranslationProvider TranslationProvider { get; set; }
 
   private void OnLanguageChanged()
   {
        if (LanguageChanged != null)
        {
            LanguageChanged(this, EventArgs.Empty);
       }
    }
 
   public object Translate(string key)
   {
       if( TranslationProvider!= null)
       {
            object translatedValue =TranslationProvider.Translate(key);
            if( translatedValue != null)
           {
               return translatedValue;
           }
        }
       return string.Format("!{0}!", key);
    }
}
 
 




Last modified: 2010-05-25 22:11:25
Copyright (c) by Christian Moser, 2011.

 Comments on this article

Show all comments
Patrick
Commented on 9.January 2009
Hi! I just compiled this on C# 2008, and altough the extensions works great at runtime, I\\\'m unable to see the XAML design window (The type \\\'Translate\\\' was not found.)
Did I do something wrong? Or is the a site affects, because the designer hasn\\\'t prepared for XAML extensions yet?

Thanks in advance.
Christian Moser
Commented on 20.January 2009
Hi Patrick, did you add the XmlnsDefinition attribute to the assembly that contains the Translation class? Otherwhise you need to include the namespace and use the prefix.
I think another problem could be if the extension and the xaml file you use it is in the same assembly, then you have to use a clr-namespace and a prefix. I hope this helps.
Greetings
Christian
Sevenate
Commented on 28.February 2009
Excellent solution! Thank you, Christian.
Andres Olivares
Commented on 6.March 2009
Hello Christian,

I have similar implementation for a localization implementation I was working on. I was wondering if you ran into the same issue I experienced. If the Translator_CultureChanged handler is raised and it is executed for a localization set on the setter of a style for an element, the execution of

targetObject.SetValue(targetProperty, Resources.ResourceManager.GetObject(key));

would fail because your targetObject never registered as a DependencyObject. Try this if you have enncountered this yet.
Hi Christian
Commented on 20.March 2009
Would it be possible for you to post a download of the above VS2008 WPF Project please? I tried out what you have explained, but I seem to be going wrong somewhere, and I would be obliged if you could provid the Project, so that I can compare my files and see where I have gone wrong.

Thanks in advance,

Manoj Kumar Sharma
vvidov
Commented on 23.March 2009
Hi
how I can use it with Microsoft.Windows.Controls.Ribbon
10x
vvidov
vvidov
Commented on 24.March 2009
Hi
how I can use it with Microsoft.Windows.Controls.Ribbon
10x
vvidov
Christian Moser
Commented on 15.April 2009
Hi, I uploaded a sample solution that contains an updated version of the markup extension.
Simon
Commented on 12.May 2009
How would you make this work for a ContextMenu?
Christian Moser
Commented on 12.May 2009
Hi Simon,
you can use it in any WPF elements - also for ContextMenus. Just get the Header of a MenuItem from the TranslateExtension.
John
Commented on 12.May 2009
I cannot download your sample project. Using IE7.
Simon
Commented on 14.May 2009
The problem with e.g. context menu is that datacontext is not flowed or something like that, and thus I have been unable to get it to work.
Greg
Commented on 14.May 2009
Exactly what I was looking for. End to locbaml woes.
The sample seems to be wired directly into Localize. Not what you have in the article above, making it much more developer friendly.
Great job, Christian. Thanks
Boris
Commented on 20.May 2009
Correct me if I'm wrong but this will give you a nice memeory leak...
Christian Moser
Commented on 20.May 2009
Hi Boris,

you are right. You talk about the event registration in the markup extension? I have to find a solution for this. Probably a weak event can solve the issue.
Chris Sullivan
Commented on 27.July 2009
Good solution, one of the better one's I've seen mainly for it's simplicity. Also like the fact the "resource" is held as a seperate project which makes it a more realistic solution. Thanks for taking the time and effort to create the solution and share with others.
SeriousM
Commented on 17.August 2009
Hello!
This is a nice approach that i have developed some months ago (4. May 08).

Have a look at this please: http://wpflocalizeextension.codeplex.com
Stephen
Commented on 14.September 2009
Hi Christian, I have a question. I have downloaded the example app. When I ran it the Hebrew text was squares, but when viewing the resx I see the Hebrew text. This seed strange. Cah you clue me in on what might be going on here? Thanks. Nice site by the way. Thanks for that as well.
Alex
Commented on 16.September 2009
Is it only me or doing the simplest thing with wpf suposses to turn the down the whole world !? Those things were simpler before !!
Vytas
Commented on 21.October 2009
What is "Localize" in 'Title="{Localize ...'
That's the only thing I can not understand and I can't apply this example to my application. Besides, the article is Totaly different from the example, but I think you know that...
Raul
Commented on 13.November 2009
Hi Christian,
I downloaded your project and it works fine, but when i added another culture SupportedCultures it doesn't work. I have also made the respective resource files in 'LocalizationResources'. Any idea what I might be doing wrong?
MGA
Commented on 23.November 2009
This is the best sample for localization! Thanks so much!
Spock
Commented on 24.November 2009
I cannot figure out how the localize markup extension is being called by
What is "Localize" in 'Title="{Localize ...'
It works in the example, but not being able to call the extension in my app is frustrating. Can Christian or someone else clarify, please? Gred, how is the sample wired into Localize? Thanks and live long and prosper
Prabu P
Commented on 14.December 2009
Hello Christian

Excellent solution for WPF localization using Resources. Keep posting such nice articles and happy coding :)
Prabu P
Commented on 14.December 2009
Hi Spock,

"What is "Localize" in 'Title="{Localize ...' "

"Localize" represents "LocalizeExtension" class name. You may or maynot use "Extension" keyword in Markup representation.
Gil
Commented on 18.February 2010
Nice article Christian.
This method is very good for Xaml, but is there still a way to use strongly-typed resources FROM CODE e.g.:
myTitle = LocalizationResources.ExampleResources.ApplicationTitle;
?
linsong
Commented on 16.March 2010
Hi Christian,

Thanks for the article. However, there is an error on the dtor of TranslationData when attempting to remove the listener from the source when the line "var manager = WeakEventManager.GetCurrentManager(managerType);" is executed. To fix the problem, I suggest adding a GC.Suppress to the TranslationData ctor as the following:

// fix the error that "Handle is not initialised" when
// calling LanguageChangedEventManager.CurrentManager
GC.SuppressFinalize(this);
Gnought
Commented on 9.April 2010
@linsong: Yes, you're right. The code will generate a "Handler is not initialized" error when the window is closed.

This error is generated from the finalizer in the TranslationData class. The faulty line is as follows:

LanguageChangedEventManager.RemoveListener(
TranslationManager.Instance, this);


The finalizer called by GC collects unmanaged resources, but I think that the above code is an unmanaged resource, and it should be called under Dispose() method.

I rewrite it with a Finalizer-Dispose pattern as follows to solve perfectly:

/// <summary>
/// Releases unmanaged resources and performs other cleanup operations before the
/// <see cref="TranslationData"/> is reclaimed by garbage collection.
/// </summary>
~TranslationData()
{
Dispose(false);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// Code to dispose the managed resources of the class
LanguageChangedEventManager.RemoveListener(TranslationManager.Instance, this);
}
// Code to dispose the un-managed resources of the class
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

Gnought
Commented on 9.April 2010
P.S.: the TranslationData class should implement IDisposable interface
Christian Moser
Commented on 9.April 2010
@Gnought: Thank you for the solution. I added it to the code sample
@SeriousM: I added a link to your codeplex site.
SeriousM
Commented on 14.April 2010
Hello Christian,
thank you for adding my project link to your article!
just one thing: my name is "Bernhard Millauer" :)

thank you!
Juliana
Commented on 21.May 2010
I created a translator project and have generated a dll.
I created another one and am trying to use the translation.
Could tell me if I have to do some configuration, so the project recognizes the project resource translator?
Juliana
Commented on 21.May 2010
I created a translator project and have generated a dll.
I created another one and am trying to use the translation.
Could tell me if I have to do some configuration, so the project recognizes the project resource translator?
Sebastian
Commented on 20.August 2010
@Gnought: Your solution is working, because the "removeListener" isn't executed, isn't it?

in your solution it will be called, if dispose is called with "disposing=true"

if (disposing)
{
// Code to dispose the managed resources of the class
LanguageChangedEventManager.RemoveListener(TranslationManager.Instance, this);
}

but the finalizer calls the dispose with "false".

another question:

shouldn't the "removeListener" function be called, after closing and destroying a form that uses the specific MarkupExtension. If you create the same Window 3 times. The Listener will be added 3 times in a row. And will only be removed after closing the application. It this intended?
Bo Skjoett
Commented on 9.October 2010
Is it possible to have design time support? I would like to see the default text (en-US locale) during design time in Visual Studio instead of the "!TEXT_KEY!" syntax.
Bo Skjoett
Commented on 9.October 2010
Is it possible to have design time support? I would like to see the default text (en-US locale) during design time in Visual Studio instead of the "!TEXT_KEY!" syntax.
Gnought
Commented on 18.October 2010
@Sebastian, did you try?
Closing and destroying a form will call Dispose() method which calls Disposing(true) so the listener will still listen 1 event.
Steven
Commented on 8.November 2010
Why is this better than simply using something like
xmlns:resx="clr-namespace:MyApp.Properties"
Text="{x:Static resx:Resources.Title}
Doc
Commented on 23.December 2010
Great article, thanks for that!

But I still wonder, is there an easy way to create this {Translator Resource Binding} dynamically in the code?
Steve G.
Commented on 9.January 2011
Awesome article! I was struggling to get a dynamic way to switch languages and did not want to deal with LocBaml. Your classes work great.
Griz
Commented on 26.January 2011
Wonderful article.

FYI, From code I was able to use
TextBox.Text = TranslationManager.Instance.Translate(tranlationKey) as string;
Griz
Commented on 26.January 2011
Continuing... to get the value in the current culture.

And to bind from code, this worked

var binding = new Binding("Value")
{
Source = new TranslationData(tranlationKey)
};

BindingOperations.SetBinding(TextBox, TextBox.TextProperty, binding);


which both methods were taken from the Translate classes in the sample.
Spook
Commented on 11.February 2011
Can you tell more about the memory management in this example? Why do you use WeakEventListener instead of simple delegation? And why one shall implement both Finalize and Dispose methods? I am not familiar with such resource allocation and collection issues :(
Flo
Commented on 11.February 2011
Do I have to do any aditional linking to get the .resource files or the .dll they are in linked to the assembly? My app continiosly tells me it can't find <Namespace>.Resource.resources but the files are created in the bin directory and then correctly packed into a dll which is found in the debug directory.

lg flo
Wom
Commented on 24.February 2011
Is there a reason why in your sample project the languages are displayed in the combobox as 'German' and 'Dutch' while in my own project it just stays 'nl' and 'de'?
Klaus
Commented on 24.March 2011
Hi

Is there a way to use this extension in a multibinding?

I want to have a label that consists of a text from the markup extension followed by a text that comes from a second control (im my example: datagrid.items.count.

The resulting label should be &quot;Number of records: 123&quot; where &quot;Number of records:&quot; comes from the extension and &quot;123&quot; is the number of records in a datagrid on the same window

Regards
Christian Moser
Commented on 24.March 2011
Hi Klaus,
since the TranslationExtension returns a binding, you can use it like a binding in a multibinding. If you are using .NET 4.0 you can profit of the new bindable &lt;Run&gt; within a &lt;TextBlock&gt; to combine fragments to one string.

Greetings
Christian
Stefan
Commented on 6.April 2011
I like that very much - Small, lean straight forward. Thanks a lot for sharing - learned some new concepts on your code!
James Hurst
Commented on 26.April 2011
Nice work Christian. Can you tell me, though, why the languages in my sample-project display in the ComboBox as &quot;en&quot; and &quot;de&quot; while they display in your project as &quot;English&quot; and &quot;German&quot;? I've been digging and searching and can't figure out why! I would expect the binding (to that ComboBox) to use the ToString() method on the CultureInfo objects by default, which yields the &quot;de&quot; and &quot;en&quot; values. The solution that comes to my mind is to add a ValueConverter. But I would like to know how you got yours to automatically show the EnglishName property in that ComboBox?!
Thank you for your advice, James Hurst
Manuel Strausz
Commented on 3.May 2011
Hey James,
I'm not sure if this will help you but I did it with a ValueConverter which gets applied on each item like so in xaml notation:

&lt;ComboBox.ItemTemplate&gt;
&lt;DataTemplate&gt;
&lt;TextBlock Text=&quot;{Binding Converter={StaticResource TranslationConverter}}&quot;/&gt;
&lt;/DataTemplate&gt;
&lt;/ComboBox.ItemTemplate&gt;

Where TranslationConverter is a simple converter which will translate the values given by the combobox by using the TranslationManager directly. Could be useful for other things as well, I suppose.
Hari
Commented on 20.May 2011
A great article.

I have one question on the use of this markup extension with binding.
For ex.

We often have situtations to bind Text property to view model property

&lt;TextBlock Text=&quot;{Binding CustomerType}&quot; /&gt;

Assuming we need translation for CustomerType

&lt;TextBlock Text=&quot;{l:Translate {Binding CustomerType}}&quot; /&gt; does not work.

I could use a value converter to do this. However, I would like keep the option open as the value converter may be used for converting from a specific object to string. Also, if we use value converters, dynamic language switching will not work

jones
Commented on 14.June 2011
I observed that if you choose the .Net Framework 4 instead of 3.5, as in the example code, the combobox for the language selection shows only the short names of the culture (de, en) instead of the complete name (Deutsch, English).

Can you please show, how to display the full name when using .net Framework 4?
ا تئنت
Commented on 20.July 2011
&Oslash;&ordf;&Oslash;&sect;&Ugrave;&Oslash;&sect;&Oslash;&ordf;&Ugrave;&Oslash;&sect;&Oslash;&ordf;&Ugrave;
apo
Commented on 12.September 2011
Hi Christian,

great job! is your code restricted to any license?

apo
Commented on 12.September 2011
Hi Christian,

great job! is your code restricted to any license?

Great article.
Commented on 17.September 2011
How about if i want to use the translation manager in different assemblies (which consists of different resource files?

Example:

Assembly.exe (resource.de.resx:MyString=&quot;Foo A&quot;)
AssemblyA.dll (resource.de.resx:MyString=&quot;Foo B&quot;)
AssemblyB.dll (resource.de.resx:MyString=&quot;Foo C&quot;)
Beck
Commented on 20.September 2011
Great post!

Do you know of a way to show a default value on the designer window, currently it is littered with the key values?

Thanks in advance

Name
E-Mail (optional)
Comment