Changing language with CultureManager

Introduction

The .NET Framework provides reasonably good support for writing multi-lingual Windows Forms applications. Once you have set the Localizable property of a form to true, then the Visual Studio designer will automatically generate code in the InitializeComponent method of the form that loads the localizable properties of the form and its controls from compiled resources. Using the Visual Studio designer, you can also create resources for other languages - simply set the Language property of the form and modify the language specific properties of the form and controls. Visual Studio will save these changes into a separate resource file and compile them into a satellite assembly for the language when you build your application.
At runtime when your form is created (and InitializeComponent is called), the .NET Framework selects the resources to load based on the current value of theSystem.Threading.Thread.CurrentThread.CurrentUICulture property. If you change this property before your form is created, then the resources for the corresponding culture will be loaded. This means that if you want to change the user interface language once your application is running, you must close any open forms and recreate them (or restart the entire application) to force the resources to be reloaded for the new UI Culture. This is somewhat clumsy and means losing any transient application state stored in the forms. This article provides a CultureManager component that allows the localizable resources for open forms and control to be dynamically reloaded using a new UI Culture.

Background

The articles Instantly Changing Language in the Form and UICultureChanger Component also provide solutions to this issue. The approach taken in this article differs from these previous articles in the following ways:
  • The above articles only handle a subset of standard properties, e.g. TextLocationSize, etc. By contrast, the solution presented here can load the resources for any localizable property of a component/control (localizable properties are those marked with the LocalizableAttribute).
  • The CultureManager component described here provides a hook (via the UICultureChanged event) that allows you to execute code after a forms resources have been reloaded. This can be useful, for instance, when the text displayed in a control is generated programmatically - but is still culture dependant.
  • The CultureManager component allows you to change the UI Culture of all open forms in the application. Alternatively you can change the UICulture for an individual form allowing you to have two (or more) open forms with different UI Cultures.
  • The CultureManager component works with forms and controls developed using VB.NET
  • The source code for this article is provided under the The Code Project Open License (CPOL). The second article above uses the GNU Lesser General Public License - this prevents inclusion of its source code directly into commercial applications. See the following CodeProject article on Licenses for more information.

How it Works

This section provides an outline of how the CultureManager component internal implementation works. If you just want to use the component "as is", you can skip down to Using the Component.

Basic Architecture

The CultureManager is implemented as a component that you place on each form in your application that you want to change culture dynamically. The CultureManager component attaches a handler to the static CultureManager.ApplicationUICultureChanged event so that it receives notification when theCultureManager.ApplicationUICulture static property is changed. The component then uses .NET Reflection to locate localizable resources for the form and reloads these resources using the new UI Culture. This event architecture allows the UI Culture of all open forms in the application to be changed by simply setting the CultureManager.ApplicationUICulture static property. Alternatively you can set theUICulture of an individual form.

Applying Resources

