Perspective : a light process for WPF localization

Perspective : a light process for WPF localization

Principle and implementation

The standard localization process for WPF applications is relatively cumbersome and unsuited to a on the fly localization of a small application under development.

Indeed, it relies on the outsourcing of BAML resources (binary resources corresponding to XAML files) in satellite assemblies. A basic utility program, LocBaml (provided as a sample application with the SDK), will retrieve textual data resource from BAML to put them into a new satellite assembly, once translated. Beforehand, all the elements to translate must have been marked by an attribute x:Uid, ideally automatically created by MsBuild. The result is that XAML code is riddled with x:Uid attributes having automatically generated names, thereby reducing its readability. In addition, BAML resources are duplicated in satellite assemblies, and any amendment of the XAML code requires a regeneration of all the translated satellite assemblies. In other words, the localization is not really possible until the end of the development cycle.

So I developed in Perspective library an alternative by using classical .NET resources string for all text properties to be localized. Thanks to the standard satellite assemblies mechanism, the application can easily be localized during the development phase.

The implementation is very simple :

  • Add in the project a .resx resource file for each XAML file, in the base language of the application, and create a resource for each text to be localized. I.e., for a given XAML file DpiScaling.xaml, create a DpiScaling.resx file in a subfolder named Strings (subfolder of the folder where the XAML file resides, Pages/pWpf for the Perspective demo application). In the Access modifier field, choose No code generation.

Visual Studio : Default resources (English language)

  • Add in the project a .resx resource file for each targeted culture, i.e. Strings\DpiScaling.fr.resx, and translate the resources.

Visual Studio : French language resources

  • In the XAML file, add a ResourceStringDecorator (a Perspective class) as container of all the elements to localize.
  • Indicate in the AssemblyName property the name of the assembly, without extension, and in the BaseName property the name of the .resx file, without extension and prefixed with the path in dot notation, i.e. :
<p:ResourceStringDecorator 
    AssemblyName="Perspective.Config"
    BaseName="View.Strings.DpiScaling">
...
</p:ResourceStringDecorator>
  • For each property to localize, use the ResourceString markup extension (a Perspective class) to reference the corresponding resource in the .resx file.
<TextBlock Text="{p:ResourceString HowToSetScale}"/>
  • To make the system operational, it is necessary to register the assembly in the ResourceAssemblies dictionary of singleton CultureManager provided in Perspective. The Perspective.Config assembly of the Perspective application encapsulates this registration in the Register() method of the ResourceAssembly class :
