Archive for the ‘Localization’ Category.

How to change language at run-time in WPF with loadable Resource Dictionaries and DynamicResource Binding

Update: I created a framework for this here: http://globalizer.codeplex.com/

Dynamically changing the language of a WPF application at run-time or on-the-fly is possible and quite simple.

With this solution you do not have Resources.resx files, you do not have to have x:Uid on all your XAML tags. See the expectations this solution is following here: My WPF Localization and Language Expectations. Most of my expectations are met with this solution.

Here is the sample project code: WpfRuntimeLocalizationExample.zip

The key to making this so easy and simplistic is to combining these features of WPF.

  1. ResourceDictionaries – to host the strings.
  2. DynamicResource Binding – To bind the strings in XAML where needed.
    Note: If you are not going to change localization dynamically at runtime, feel free to use StaticResource.
  3. ResourceDictionary.Source() – A function to load XAML from Content files at run-time.
  4. MergedDictionaries – Where we can store/replace a localization specific ResourceDictionary.
  5. EventHandler – To allow string in code (such as in the ViewModel) to change dynamically.
  6. INotifyPropertChanged – Used with the event handler so properties in code can notify the UI.

This is going to be very similar to a previous post where I discuss changing Styles in a similar fashion.

The idea for this came by merging the knowledge from these two posts with my localization knowledge.

Example 1 – Dynamic Localization in WPF with Code-behind

Step 1 – Create a new WPF Application project in Visual Studio

  1. In Visual Studio, go to File | New | Project.
  2. Select WPF Application.
  3. Provide a name for the project and make sure the path is correct.
  4. Click OK.

Step 2 – Configure App.xaml.cs to support dynamic ResourceDictionary loading

The App.xaml.cs is empty by default. We are going to add a few variables, a constructor, and add a few simple functions. This is a fairly small amount of code.

  1. Open App.xaml.cs.
  2. Add a static member variable for App called Instance, so it can be accesses from anywhere.
  3. Add a static member variable for App called Directory, so it can be accesses from anywhere.
  4. Add a LanguageChangedEvent.
  5. Add a private GetLocXAMLFilePath(string inFiveCharLang) method.
  6. Add a public SwitchLanguage(string inFiveCharLanguage) method.
  7. Add a private SetLanguageResourceDictionary(string inFile) method.
  8. Add code to the constructor to initialize these variables and to set the default language.Note: The code is well commented.
    using System;
    using System.Globalization;
    using System.IO;
    using System.Threading;
    using System.Windows;
    
    namespace WpfRuntimeLocalizationExample
    {
        /// <summary>
        /// Interaction logic for App.xaml
        /// </summary>
        public partial class App : Application
        {
            #region Member variables
            public static App Instance;
            public static String Directory;
            public event EventHandler LanguageChangedEvent;
            #endregion
    
            #region Constructor
            public App()
            {
                // Initialize static variables
                Instance = this;
                Directory = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
    
                // Load the Localization Resource Dictionary based on OS language
                SetLanguageResourceDictionary(GetLocXAMLFilePath(CultureInfo.CurrentCulture.Name));
            }
    
            #endregion
    
            #region Functions
            /// <summary>
            /// Dynamically load a Localization ResourceDictionary from a file
            /// </summary>
            public void SwitchLanguage(string inFiveCharLang)
            {
                if (CultureInfo.CurrentCulture.Name.Equals(inFiveCharLang))
                    return;
    
                var ci = new CultureInfo(inFiveCharLang);
                Thread.CurrentThread.CurrentCulture = ci;
                Thread.CurrentThread.CurrentUICulture = ci;
    
                SetLanguageResourceDictionary(GetLocXAMLFilePath(inFiveCharLang));
                if (null != LanguageChangedEvent)
                {
                    LanguageChangedEvent(this, new EventArgs());
                }
            }
    
            /// <summary>
            /// Returns the path to the ResourceDictionary file based on the language character string.
            /// </summary>
            /// <param name="inFiveCharLang"></param>
            /// <returns></returns>
            private string GetLocXAMLFilePath(string inFiveCharLang)
            {
                string locXamlFile = "LocalizationDictionary." + inFiveCharLang + ".xaml";
                return Path.Combine(Directory, inFiveCharLang, locXamlFile);
            }
    
            /// <summary>
            /// Sets or replaces the ResourceDictionary by dynamically loading
            /// a Localization ResourceDictionary from the file path passed in.
            /// </summary>
            /// <param name="inFile"></param>
            private void SetLanguageResourceDictionary(String inFile)
            {
                if (File.Exists(inFile))
                {
                    // Read in ResourceDictionary File
                    var languageDictionary = new ResourceDictionary();
                    languageDictionary.Source = new Uri(inFile);
    
                    // Remove any previous Localization dictionaries loaded
                    int langDictId = -1;
                    for (int i = 0; i < Resources.MergedDictionaries.Count; i++)
                    {
                        var md = Resources.MergedDictionaries[i];
                        // Make sure your Localization ResourceDictionarys have the ResourceDictionaryName
                        // key and that it is set to a value starting with "Loc-".
                        if (md.Contains("ResourceDictionaryName"))
                        {
                            if (md["ResourceDictionaryName"].ToString().StartsWith("Loc-"))
                            {
                                langDictId = i;
                                break;
                            }
                        }
                    }
                    if (langDictId == -1)
                    {
                        // Add in newly loaded Resource Dictionary
                        Resources.MergedDictionaries.Add(languageDictionary);
                    }
                    else
                    {
                        // Replace the current langage dictionary with the new one
                        Resources.MergedDictionaries[langDictId] = languageDictionary;
                    }
                }
            }
            #endregion
        }
    }
    

