Table of Contents

Localization Using RESX Files
WinForms Localization
WPF Localization

Deployment

Localization Sample

Source: WAF Framework (Localization sample included)

WPF Application Framework (WAF)

Introduction

The LocalizationSample demonstrates a way to localize WPF applications. It uses the “old” ResX files for this task because they are well supported by the .NET Framework and Visual Studio.

Remark: Microsoft encourages you to use their tool “LocBaml” for localizing WPF applications (see MSDN: How to Localize an Application). Unfortunately, this tool is not production-ready and you don’t get the same comfortable support from Visual Studio.

The LocalizationSample is part of the WPF Application Framework (WAF) download.

Run the sample

1. Open the WpfApplicationFramework solution. 2. Set the LocalizationSample project as StartUp project and start it in Debug mode. 3. The application shows a window with two text boxes and a button. The text shown in the application is in English or German depending on the Windows language settings. 4. The birthday is formatted as short date time format. This formatting is defined in the Windows Regional and Language Options. 5. Close the application. 6. Open the App.config file. In this file you can define the cultures that should be used in the application. You can use one of these settings:

<WpfLocalizationDemo.Properties.Settings>
  <setting name="Culture" serializeAs="String">
    <!--<value>en-US</value>-->
    <value>de-DE</value>
    <!--<value />-->
  </setting>
  <setting name="UICulture" serializeAs="String">
    <!--<value>en-US</value>-->
    <value>de-DE</value>
    <!--<value />-->
  </setting>
</WpfLocalizationDemo.Properties.Settings>

7. Run the application again and see how the changes affect its appearance.

CurrentCulture and CurrentUICulture

The difference between CurrentCulture and CurrentUICulture is not that obvious. These two properties represent the “Regional and Language Options” which can be found in the Windows Control Panel.

Code Walkthrough

  1. Open the WpfApplicationFramework solution.
  2. Expand the LocalizationSample project.
  3. Open the App.xaml.cs file.
    • In the constructor we call the InitializeCulture method.
    • In the InitializeCulture method we try to retrieve the Culture and UICulture property from the application settings file. When they exist then we set the Culture on the CurrentThread.
    • In the last line of the InitializeCulture method we set the WPF XmlLanguage with the CurrentCulture. This line is necessary that WPF uses the CultureInfo.CurrentCulture settings.
      FrameworkElement.LanguageProperty.OverrideMetadata( 
        typeof(FrameworkElement), 
        new FrameworkPropertyMetadata( 
               XmlLanguage.GetLanguage( CultureInfo.CurrentCulture.IetfLanguageTag )
            )
        );

      Remark: Windows Forms and Console applications use the CultureInfo.CurrentCulture setting by default. In my opinion, WPF should do the same so that we don’t have to write the previous line in our applications.

  4. Open Properties/Settings.settings file
    • Important: Change the “Access Modifier” from internal to public (see top bar in document within VS2010 IDE).
  5. Open the Properties/Resources.resx file
    • This file contains the string resources used in the user interface.
    • Important: Note that the “Access Modifier” was changed from internal to public. You find this setting in the Visual Studio Resources Designer toolbar. This is necessary so that you are able to access the resources within XAML.
    • Besides the Resources.resx file you find the Resources.de.resx file in the same folder. The second resource file contains the resources for the German language.
  6. Open the Presentation/ShellWindow.xaml file
    • The Title property of the Window uses the x:Static markup extension to access one of the Resources properties which are static.
      Title="{x:Static p:Resources.WpfLocalizationDemo}"
    • The Window defines the SizeToContent property with the “WidthAndHeight” value. This allows the Window to automatically define its Width and Height in respect to the content. By example the words in the German language are longer than in English. With the SizeToContent setting you allow WPF to create a larger Window when it has to show its resources in the German language so that none of the text gets cut.
    • The Grid in the MainWindow defines its ColumnDefinition.Width and RowDefinition.Height properties with the “Auto” value. This is done because of the same reason as the previous point defines the SizeToContent property. Remark: Try to avoid defining the Width and Height properties with concrete values. Let WPF calculate those values itself.

Code

App.config:

<?xml version="1.0"?>
<configuration>
  <configSections>
    <sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
      <section name="LocalizationSample.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false"/>
    </sectionGroup>
  </configSections>
 
  <userSettings>
    <LocalizationSample.Properties.Settings>
      <setting name="Culture" serializeAs="String">
        <!--Pick only one value-->
        <!--<value>en-US</value>-->
        <!--<value>de-DE</value>-->
        <value/>
      </setting>
      <setting name="UICulture" serializeAs="String">
        <!--Pick only one value-->
        <!--<value>en-US</value>-->
        <!--<value>de-DE</value>-->
        <value/>
      </setting>
    </LocalizationSample.Properties.Settings>
  </userSettings>
 
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0,Profile=Client"/>
  </startup>
</configuration>

App.xaml.cs:

using System;
using System.Globalization;
using System.Threading;
using System.Windows;
using System.Windows.Markup;
using LocalizationSample.Domain;
using LocalizationSample.Presentation;
using LocalizationSample.Properties;
 
namespace LocalizationSample
{
    public partial class App : Application
    {
        public App()
        {
            InitializeCultures();  // call here or before InitializeComponent()
        }
 
 
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);
 
            Person person = new Person() 
            { 
                Name = "Luke", 
                Birthday = new DateTime(2080, 2, 6) }
            ;
 
            ShellWindow mainWindow = new ShellWindow();
            mainWindow.DataContext = person;
            mainWindow.Show();
        }
 
        private static void InitializeCultures()
        {
            if (!string.IsNullOrEmpty(Settings.Default.Culture))
            {
                Thread.CurrentThread.CurrentCulture = new CultureInfo(Settings.Default.Culture);
            }
            if (!string.IsNullOrEmpty(Settings.Default.UICulture))
            {
                Thread.CurrentThread.CurrentUICulture = new CultureInfo(Settings.Default.UICulture);
            }
 
            FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata(
                XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag)));
        }
    }
}

English (en/default) resources. File Properties/Resources.resx:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <!-- 
    Microsoft ResX Schema 
 
    Version 2.0
 
    The primary goals of this format is to allow a simple XML format 
    that is mostly human readable. The generation and parsing of the 
    various data types are done through the TypeConverter classes 
    associated with the data types.
 
    Example:
 
    ... ado.net/XML headers & schema ...
    <resheader name="resmimetype">text/microsoft-resx</resheader>
    <resheader name="version">2.0</resheader>
    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
        <value>[base64 mime encoded serialized .NET Framework object]</value>
    </data>
    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
        <comment>This is a comment</comment>
    </data>
 
    There are any number of "resheader" rows that contain simple 
    name/value pairs.
 
    Each data row contains a name, and value. The row also contains a 
    type or mimetype. Type corresponds to a .NET class that support 
    text/value conversion through the TypeConverter architecture. 
    Classes that don't support this are serialized and stored with the 
    mimetype set.
 
    The mimetype is used for serialized objects, and tells the 
    ResXResourceReader how to depersist the object. This is currently not 
    extensible. For a given mimetype the value must be set accordingly:
 
    Note - application/x-microsoft.net.object.binary.base64 is the format 
    that the ResXResourceWriter will generate, however the reader can 
    read any of the formats listed below.
 
    mimetype: application/x-microsoft.net.object.binary.base64
    value   : The object must be serialized with 
            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
            : and then encoded with base64 encoding.
 
    mimetype: application/x-microsoft.net.object.soap.base64
    value   : The object must be serialized with 
            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
            : and then encoded with base64 encoding.
 
    mimetype: application/x-microsoft.net.object.bytearray.base64
    value   : The object must be serialized into a byte array 
            : using a System.ComponentModel.TypeConverter
            : and then encoded with base64 encoding.
    -->
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" />
              </xsd:sequence>
              <xsd:attribute name="name" use="required" type="xsd:string" />
              <xsd:attribute name="type" type="xsd:string" />
              <xsd:attribute name="mimetype" type="xsd:string" />
              <xsd:attribute ref="xml:space" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string" />
              <xsd:attribute name="name" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
              <xsd:attribute ref="xml:space" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" />
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <data name="Birthday" xml:space="preserve">
    <value>Birthday</value>
  </data>
  <data name="Close" xml:space="preserve">
    <value>Close</value>
  </data>
  <data name="Name" xml:space="preserve">
    <value>Name</value>
  </data>
  <data name="WpfLocalizationDemo" xml:space="preserve">
    <value>WPF Localization Demo</value>
  </data>
</root>