The CultureManager.ApplyResources method (shown below) implements the core logic used to reload the localizable resources for a component.
/// <summary>
/// Recursively apply localized resources to a component and its constituent components
/// </summary>
/// <PARAM name="componentType">The type we are applying resources for</PARAM>
/// <PARAM name="instance">The component instance to apply resources to</PARAM>
/// <PARAM name="culture">The culture resources to apply</PARAM>
protected virtual void ApplyResources(Type componentType, 
                                      Component instance, 
                                      CultureInfo culture)
{
    // check whether there are localizable resources for the type - if not we are done
    //
    System.IO.Stream resourceStream 
        = componentType.Assembly.GetManifestResourceStream(componentType.FullName 
             + ".resources");
    if (resourceStream == null) return;

    // recursively apply the resources localized in the base type
    //
    Type parentType = componentType.BaseType;
    if (parentType != null)
    {
        ApplyResources(parentType, instance, culture);
    }

    // load the resources for this component type into a sorted list
    //
    ComponentResourceManager resourceManager 
        = new ComponentResourceManager(componentType);
    SortedList<string object,> resources = new SortedList<string object,>();
    LoadResources(resourceManager, culture, resources);

    // build a lookup table of components indexed by resource name
    //
    Dictionary<string Component,> components = new Dictionary<string Component,>();

    // build a lookup table of extender providers indexed by type
    //
    Dictionary extenderProviders 
        = new Dictionary();

    bool isVB = IsVBAssembly(componentType.Assembly);

    components["$this"] = instance;
    FieldInfo[] fields = componentType.GetFields(BindingFlags.Instance | 
                                                 BindingFlags.NonPublic | 
                                                 BindingFlags.Public);
    foreach (FieldInfo field in fields)
    {
        string fieldName = field.Name;
        
        // in VB the field names are prepended with an "underscore" so we need to 
        // remove this
        //
        if (isVB)
        {
            fieldName = fieldName.Substring(1, fieldName.Length - 1);
        }

        // check whether this field is a localized component of the parent
        //
        string resourceName = ">>" + fieldName + ".Name";
        if (resources.ContainsKey(resourceName))
        {
            Component childComponent = field.GetValue(instance) as Component;
            if (childComponent != null)
            {
                components[fieldName] = childComponent;

                // apply resources localized in the child component type
                //
                ApplyResources(childComponent.GetType(), childComponent, culture);

                // if this component is an extender provider then keep track of it
                //
                if (childComponent is IExtenderProvider)
                {
                    extenderProviders[childComponent.GetType()] 
                        = childComponent as IExtenderProvider;
                }
            }
        }
    }

    // now process the resources 
    //
    foreach (KeyValuePair<string object,> pair in resources)
    {
        string resourceName = pair.Key;
        object resourceValue = pair.Value;
        string[] resourceNameParts = resourceName.Split('.');
        string componentName = resourceNameParts[0];
        string propertyName = resourceNameParts[1];

        if (componentName.StartsWith(">>")) continue;
        if (IsExcluded(componentName, propertyName)) continue;

        Component component = null;
        if (!components.TryGetValue(componentName, out component)) continue;

        // some special case handling for control sizes/locations
        //
        Control control = component as Control;
        if (control != null)
        {
            switch (propertyName)
            {
               case "AutoScaleDimensions":
                   SetAutoScaleDimensions(control as ContainerControl, (SizeF)resourceValue);
                   continue;
               case "Size":
                   SetControlSize(control, (Size)resourceValue);
                   continue;
               case "Location":
                   SetControlLocation(control, (Point)resourceValue);
                   continue;
               case "Padding":
               case "Margin":
                   resourceValue = AutoScalePadding((Padding)resourceValue);
                   break;
               case "ClientSize":
                   if (control is Form && PreserveFormSize) continue;
                   resourceValue = AutoScaleSize((Size)resourceValue);
                   break;
             }
        }

        // use the property descriptor to set the resource value
        //
        PropertyDescriptor pd 
            = TypeDescriptor.GetProperties(component).Find(propertyName, false);
        if (((pd != null) && !pd.IsReadOnly) && 
           ((resourceValue == null) || pd.PropertyType.IsInstanceOfType(resourceValue)))
        {
            pd.SetValue(component, resourceValue);
        }
        else 
        {
            // there was no property corresponding to the given resource name.  
            // If this is a control the property may be an extender property so 
            // try applying it as an extender resource
            //
            if (control != null)
            {
                ApplyExtenderResource(extenderProviders, 
                                      control, propertyName, resourceValue);
            }
        }
    }
}
The ApplyResources method first checks if there are localizable resources associated with the component type. If there aren't, there is nothing more to do. If there are, then the method recursively calls itself to first apply any localized resources associated with its base type. The base type resources must be applied before any localized resources defined by the derived class.
Next we call LoadResources to load the localized resources for this component type into a SortedList. This gives us a list of all the localized resources used by the component and enables us to quickly locate resources by name. We use reflection to locate the member variables of the component that may have localized resources and build a lookup table to enable us to quickly locate a child component object given its name. Note that the naming convention used by the VB.NET designer for component member variables requires some special handling for VB.NET components.
Finally we iterate through the list of localizable resources and use reflection to set the properties of the components with some special case handling as discussed below.

Some Special Cases