Step 3 – Create a basic WPF User Interface

We need a little sample project to demonstrate the localization, so lets quickly make one.

    1. Create a basic interface.
      Note 1: You can make one yourself, or you can use the basic interface I used. Just copy and paste from below.
      Note 2: I have already added a menu for selecting the Language for you.
<Window x:Class="WpfRuntimeLocalizationExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="WPF Run-time Localization Example"
        MinHeight="350" MinWidth="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="30" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="25" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="15" />
            <ColumnDefinition Width="*" MinWidth="100"/>
            <ColumnDefinition Width="*" MinWidth="200"/>
            <ColumnDefinition Width="15" />
        </Grid.ColumnDefinitions>
        <DockPanel Grid.ColumnSpan="4" >
            <Menu DockPanel.Dock="Top" >
                <MenuItem Header="_File">
                    <MenuItem Header="E_xit" Click="MenuItem_Exit_Click" />
                </MenuItem>
                <MenuItem Name="menuItemLanguages" Header="_Languages">
                    <MenuItem Tag="en-US" Header="_English" Click="MenuItem_Style_Click" />
                    <MenuItem Tag="es-ES" Header="_Spanish" Click="MenuItem_Style_Click" />
                    <MenuItem Tag="he-IL" Header="_Hebrew" Click="MenuItem_Style_Click" />
                </MenuItem>
            </Menu>
        </DockPanel>
        <Label Content="First Name" Name="labelFirstName" Grid.Row="2" FlowDirection="RightToLeft" Grid.Column="1" />
        <Label Content="Last Name" Name="labelLastName" Grid.Row="4" FlowDirection="RightToLeft" Grid.Column="1" />
        <Label Content="Age" Name="labelAge" Grid.Row="3" FlowDirection="RightToLeft" Grid.Column="1" />
        <TextBox Name="textBox1" Grid.Column="2" Grid.Row="2" />
        <TextBox Name="textBox2" Grid.Column="2" Grid.Row="3" />
        <TextBox Name="textBox3" Grid.Column="2" Grid.Row="4" />
        <Button Content="Clear" Grid.Column="2" Grid.Row="5" Height="23" HorizontalAlignment="Right" Name="button1" VerticalAlignment="Top" Width="75" Click="button1_Click" />
    </Grid>
</Window>
  1. Also populate the MainWindow.xaml.cs file with the code-behind needed for the menu click events.
    using System;
    using System.Globalization;
    using System.Windows;
    using System.Windows.Controls;
    
    namespace WpfRuntimeLocalizationExample
    {
        /// <summary>
        /// Interaction logic for MainWindow.xaml
        /// </summary>
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
                foreach (MenuItem item in menuItemLanguages.Items)
                {
                    if (item.Tag.ToString().Equals(CultureInfo.CurrentUICulture.Name))
                        item.IsChecked = true;
                }
            }
    
            private void MenuItem_Exit_Click(object sender, RoutedEventArgs e)
            {
                Environment.Exit(0);
            }
    
            private void MenuItem_Style_Click(object sender, RoutedEventArgs e)
            {
                // Uncheck each item
                foreach (MenuItem item in menuItemLanguages.Items)
                {
                    item.IsChecked = false;
                }
    
                MenuItem mi = sender as MenuItem;
                mi.IsChecked = true;
                App.Instance.SwitchLanguage(mi.Tag.ToString());
            }
    
            private void button1_Click(object sender, RoutedEventArgs e)
            {
                labelFirstName.Content = labelLastName.Content = labelAge.Content = string.Empty;
            }
        }
    }
    

Though localization is not yet working, this should compile and run.

Step 4 – Create Resource Dictionaries for Localization

