
Introduction
For developers used to the integrated, inbuilt localization support for Windows
Forms applications, the Microsoft approach to localizing WPF applications using
the somewhat primitive
Locbaml tool can come as a shock. Some of the issues that have been identified
with this approach are:
- The localization process is not integrated into the standard Visual Studio build
mechanism (as it is for Windows Forms applications).
- There is no way to view or edit the localized XAML within the Visual Studio designer.
- Locbaml uses CSV files and has issues when the translated text includes commas.
The use of CSV files forces translators to work with two separate mechanisms since
they still have to work with standard .NET RESX files for programmatically translated
strings.
- The Locbaml approach results in the complete binary XAML for the window being replicated
in the satellite assemblies for each localized language. This results in much larger
footprint for localized applications compared to the Windows Forms approach where
only those resources that differ from the invariant culture are compiled into the
satellite assemblies.
- There is no way to dynamically change the language of the application at runtime
without closing and recreating windows.
The issues with the Locbaml approach have resulted in the development of a multitude
of different solutions for localizing WPF applications. The following are just some
of the solutions proposed:
At the risk of adding to the confusion, this article outlines another approach which
builds on the strengths of some of these earlier solutions.
Background
The article Simple
WPF Localization provides an effective, simple solution for localizing text
resources in WPF applications. It uses a WPF Extension to get the string resources
from the standard project Properties.Resources RESX file. This article
takes a similar approach, and defines a RESX extension that allows WPF properties
to be pulled from embedded RESX resources. The solution outlined here differs in
the following ways:
- Resources can be localized using any embedded RESX file. This allows you to take
a similar approach to Windows Forms localization and store the resources associated
with each WPF window or control in a separate RESX file (typically named the same
as the Window). This is important for projects with a large number of windows where
it can become difficult to manage the large number of resources in a single file,
particularly with multiple developers. It also means that your resource names only
have to be unique within the window.
- Any WPF property (not just strings) can be localized using the RESX Extension. You
can use it to localize images, locations, sizes, and other layout properties. The
built-in support for images makes defining icons for windows, menus, and toolbars
much simpler, and is worth using even if you don't want to localize them.
- The design time display culture can be selected dynamically using a notification
icon in the system tray, allowing you to view and edit the localized windows in
the Visual Studio designer. This is great for verifying the localized layout for
a window without having to run the application.
- The RESX extension allows you to define a default fallback value for properties
that is used if the associated resource cannot be loaded. This is particularly important
when localizing non-text properties where returning a
null value may
cause the page to fail to load.
Using the RESX Extension
Once you have downloaded the source code and built it, add a reference to the compiled
assembly (Infralution.Localization.Wpf) to your project. If you are using
the demo solution, then this is already done for you. You are now ready to use the
ResxExtension in your own XAML.
Markup extensions allow you to define XAML property values that are evaluated by
calling custom code defined by the Markup Extension. See the MSDN article
Markup Extensions and XAML for more information. The ResxExtension
derives from the base MarkupExtension class, and evaluates the property
by retrieving the data from an embedded RESX resource. For instance, the following
markup sets the Text property of a TextBlock to the "MyText"
resource from the embedded RESX resource called MyApp.TestWindow:
<TextBlock Text="{Resx ResxName=MyApp.TestWindow, Key=MyText}"/>
If you haven't yet created the resource (and compiled your project), then this will
be displayed in the WPF designer as #MyText, where the # highlights the
fact that the resource has not yet been defined. The next step is to create the
RESX file and define the resource. If the default namespace for your project is
"MyApp", then you simply create a RESX file called "TestWindow.resx"
and set its Build Action to "Embedded Resource". Add a string resource with the
name "MyText" and recompile the project. If you now open the TestWindow
XAML in the designer, you will see the string from the resource file displayed
in the TextBlock.
To add a localization, simply copy the RESX file and rename it. For instance, to
create a French localization, copy the RESX file to TestWindow.fr.resx
and include it in your project as an embedded resource. Change the "MyText"
resource to the French translation. When you recompile your project, Visual Studio
will automatically create the French satellite assembly containing the French resources.
Setting the Default ResxName
Setting the ResxName property for each Resx element within
a window leads to a lot of duplicated XAML.
Patrick Duffy suggested a solution using attached properties which allows
much concise XAML. This allows you to set the attached ResxExtension.DefaultResxName
property at the top most element as shown below:
<Window ResxExtension.ResxName="WpfApp.MainWindow" Language="{UICulture}>"
The ResxName property can now be omitted from the Resx
elements. Furthermore, if we only need to specify the Key property
(which is the default property), then we can omit the parameter name leading to
a very concise declaration:
<TextBlock Text="{Resx MyText}"/>
Setting the DefaultResxName using the attached property works provided
you are using the Resx element within a normal FrameworkElement
property. It will not work however when the Resx element is used within
another MarkupExtension. In this case, you will still need to set the
ResxName explicitly.
Changing the Design Time Culture
Open TestWindow.xaml in the designer again. You should notice that a new
icon appears in your Windows desktop notification tray. This allows you to select
the culture used at design time. Click on the icon and select "Other Cultures...".
Select one of the French cultures from the dropdown list. The translations displayed
in the designer will switch to those from the French RESX file.
Changing the Culture Dynamically at Runtime
The culture of your application can be changed dynamically at runtime simply by
setting the CultureManager.UICulture property. This sets the CurrentThread.UICulture
and automatically updates all active XAML that uses the ResxExtension.
Images and Icons
The ResxExtension doesn't just work for text. It also makes it easy
to use icons and images from RESX files in your XAML markup. For instance, to define
the icon for a window, simply add an icon resource to the RESX file named "Window.Icon",
then define the markup as follows:
<Window Icon="{Resx Window.Icon}"/>
This is so much easier than the standard way of defining WPF window icons, that
you will probably want to use this even if you don't want to localize the icons.
You can use the same technique for setting the icons for menus and toolbars.
Localizing Other Property Types
The ResxExtension can be used to localize properties of any type. For
instance, the Margin property of a TextBlock could be
defined as follows:
<TextBlock
Margin="{Resx Key=MyMargin, DefaultValue='18,0,0,71'}"/>
In this case, note that we have defined a DefaultValue attribute. This
is the value used if no resource can be found and loaded. If you don't provide a
DefaultValue for non-text properties, then the XAML may fail to load
in the designer if the resource has not been defined yet. The resource can be defined
as a simple string resource (with value "18, 0, 0, 71"), or you can define it as
a fully typed value in the RESX file, e.g.:
<data name="MyMargin" type="System.Windows.Thickness, PresentationFramework">
<value>18, 0, 0, 71</value>
</data>
The latter is somewhat more work - but it does ensure that only valid values can
be entered using the resource editor.
Formatting Bound Data
WPF provides the Binding element to bind a property to a data source.
The Binding.StringFormat property allows you to supply a string
used to format the data for display. For instance:
<Binding StringFormat="Selected Item: {0}" ElementName="_fileListBox" Path="SelectedItem"/>
To localize this, we would like to specify the StringFormat property
using a Resx element. This works, provided you don't change the culture
at runtime. If you change the culture then you will get an InvalidOperation
exception with the message "Binding cannot be changed after it has been used". Unfortunately
bindings weren't designed with dynamic updating of culture in mind. To overcome
this, the ResxExtension can itself act like a binding. You simply set
the binding properties (prefixed with "Binding") and the resource value
is used as a format string. For instance, the binding above would become:
<Resx Key="MyFormatString" BindingElementName="_fileListBox" BindingPath="SelectedItem"/>
The ResxExtension also supports formatting data from multiple data
sources (similar to a MultiBinding) by nesting Resx elements.
For instance:
<Resx Key="MyMultiFormatString">
<Resx BindingElementName="_fileListBox" BindingPath="Name"/>
<Resx BindingElementName="_fileListBox" BindingPath="SelectedItem"/>
</Resx>
In this case, you would define the MyMultiFormatString resource with
placeholders for both data source arguments eg "Selected {0}: {1}".
UICulture Extension
The project also defines another markup extension - UICulture. This
extension is used to set the Language property of a WPF window (or
other elements) to the language matching the current CultureManager.UICulture.
Like the RESX Extension, the UICulture extension automatically updates
attached elements when the CultureManager.UICulture changes.
ResourceEnumConverter
For convenience, the project includes the ResourceEnumConverter class.
This provides an easy mechanism for localizing the display text associated with
enums. It is described in more detail in
this article.
Hiding Window RESX Files in the Visual Studio Solution Explorer
One nice feature of Windows Forms localization is that the RESX files associated
with a form or control are hidden by default. To see the RESX files, you click on
the expand button next to the form or control. This means that as you add more languages,
your Solution Explorer pane does not become too cluttered. You can also do this
for the RESX files associated with a window XAML file by editing the Visual Studio
project file and adding a DependentUpon XML node for the RESX file.
E.g.:
<EmbeddedResource Include="TestWindow.resx">
<DependentUpon>TestWindow.xaml</DependentUpon>
<SubType>Designer</SubType>
</EmbeddedResource>
Implementation Notes
This section provides some notes on the internal implementation of the ResxExtension
class. You don't need to read this to use the ResxExtension, but if
you are interested in some of the design choices, then read on.
Object Lifetime Management
Object lifetime management is one of the main issues when designing a MarkupExtension
that will dynamically update the XAML that uses it. The ResxExtension
needs to maintain a reference to the target XAML elements that use it to enable
the elements to be updated when the CultureManager.UICulture is changed.
If this reference is a strong reference however, then the WPF elements that use
the extension will never be garbage collected. To avoid this situation, the extension
instead maintains a weak reference to the WPF target objects. This enables the target
objects to be garbage collected. The only problem is that there is still a strong
reference to the extension objects themselves (since we need to hold a collection
of the extension objects in order to update them). This issue is overcome by periodically
calling a cleanup function that removes extensions that no longer have active targets.
The cleanup is triggered after a set number of extension objects have been created
since the last cleanup. This lifetime management mechanism has been implemented
in some base classes to enable it to be used for any MarkupExtension
that requires this behaviour. The ManagedMarkupExtension provides the
base class for the extension, and implements the weak reference to the WPF target
objects. The MarkupExtensionManager class manages a collection of
ManagedMarkupExtension objects, and implements the update and cleanup
mechanism. The ResxExtension and UICultureExtension both
derive from ManagedMarkupExtension, and use a static instance
of the MarkupExtensionManager class to handle updating WPF targets
when the CultureManager.UICulture is changed.
Data Binding Support
Overcoming the immutability of the standard Binding markup extension
proved to be quite difficult and I tried quite a few approaches before finally settling
on the current design which I think is quite elegant. The solution allows you to
set binding properties directly on a Resx element. The ResxExtension
just delegates these properties to an underlying Binding instance and
sets the Binding.StringFormat to the resource value. When the culture
is changed, the ResxExtension creates a new copy of the binding and
updates the target to use the new binding.
Expression Blend Support
The ResxExtension will work inside
Expression Blend and display the resources from the embedded invariant RESX
file (provided that the project has been compiled). Unfortunately, Expression Blend
will not load resources from satellite assemblies, and so changing the design time
culture using the desktop tray icon has no effect when using Expression Blend.
Globalizer.NET Support
The ResxExtension was designed to support localizing WPF applications
using Infralution's Globalizer.NET
tool (although you certainly don't need Globalizer.NET to use the ResxExtension).
The ResxExtension class includes a static GetResource
event that allows Globalizer.NET (or any other localization tool) to hook into the
resource translation mechanism and dynamically provide translations for resources.
This enables Globalizer.NET to display translated previews of windows and controls
that use the ResxExtension without having to first compile the satellite
assemblies. Globalizer.NET also includes the ability to scan existing WPF projects
for localizable properties and automatically convert them to use the ResxExtension.
History
- 2009.04.08
- 2009.04.20
- Fixed bug in
ResxExtension.ConvertValue
- 2009.09.22
- Fixed
ResxExtension.GetDefaultValue to handle non-dependency types
and added UpdateTarget methods
- 2010.01.05
- Fixed
ManagedMarkupExtension.UpdateTarget to handle targets which don’t
inherit from FrameworkElement
- 2010.06.30
- Fixed issue with using
ResxExtension with PRISM
- 2011.05.30
- Fixed issue with using
ResxExtension in templates
- 2012.02.06
- Added
ResxExtension.ResxName attached property to allow setting the
default ResxName for a window/control
- Added binding properties to
ResxExtension to allow you to format bound
data using a resource string
- Added support for binding to multiple data sources (similar to
MultiBinding)
by nesting ResxExtension elements
- Changed
ResourceEnumConverter to implement IValueConverter
to allow derived classes to be used in XAML as binding converters
- Added check for dynamic assemblies in
HasEmbeddedResx to avoid
exceptions being thrown internally
- 2012.03.02
- Changed name of attached property from
ResxExtension.ResxName to
ResxExtension.DefaultResxName. This fixes a runtime markup parsing
exception in .NET 4 if you explicitly set the ResxName parameter for an extension
(see discussion regarding this bug in comments).
- 2012.03.22
- Fixed exception when changing cultures if the the
ResxExtension is used in a
template
- Fixed display of culture names at design time when using in .NET 4 projects (previously the culture code was displayed rather than the friendly name)
- Add overridable
GetResourceName method to ResourceEnumConverter class to allow derived classes to change the default resource naming
2013.02.14
- Fixed exception when using
ResourceEnumConverter inside a multibinding ResxExtension with .NET 4 Framework