Setting the Location or Size property for a component that is anchored or docked can cause unexpected behaviour. The SetControlSize and SetControlLocation methods contain logic that checks the Anchorand Dock properties and only updates the components of the location or size that are not controlled by anchoring or docking.  The other issue the code addresses is when the screen resolution is different to that used at design time (because the user is using DPI scaling).   Normally Windows Forms automatically scales the sizes and locations of controls and forms after they are first created to account for this.   Since the sizes and locations in the resources are defined in the design time resolution we need to scale them to match the current screen resolution.  The SetAutoScaleDimensions method sets the AutoScaleFactor based the design time resolution dimensions and the CurrentAutoScaleDimensions.  Subsequent size, location and padding resources are scaled by this value

Extender Properties

Some localized resources do not correspond to simple properties on the constituent components. Extender components (for example ToolTips) provide extended design time properties for other controls on the form. The extender component handles code (and resource) serialization of these properties. Unfortunately there is no reflection mechanism to discover the methods on the extender component that we need to call - or the resource naming convention used for the extender properties. If there is no standard property corresponding to a localized resource, then the ApplyExtenderResource method (see below) is called to process the resource using some special case logic.
/// <summary>
/// Apply a resource for an extender provider to the given control
/// </summary>
/// <param name="extenderProviders">Extender providers for the parent 
/// control indexed by type</param>
/// <param name="control">The control that the extended resource is 
/// associated with</param>
/// <param name="propertyName">The extender provider property name</param>
/// <param name="value">The value to apply</param>
/// <remarks>
/// This can be overridden to add support for other ExtenderProviders.  
/// The default implementation handles <see cref="ToolTip">ToolTips</see>, 
/// <see cref="HelpProvider">HelpProviders</see>, and 
/// <see cref="ErrorProvider">ErrorProviders</see> 
/// </remarks>
protected virtual void ApplyExtenderResource
    (Dictionary extenderProviders, 
     Control control, string propertyName, object value)
{
    IExtenderProvider extender = null;

    if (propertyName == "ToolTip")
    {
        if (extenderProviders.TryGetValue(typeof(ToolTip), out extender))
        {
            (extender as ToolTip).SetToolTip(control, value as string);
        }
    }
    ...
}
We pass the ApplyExtenderResource method a lookup table of the components that implement theIExtenderProvider interface (indexed by type). This allows the method to locate the extender component associated with the given property and call the appropriate method to set the localized property. The base implementation of the ApplyExtenderResource method implements logic for ToolTipErrorProviderand HelpProvider extender components. It can be overridden to add support for other custom extender components.

Using the Component

Once you have downloaded the source code and built it, you can add the CultureManager component to your Visual Studio toolbox. If you are using the demo solution, then the component should already appear under the "Infralution.Localization Components" tab. To use the component in another project, right click on the toolbox and select "Choose Items..", then select the "Browse" button and locate theInfralution.Localization.dll assembly that you built earlier.
Place an instance of the component on each form in your application that you want to change culture dynamically. Note that this typically does not need to be all forms in your application, only those that are opened non-modally. If a form is always opened as a modal dialog (using ShowDialog) then there is no way for a user to change UI Culture of the application while the form is open.
Add a menu (or other mechanism) to set the CultureManager.ApplicationUICulture of the application. This sets the System.Threading.Thread.CurrentThread.CurrentUICulture and reloads the localizable resources for each open form that has a CultureManager component.
The component also provides some properties and events that you can use for additional control over the UI Culture change process:
  • ExcludeProperties - This allows you to specify a list of properties that you don't wish to be reloaded from resources when the UI Culture is changed. For instance, if you add "Enabled" to this list then the Enabled property of any controls will not be reloaded from resources. Note that generally you don't need to use this mechanism because Visual Studio will only serialize resources if they have non-default values. So the Enabled property for a control would not be stored in the form resources unless you had set it to false in the Visual Studio designer.
  • PreserveFormSize - If set to true then the forms size will not be reloaded from resources.
  • PreserveFormLocation - If set to true then the forms location will not be reloaded from resources.
  • UICultureChanged - This event is fired when UI Culture for the form is changed (after the resources have been reloaded). This allows you to execute code to update programmatically generated text.