By recommendation of my expert localization team, we are going to create a folder for each language, using the five character string (en-US, es-ES, he-IL), and put a ResourceDictionary in each folder. The resource dictionary will also have the five character language string.

  1. Create the folder and the file.
    1. Right-click on the project in Visual Studio and choose Add | New Folder.
    2. Name the folder en-US
    3. Right-click on the en-US folder and choose Properties.
    4. Set the ‘Namespace Provider’ value to false.
    5. Right-click on the en-US folder and choose Add | Resource Dictionary.
    6. Provide a file name and make sure that Resource Dictionary (WPF) is selected.
    7. Note: I named my first resource dictionary LocalizationDictionary.en-US.xaml.
    8. Click Add.
    9. Right-click on the LocalizationDictionary.en-US.xaml file and choose Properties.
    10. Set ‘Build Action’ to ‘Content’.
    11. Set ‘Copy to Output Directory’ to be ‘Copy if newer’.
    12. Set ‘Custom Tool’ to blank.
  2. Name the ResourceDictionary.
    1. Open the resource LocalizationDictionary.en-US.xaml
    2. Add a reference to the System namespace from the mscorlib assembly.
    3. Add a string with the x:Key set to ResourecDictionaryName.
    4. Set the string value to “Loc-en-US”.
      Important! Because our code is specifically looking for Loc-, you need to use that scheme or change the code.
    5. Add to the string a Localization.Comment and set the value to $Content(DoNotLocalize).
    6. Save the resource dictionary.
      <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                          xmlns:sys="clr-namespace:System;assembly=mscorlib">
      
          <!-- The name of this ResourceDictionary. Should not be localized. -->
          <sys:String x:Key="ResourceDictionaryName" Localization.Comments="$Content(DoNotLocalize)">Loc-en-US</sys:String>
      
      </ResourceDictionary>
      
  3. Repeat all the steps in steps 1 and 2 for each langauge.
    Note: We will do the following three languages in this example:

    1. en-US
    2. es-ES
    3. he-IL

Step 4 – Localize the strings

We now are going to find each string that needs to be localized, add a string resource in our localization resource dictionary, and replace the string with DynamicResource binding.
  1. Open the MainWindow.xaml.
  2. The first string is the Title of the Window.
    Note: My window title is “WPF Run-time Localization Example”
  3. Replace the value with a DynamicResource to MainWindow_Title.
    Here is a snippet…

    <Window x:Class="WpfRuntimeLocalizationExample.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="{DynamicResource ResourceKey=MainWindow_Title}"
            MinHeight="350" MinWidth="525">
        <Grid>
    
  4. Open the LocalizationDictionary.en-US.xaml.
  5. Add a string as a resource for the MainWindow’s  Title, making sure the x:Key value is matches the ResourceKey in the MainWindow.xaml: MainWindow_Title.
        <!-- Localized strings -->
        <sys:String x:Key="MainWindow_Title">WPF Run-time Localization Example</sys:String>
    
  6. Repeat steps for each of the remaining string that needs to be localized.
    The following strings need to be localized in my sample UI:

    1.  _File
    2. E_xit
    3. _Language
    4. _English
    5. _Spanish
    6. _Hebrew
    7. First Name
    8. Last Name
    9. Age
    10. Clear
You program should now compile and switch language.

Step 5 – Configure the FlowDirection

The main motivation for adding Hebrew to this example is so that we can show how to dynamically switch FlowDirection for languages that flow from right to left.

Note: This step is optional and only necessary if your application will support a language that flows right to left.

  1. Add two FlowDirection resources to the LocalizationDictionary.en-US.xaml files.
        <!-- Localization specific styles -->
        <FlowDirection x:Key="FlowDirection_Default" Localization.Comments="$Content(DoNotLocalize)">LeftToRight</FlowDirection>
        <FlowDirection x:Key="FlowDirection_Reverse" Localization.Comments="$Content(DoNotLocalize)">RightToLeft</FlowDirection>
    
  2. For Spanish, copy these same lines to the LocalizationDictionary.es-ES.xaml file. Spanish and English have the same FlowDirection.
  3. For Hebrew, (you have probably already guessed this), add these same lines to the LocalizationDictionary.he-IL.xaml file but reverse the values.
        <!-- Localization specific styles -->
        <FlowDirection x:Key="FlowDirection_Default" Localization.Comments="$Content(DoNotLocalize)">RightToLeft</FlowDirection>
        <FlowDirection x:Key="FlowDirection_Reverse" Localization.Comments="$Content(DoNotLocalize)">LeftToRight</FlowDirection>
    
  4. Open the MainWindow.xaml file.
  5. Add a FlowDirection tag to the MainWindow.xaml file.
  6. Set the FlowDirection value to using DynamicResource binding to FlowDirection_Default.Here is the XAML snipppet.
    <Window x:Class="WpfRuntimeLocalizationExample.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="{DynamicResource MainWindow_Title}"
            MinHeight="350" MinWidth="525"
            FlowDirection="{DynamicResource FlowDirection_Default}">
        <Grid>
    

