Download code ver 1.3 from here.
Many of my customers asked me:
How WPF supports localization and globalization?
Is there any built-in mechanism for choosing a language on-the-fly, at runtime and without closing any window?
How do I translate formatted text with parameters?
Well..., you know the story,... aren't you!? No? So start by reading this, and this. Still confused? continue with this and also this.
As you can see, there is more than one solution. Each has its pros and cons. The WPF official language support provides a complex localization set of API's, and a bizarre SDK tool for translation and creation of localized assemblies. Others provide unofficial great ideas for localizing WPF based on .NET 2.0 resource files, WPF resource dictionary and XML.
I decided to share another mechanism for localizing WPF applications.
Download the code from here.
Why did I bother to invent another solution, and why would you want to use my solution
- It provides an option for replacing languages at runtime, on-the-fly
- It performs better than a lame XML, XPath based binding solution
- It can be used via Styles, Control Templates and Data Templates
- It translates a formatted text with parameters, using default and custom formatters
- It provides a Translate custom markup-extension to write an elegant XAML
I will start by discussing the markup snippet bellow:
<Window x:Class="Tomers.WPF.Localization.MainWindow" x:Name="Root"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:loc="http://schemas.tomer.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Tomers.WPF.Localization"
xmlns:d="http://schemas.microsoft.com/expression/blend/2006"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{StaticResource WindowBrush}"
loc:Translate.Uid="0"
Title="{loc:Translate}"
Width="{loc:Translate}"
Height="{loc:Translate}"
FlowDirection="{loc:Translate}">
<Window.Resources>
<DataTemplate DataType="{x:Type local:Data}">
<TextBlock Margin="8" HorizontalAlignment="Left">
<TextBlock.Text>
<loc:Translate>
<Binding Path="Uid" />
<Binding Path="ID" />
</loc:Translate>
</TextBlock.Text>
<TextBlock.Foreground>
<loc:Translate>
<Binding Path="Uid" />
</loc:Translate>
</TextBlock.Foreground>
</TextBlock>
</DataTemplate>
</Window.Resources>
<StackPanel x:Name="_panel">
<Label loc:Translate.Uid="1"
Content="{loc:Translate}"
FontSize="14" />
<TextBlock loc:Translate.Uid="2" FontSize="16">
<TextBlock.Text>
<loc:Translate>
<Binding ElementName="Root" Path="Width" />
<Binding ElementName="Root" Path="Height" />
</loc:Translate>
</TextBlock.Text>
</TextBlock>
<TextBlock loc:Translate.Uid="3"
Text="{loc:Translate}"
Background="{loc:Translate}"
Width="{loc:Translate}"
Height="{loc:Translate}" FontSize="18" />
<TextBlock FontSize="18">
<TextBlock.Text>
<loc:Translate>
<Binding ElementName="Root" Path="Uid" />
</loc:Translate>
</TextBlock.Text>
</TextBlock>
<ContentControl x:Name="_elementI" FontSize="20" />
<ContentControl x:Name="_elementII" FontSize="20" />
<Button loc:Translate.Uid="7"
Margin="8"
HorizontalAlignment="Right"
Content="{loc:Translate}"
Width="200"
Click="Button_Click" FontSize="16" />
</StackPanel>
</Window>


As you can see, the markup snippet produces the window above. When clicking the "Click to change language" button, all the elements attached with the "loc:Translate" custom markup-extension, are automatically have new values: In our case the language, alignment, size, position, color and other.
So why did I choose to use a custom markup extension, what's wrong with a simple Data Binding markup
Well, this is the magic of my solution. It is possible to use a complex binding expression indeed, for example:
<Label Content="{Binding Source={x:Static loc:LanguageContext.Instance}, Path=Dictionary, Mode=OneWay
Converter={StaticResource languageConverter}, ConverterParameter=2}" />
But as you can see, it's very long expression. Now try to imagine how long it will be if you pass parameters...
What's in the package
There are few types you should know before you start localizing:
- LanguageDictionary - This is an abstract class. It provides an abstraction for translating values by exposing two important methods: Load and Translate. The Load method loads the repository with data for each locale, and the Translate method translates a key-value pair into locale value from the repository. For example, I wrote a XmlLanguageDictionary class that loads and stores an XML language file inside a dictionary (you can find it in the LocalizationDemo project).
- LanguageContext - This singleton type holds the culture and the dictionary of the active language. It also plays an important role by notifying on culture change.
- LanguageConverter - This important type calls the active dictionary to translate values each time a language is replaced. The language converter is instantiated by the TranslateExtesnion during the internal-binding operation (see TranslateExtesnion type bellow). The LanguageConverter implements both IValueConverter and IMultiValueConverter interfaces.
- TranslateExtension - This is the magic of this solution. It provides the user an option to bind a localized-property to the language dictionary using a simple and short markup within XAML, as demonstrated in the markup snippet above. By extracting the target element and the dependency property, it binds the LanguageContext.Dictionary to the targets property.
How to use it
First, you have to create your own language-dictionary by deriving LanguageDictionary. You can also start by using my XmlLanguageDictionary, provided as a lame example inside the LocalizationDemo project.
Second, you should create and register a dictionary instance for each language and select the default one, before you create any localized UI element. See the markup bellow:
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
LanguageDictionary.RegisterDictionary(
CultureInfo.GetCultureInfo("en-US"),
new XmlLanguageDictionary("Languages/en-US.xml"));
LanguageDictionary.RegisterDictionary(
CultureInfo.GetCultureInfo("he-IL"),
new XmlLanguageDictionary("Languages/he-IL.xml"));
LanguageContext.Instance.Culture = CultureInfo.GetCultureInfo("en-US");
base.OnStartup(e);
}
}
Now, use the Translate markup extension to localize elements as demonstrated above.
To translate a simple element with no parameters use the following syntax (The uid value, Content and FontSize should be keys in your dictionary back-storage - see my xml files for an example):
<Label loc:Translate.Uid="1" Content="{loc:Translate}" FontSize="{loc:Translate}" />
To translate an element with parameters use the following syntax
<TextBlock loc:Translate.Uid="2" FontSize="16">
<TextBlock.Text>
<loc:Translate>
<Binding ElementName="Root" Path="Width" />
<Binding ElementName="Root" Path="Height" />
</loc:Translate>
</TextBlock.Text>
</TextBlock>
To translate an element with a dynamic Uid, use the previous syntax, where the Uid must be the first parameter (this is a great solution for styles and templates):
<TextBlock FontSize="18">
<TextBlock.Text>
<loc:Translate>
<Binding ElementName="Root" Path="Uid" />
</loc:Translate>
</TextBlock.Text>
</TextBlock>
There are several more issues that should be considered, also the code should be refactored for production. Use it on your own risk.
Download code ver 1.0 from here.
Download code ver 1.1 from here.
Update: On 13-Aug-2008
- Markup extension bug fixed
- Fully supported on both Blend and Cider designers
- Default fallback value was added
- Demo visuals improvements
Download code ver 1.2 from here.
Update: On 06-Feb-2009
- Uid's can be provided directly via Translate markup (now works with Setters - see demo)
- Demo upgrade to support new feature
Download code ver 1.3 from here.