German (de) resources. File Properties/Resources.de.resx:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <!-- 
    Microsoft ResX Schema 
 
    Version 2.0
 
    The primary goals of this format is to allow a simple XML format 
    that is mostly human readable. The generation and parsing of the 
    various data types are done through the TypeConverter classes 
    associated with the data types.
 
    Example:
 
    ... ado.net/XML headers & schema ...
    <resheader name="resmimetype">text/microsoft-resx</resheader>
    <resheader name="version">2.0</resheader>
    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
        <value>[base64 mime encoded serialized .NET Framework object]</value>
    </data>
    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
        <comment>This is a comment</comment>
    </data>
 
    There are any number of "resheader" rows that contain simple 
    name/value pairs.
 
    Each data row contains a name, and value. The row also contains a 
    type or mimetype. Type corresponds to a .NET class that support 
    text/value conversion through the TypeConverter architecture. 
    Classes that don't support this are serialized and stored with the 
    mimetype set.
 
    The mimetype is used for serialized objects, and tells the 
    ResXResourceReader how to depersist the object. This is currently not 
    extensible. For a given mimetype the value must be set accordingly:
 
    Note - application/x-microsoft.net.object.binary.base64 is the format 
    that the ResXResourceWriter will generate, however the reader can 
    read any of the formats listed below.
 
    mimetype: application/x-microsoft.net.object.binary.base64
    value   : The object must be serialized with 
            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
            : and then encoded with base64 encoding.
 
    mimetype: application/x-microsoft.net.object.soap.base64
    value   : The object must be serialized with 
            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
            : and then encoded with base64 encoding.
 
    mimetype: application/x-microsoft.net.object.bytearray.base64
    value   : The object must be serialized into a byte array 
            : using a System.ComponentModel.TypeConverter
            : and then encoded with base64 encoding.
    -->
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" />
              </xsd:sequence>
              <xsd:attribute name="name" use="required" type="xsd:string" />
              <xsd:attribute name="type" type="xsd:string" />
              <xsd:attribute name="mimetype" type="xsd:string" />
              <xsd:attribute ref="xml:space" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string" />
              <xsd:attribute name="name" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
              <xsd:attribute ref="xml:space" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" />
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <data name="Birthday" xml:space="preserve">
    <value>Geburtstag</value>
  </data>
  <data name="Close" xml:space="preserve">
    <value>Schließen</value>
  </data>
  <data name="Name" xml:space="preserve">
    <value>Name</value>
  </data>
  <data name="WpfLocalizationDemo" xml:space="preserve">
    <value>WPF Lokalisierungs Demo</value>
  </data>
</root>

User Settings. File Properties/Settings.settings:

<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="WpfLocalizationDemo.Properties" GeneratedClassName="Settings">
  <Profiles />
  <Settings>
    <Setting Name="Culture" Type="System.String" Scope="User">
      <Value Profile="(Default)" />
    </Setting>
    <Setting Name="UICulture" Type="System.String" Scope="User">
      <Value Profile="(Default)" />
    </Setting>
  </Settings>
</SettingsFile>

Main Window: Presentation/ShellWindow.xaml.cs:

using System.Windows;
 
namespace LocalizationSample.Presentation
{
    public partial class ShellWindow : Window
    {
        public ShellWindow()
        {
            InitializeComponent();
        }
    }
}

Main Window: Presentation/ShellWindow.xaml

<Window x:Class="LocalizationSample.Presentation.ShellWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:p="clr-namespace:LocalizationSample.Properties"
    Title="{x:Static p:Resources.WpfLocalizationDemo}" SizeToContent="WidthAndHeight" ResizeMode="CanMinimize">
 
    <Grid Margin="0,0,11,11">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
 
        <Label Content="{x:Static p:Resources.Name}" Grid.Column="0" Grid.Row="0" Margin="11,10,0,0"/>
        <TextBox Text="{Binding Name}" Grid.Column="1" Grid.Row="0" Width="100" Margin="11,11,0,0"/>
 
        <Label Content="{x:Static p:Resources.Birthday}" Grid.Column="0" Grid.Row="1" Margin="11,6,0,0"/>
        <TextBox Text="{Binding Birthday, StringFormat=d}" Grid.Column="1" Grid.Row="1" Width="100" Margin="11,7,0,0"/>
 
        <Button Content="{x:Static p:Resources.Close}" Grid.Column="1" Grid.Row="2" Width="73" HorizontalAlignment="Right" Margin="11,22,0,0"/>
    </Grid>
</Window>

Data Model: Domain/Person.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace LocalizationSample.Domain
{
    internal class Person
    {
        public string Name { get; set; }
 
        public DateTime Birthday { get; set; }
    }
}
Reference Material