Build compile and test. Both the language and FlowDirection should now change when switching to Hebrew.

See the Framework for this here:

http://globalizer.codeplex.com/

Resources

My WPF Localization and Language Expectations

WPF Localization and Language switching at run-time is not exactly solved clearly by Microsoft.  After my experience (which includes researching for days on separate occasions and changing to localize using Resources.resx when localizing with BAML was not working for everyone involved) I had come to the conclusion that WPF Localization was just going to remain a black mark on WPF until Microsoft provided an easy recommended solution.

Microsoft has a lot of information on localization.  They have pages and pages of it and a lot of talks about localization using Resources.resx or using BAML (Binary XAML).

I came to realize that I had the following expectations:

  1. Development for Localization should be easy.
  2. Development for Localization should not require “extra” libraries or separate projects.
  3. Localized strings should come from the same place for XAML as for C# code.
  4. Language switching should be a run-time option.
  5. Localization changes should not require a rebuild or recompile.
  6. Localization method should allow for us of a localization tool (such as Alchemy Catalyst).
  7. Localization should not require every item has an a unique identifier (as in no x:Uid tags in WPF).
  8. Missing strings in any language should be easily identified.
  9. FlowDirection should change with language when appropriate (such as for Hebrew).

Neither the Resources.resx or the XAML solutions seemed to meet my expectations.

Time came to decide on a localization method for WPF again, and I again embarked into similar research as before. I found all the same answers.

However, I am now a WPF Expert. A Senior WPF Developer if you can call anyone such for such a young technology.

I went through all the same pages as before only this time, I pieced together a localization solution that comes the closest to meeting all my expectations.

If you don’t agree with most of my expectations, feel free to use another WPF localization solution. I am not going to argue that mine is the best for everyone, just the best for me and my expectations.

In all my searching I have found no one who is using my solution. Expect my new solution soon and link at the end of this post soon.

How to change language at run-time in WPF with loadable Resource Dictionaries and DynamicResource Binding (Example 1)

 

 

WPF and Localization in the XAML with String.Format()

I had a localization issue today.

I was trying to get our product running in Spanish to show decimals as a comma (,) instead of a period (.).

Here is the XAML snippet I have.

        <StackPanel Orientation="Horizontal" Margin="0,0,0,5" >
            <Image Source="{Binding Image}" Width="24" Height="24" />
            <Label>
                <TextBlock>
                  <TextBlock.Text>
                         <MultiBinding StringFormat="{}{0} ({1}:) - {2} {3:F} GB, {4} {5:F} GB, {6} {7:F} GB">
                            <Binding Path="Drive.VolumeLabel" />
                            <Binding Path="Drive.DriveLetter" />
                            <Binding Source="{x:Static p:Resources.Required}" />
                            <Binding Path="Drive.TotalRequiredSpace.Gigabyte" />
                            <Binding Source="{x:Static p:Resources.Recommended}" />
                            <Binding Path="Drive.TotalRecommendedSpace.Gigabyte" />
                            <Binding Source="{x:Static p:Resources.Available}" />
                            <Binding Path="Drive.AvailableSpace.Gigabyte" />
                         </MultiBinding>
                  </TextBlock.Text>
                </TextBlock>
            </Label>
            <Image Source="{Binding DriveSpaceStatusImage}" Width="24" Height="24" Margin="15,0" />
        </StackPanel>

The Drive.TotalRequiredSpace object is a DataMeasurement object (which I have posted code for in a previous post).

The Gigabyte parameter is a Double.  These Double’s displayed the decimal separator using a period (.) character even though I was testing on a Spanish version of Windows Server 2008 R2.

I located the answer on another blog.
WPF Data Binding Cheat Sheet Update – The Internationalization Fix

He was using Date’s which like Double’s require decimals, and was seeing the same issue.  The solution is the same though.

Add the following code somewhere before your first WPF window opens.

System.Windows.FrameworkElement.LanguageProperty.OverrideMetadata
(
    typeof(System.Windows.FrameworkElement),
    new System.Windows.FrameworkPropertyMetadata(
        System.Windows.Markup.XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag)
    )
);

I am pretty sure this is a poor oversight and a bug in my opinion. Even though there is a one line fix, it was a costly fix, as I had to spend time troubleshooting and researching to find this one line solution which should be the default behavior.