public static class ResourceAssembly
{
    private static bool _registered;

public static void Register() { if (!_registered) { Assembly assembly = Assembly.GetExecutingAssembly(); CultureManager.Current.ResourceAssemblies.Add( //"Perspective.Config", Extension.AssemblyNameConst, assembly); _registered = true; } } }

  • Each module assembly exposes an equivalent class. The Register() methods of these classes must be called in the constructor of the module's main class :
public Extension()
    : base()
{
    ...
    ResourceAssembly.Register();
    ...
}

When executing, the resources corresponding to the current culture (DpiScaling.fr.resx for the French language) are automatically loaded by .NET.

Execution : French language resources

If the satellite assembly is not found for the current culture, default resources are loaded (DpiScaling.resx).

Execution : Default resources (English language)

It is possible to change the culture at run time through a user interface control (RadioButton or other). Just change the UICulture property of CultureManager. The Perspective mechanism then reloads the related resources.

Culture changing at run time

Extract from LocalizationConfig.xaml and LocalizationViewModel.cs (project Perspective.Config):

<ListView.ItemTemplate>
    <DataTemplate>
        <RadioButton 
            GroupName="LocaleInfo"
            Content="{Binding Title}"
            Command="{Binding 
                RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Page}}, 
                Path=DataContext.SetCurrentLocaleInfoCommand}"
            CommandParameter="{Binding}">
            <RadioButton.IsChecked>
                <p:SignalBinding 
                    x:Name="radioButtonIsCheckedBinding"
                    Path="CultureName"
                    Mode="OneWay"
                    Converting="radioButtonIsCheckedBinding_Converting"/>
            </RadioButton.IsChecked>
        </RadioButton>
    </DataTemplate>
</ListView.ItemTemplate>
...
public SignalCommand SetCurrentLocaleInfoCommand { get; private set; }
public LocalizationViewModel()
{
    ...
    SetCurrentLocaleInfoCommand = new SignalCommand();
    SetCurrentLocaleInfoCommand.Executed += (sender, e) =>
    {
        if ((e.Parameter != null) && (e.Parameter is LocaleInfo))
        {
            CurrentLocaleInfo = e.Parameter as LocaleInfo;
            CultureManager.Current.UICulture = CurrentLocaleInfo.CultureName;
        }
    };
}

Controls without inheritance context

The system described above uses the mechanism of inherited properties. Some WPF controls, like ContextMenu, have no inheritance context and thus are not compatible with ResourceStringDecorator. In this case (and only in this case), it is necessary to use the AssemblyName and BaseName properties of the the ResourceString markup extension :

<TabItem.ContextMenu>
    <ContextMenu>
        <MenuItem Header="{p:ResourceString Name=English, AssemblyName=PerspectiveDemo.UI, BaseName=UserControls.Strings.MainControl}" />
        <MenuItem Header="{p:ResourceString Name=French, AssemblyName=PerspectiveDemo.UI, BaseName=UserControls.Strings.MainControl}" />
        <MenuItem Header="{p:ResourceString Name=BubbleChart, AssemblyName=PerspectiveDemo.UI, BaseName=UserControls.Strings.MainControl}" />
    </ContextMenu>
</TabItem.ContextMenu>

Architecture

The concerned classes (ResourceStringDecorator, ResourceStringExtension, CultureManager) are declared in the namespace Perspective.Wpf.ResourceStrings (project Perspective.Wpf).

ResourceStringDecorator inherits from Decorator, and publishes the AssemblyName and BaseName dependency properties. When the value of this one changes, the ApplyResources() method is called. A ResourceManager is instanciated, and referenced by the ResourceManager dependency property. The GetAssembly() method returns the assembly first registered with the Register() method of the ResourceAssembly class (this is necessary for the partial trust mode of XBAP).

private void ApplyResources(string baseName)
{
    string fullBaseName = AssemblyName + "." + baseName;
    Assembly assembly = GetAssembly(AssemblyName);
    ResourceManager rm = new ResourceManager(
        fullBaseName,
        assembly);
    SetValue(ResourceManagerProperty, rm);
    ...
}

ResourceManager is an "heritable" property : each child element acquires automatically this property (and its value), as if it were its own property.

ResourceStringExtension inherits from MarkupExtension. It has a Name property, indicating the name of the resource to use. The ProvideValue() method is overloaded to return the value of the resource. For this, it gets a reference on the object owner of the property, through the IProvideValueTarget service. Once it gets this reference, it calls the InitializeValue() to get the resource's value.

public override object ProvideValue(IServiceProvider serviceProvider)
{
    string value = "";
    IProvideValueTarget target = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
    if (target != null)
    {
        DependencyObject d = (DependencyObject)target.TargetObject;
        if ( d != null)
        {
            //...
            value = ResourceStringDecorator.InitializeValue(
                d,
                target.TargetProperty,
                _name);
        }
    }
    return value;
}

The method InitializeValue() of the ResourceStringDecorator class returns the value of a resource and registers the property so that its contents can be refreshed. It gets a reference on the ResourceManager, "inherited" (acquired) from the ResourceStringDecorator and calls its method GetString () which returns the value of the resource. The property is registered by an object DepPropResourceLink or ClrPropResourceLink depending on its nature (dependency property or CLR property). This object is added to the collection localizedChildren. The method ApplyResourceFrom () of this object will be called during a refreshment by the method Refresh() of ResourceStringDecorator.

public static string InitializeValue(
    DependencyObject d,
    object property,
    string resourceName)
{
    string value = "";
    ResourceManager rm = ResourceStringDecorator.GetResourceManager(d);
    if (rm != null)
    {
        value = rm.GetString(resourceName);
        List<ResourceLink> localizedChildren = ResourceStringDecorator.GetLocalizedChildren(d);
        if (localizedChildren != null)
        {
            if (property is DependencyProperty)
            {
                DepPropResourceLink dprl = new DepPropResourceLink(
                    d, resourceName, (DependencyProperty)property);
                localizedChildren.Add(dprl);
            }
            if (property is PropertyInfo)
            {
                ClrPropResourceLink cprl = new ClrPropResourceLink(
                    d, resourceName, (PropertyInfo)property);
                localizedChildren.Add(cprl);
            }
        }
    }
    return value;
}

The Refresh() method is called automatically when the UICulture property of singleton CultureManager has changed (ResourceStringDecorator has registered an event handler on the event UICultureChanged).

Controls without inheritance context

The localization resources of controls without inheritance context are managed through the ResourceStringCoordinator and ResourceSetManager classes (instead of ResourceStringDecorator).

About this article

Author: Olivier Dewit.

History:

  • the 30th june, 2010 : update for the 2.0 version of Perspective, WPF 4 and Visual Studio 2010
  • the 26th may, 2008 : update for the 0.9.2 version of Perspective (Controls without inheritance context). Original article
  • the 19-20th may, 2008 : update for the 0.9 version of Perspective.
  • January 8, 2008 : 1st publication (Perspective version 0.5). Original article