Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Instantly Changing Language in the Form

0.00/5 (No votes)
21 Jan 2007 3  
How to switch language on the form instantly.

Introduction

.NET framework provides a rather good support for writing multilingual applications. Localized resources are stored in separate resource files that are compiled into separate DLLs. When the application is started, resources for selected localized settings are loaded from the appropriate DLL.

To see how this actually works, you should sneak into the code that Visual Studio automatically generates. When a Localizable property of a form is set to true, Windows Forms Designer modifies the code inside InitializeComponent method, adding an instance of ResourceManager class and modifying properties' set methods; all localizable properties of controls are set calling ResourceManager�s GetObject or GetString methods, making ResourceManager responsible for loading appropriate localized resources. These methods load corresponding values for the language set by the current thread�s culture. It is worth noting that not only captions displayed are localized, but also other settings for individual controls, such as position, size and visibility. This is useful in cases when controls are positioned differently for different languages.

The simplest way to change the language of the form is to set application thread UI language before InitializeComponent is called in the form�s constructor. Obviously, this requires the application to be restarted in order to change the language of the UI. Namely, InitializeComponent not only loads resources, but also initializes all controls on the form. Calling it for the second time in the same application run would create a new set of controls that are appended to the existing collection. These new controls will not be visible since they are covered with originally created controls, except if their positions and/or sizes differ. Moreover, reloading resources will reset the content of controls like textboxes.

And what if you want to change the UI language without restarting the application and keeping the changes the user has made? One possible solution is provided in this article.

Background

The basic idea is to rescan all properties of the parent form and containing controls and set their localizable values to a newly selected culture. The simplest approach would be to obtain the list of all properties using reflection. However, this �unselective� approach may cause unintended changes. For example, we usually do not want to change Text property of the TextBox control, but want to update this property for the Label control. Also, reloading Location property for the parent form will very probably reposition it. Therefore, properties are reloaded selectively, the list of properties being hard-coded.

We start with the parent form, reloading its localizable properties and then recurring containing controls. The procedure is repeated for each control, recurring its containing controls if they exist:

private void ChangeFormLanguage(Form form) {
  form.SuspendLayout();
  Cursor.Current = Cursors.WaitCursor;
  ResourceManager resources = new ResourceManager(form.GetType());
  // change main form resources

  form.Text = resources.GetString("$this.Text", m_cultureInfo);
  ReloadControlCommonProperties(form, resources);
  ToolTip toolTip = GetToolTip(form);
  // change text of all containing controls

  RecurControls(form, resources, toolTip);
  // change the text of menus

  ScanNonControls(form, resources);
  form.ResumeLayout();
}

ReloadControlCommonProperties method reloads the hard-coded list of properties that are common to the majority of controls:

protected virtual void ReloadControlCommonProperties(Control control, 
                                         ResourceManager resources) {
  SetProperty(control, "AccessibleDescription", resources);
  SetProperty(control, "AccessibleName", resources);
  SetProperty(control, "BackgroundImage", resources);
  SetProperty(control, "Font", resources);
  SetProperty(control, "ImeMode", resources);
  SetProperty(control, "RightToLeft", resources);
  SetProperty(control, "Size", resources);
  // following properties are not changed for the form

  if (!(control is System.Windows.Forms.Form)) {
    SetProperty(control, "Anchor", resources);
    SetProperty(control, "Dock", resources);
    SetProperty(control, "Enabled", resources);
    SetProperty(control, "Location", resources);
    SetProperty(control, "TabIndex", resources);
    SetProperty(control, "Visible", resources);
  }
  if (control is ScrollableControl) {
    // reloads properties specific to ScrollableControl:

    // AutoScroll, AutoScrollMargin, AutoScrollMinSize

    ReloadScrollableControlProperties((ScrollableControl)control, resources);
    if (control is Form) {
      // reloads properties specific to Form control only:

      // AutoScaleBaseSize, Icon, MaximumSize and MinimumSize

      ReloadFormProperties((Form)control, resources);
    }
  }
}

SetProperty method reloads a value of the property for which a name is passed:

private void SetProperty(Control control, string propertyName, 
                               ResourceManager resources) {
  PropertyInfo propertyInfo = control.GetType().GetProperty(propertyName);
  if (propertyInfo != null) {
    string controlName = control.Name;
    if (control is Form)
      controlName = "$this";
    object resObject = resources.GetObject(controlName + "." + 
                                 propertyName, m_cultureInfo);
    if (resObject != null) 
      propertyInfo.SetValue(control, Convert.ChangeType(resObject, 
                                propertyInfo.PropertyType), null);
  }
}

First, it checks if a property with this name exists for the control. If GetProperty method returns a non-null value (which means that the property does exist), ResourceManager tries to get the value of the resource; if this value is successfully obtained, the corresponding property is changed.

The method utilizes reflection to make setting property generic. Casting to the appropriate type is implemented by static ChangeType method of the Convert class.

The alternative to this generic approach would be to replace each call to SetProperty method with an appropriate code with hard-coded casts. Although such approach would result in faster code execution, it has some pitfalls. For example, there are properties of the same name in different classes that are of a different type: Appearance property in TabControl is of TabAppearance enumeration type, while in CheckBox it is of Appearance enumeration type. Therefore, such approach would require more extensive type checking and would be more error-prone.

RecurControls method scans all items in Controls collection, reloading its properties and recurring for containing controls. Since any containing control may have a localized text associated to it, we have to pass the reference to the parent form�s ToolTip object too.

private void RecurControls(Control parent, 
                    ResourceManager resources, ToolTip toolTip) {
  foreach (Control control in parent.Controls) {
    ReloadControlCommonProperties(control, resources);
    ReloadControlSpecificProperties(control, resources);
    if (toolTip != null)
      toolTip.SetToolTip(control, resources.GetString(control.Name + 
                                        ".ToolTip", m_cultureInfo));
    if (control is UserControl)
      RecurUserControl((UserControl)control);
    else {
      ReloadTextForSelectedControls(control, resources);
      // change ListBox and ComboBox items

      ReloadListItems(control, resources);
      if (control is TreeView) 
        ReloadTreeViewNodes((TreeView)control, resources);
      if (control.Controls.Count > 0)
        RecurControls(control, resources, toolTip);
    }
  }
}

Since containing control can be of any type, besides properties common to all controls, we also have to check properties specific to some controls, which is done in the ReloadControlSpecificProperties method given below:

protected virtual void 
         ReloadControlSpecificProperties(System.Windows.Forms.Control control, 
         System.Resources.ResourceManager resources) {
  // ImageIndex property for ButtonBase, Label,

  // TabPage, ToolBarButton, TreeNode, TreeView

  SetProperty(control, "ImageIndex", resources);
  // ToolTipText property for StatusBar, TabPage, ToolBarButton

  SetProperty(control, "ToolTipText", resources);
  // IntegralHeight property for ComboBox, ListBox

  SetProperty(control, "IntegralHeight", resources);
  // ItemHeight property for ListBox, ComboBox, TreeView

  SetProperty(control, "ItemHeight", resources);
  // MaxDropDownItems property for ComboBox

  SetProperty(control, "MaxDropDownItems", resources);
  // MaxLength property for ComboBox, RichTextBox, TextBoxBase

  SetProperty(control, "MaxLength", resources);
  // Appearance property for CheckBox, RadioButton, TabControl, ToolBar

  SetProperty(control, "Appearance", resources);
  // CheckAlign property for CheckBox and RadioBox

  SetProperty(control, "CheckAlign", resources);
  // FlatStyle property for ButtonBase, GroupBox and Label

  SetProperty(control, "FlatStyle", resources);
  // ImageAlign property for ButtonBase, Image and Label

  SetProperty(control, "ImageAlign", resources);
  // Indent property for TreeView

  SetProperty(control, "Indent", resources);
  // Multiline property for RichTextBox, TabControl, TextBoxBase

  SetProperty(control, "Multiline", resources);
  // BulletIndent property for RichTextBox

  SetProperty(control, "BulletIndent", resources);
  // RightMargin property for RichTextBox

  SetProperty(control, "RightMargin", resources);
  // ScrollBars property for RichTextBox, TextBox

  SetProperty(control, "ScrollBars", resources);
  // WordWrap property for TextBoxBase

  SetProperty(control, "WordWrap", resources);
  // ZoomFactor property for RichTextBox

  SetProperty(control, "ZoomFactor", resources);
}

As can be noted, RecurControls method calls itself recursively for any containing child.

If containing control is UserControl, a new ResourceManager has to be initialized for it to possibly load resources from the external DLL. Also, a ToolTip object of the UserControl must be obtained to pass reference to RecurControls method:

private void RecurUserControl(UserControl userControl) {
  ResourceManager resources = new ResourceManager(userControl.GetType());
  ToolTip toolTip = GetToolTip(userControl);
  RecurControls(userControl, resources, toolTip);
}

Some UI components, like MenuItems, StatusBarPanels and ColumnHeaders in ListViews, are not contained in Controls collections. These components are direct members of the parent form, so we are accessing them using reflection (the code below is somewhat simplified):

protected virtual void ScanNonControls(Form form, ResourceManager resources) {
  FieldInfo[] fieldInfo = form.GetType().GetFields(BindingFlags.NonPublic 
                          | BindingFlags.Instance | BindingFlags.Public);
  for (int i = 0; i < fieldInfo.Length; i++) {
    object obj = fieldInfo[i].GetValue(form);
    string fieldName = fieldInfo[i].Name;
    if (obj is MenuItem) {
      MenuItem menuItem = (MenuItem)obj;
      menuItem.Enabled = (bool)(resources.GetObject(fieldName + 
                                   ".Enabled", m_cultureInfo));
      // etc.

    }
    if (obj is StatusBarPanel) {
      StatusBarPanel panel = (StatusBarPanel)obj;
      panel.Alignment = 
        (HorizontalAlignment)(resources.GetObject(fieldName + 
        ".Alignment", m_cultureInfo));
      // etc.

    }
    if (obj is ColumnHeader) {
      ColumnHeader header = (ColumnHeader)obj;
      header.Text = resources.GetString(fieldName + ".Text", m_cultureInfo);
      header.TextAlign = 
        (HorizontalAlignment)(resources.GetObject(fieldName + 
        ".TextAlign", m_cultureInfo));
      header.Width = (int)(resources.GetObject(fieldName + ".Width", m_cultureInfo));
    }
    if (obj is ToolBarButton) {
      ToolBarButton button = (ToolBarButton)obj;
      button.Enabled = (bool)(resources.GetObject(fieldName + 
                                 ".Enabled", m_cultureInfo));
      // etc.

    }
  }
}

Using the code

The procedure described is implemented as a FormLanguageSwitchSingleton class with two public methods: ChangeCurrentThreadUICulture and ChangeLanguage, the latter having two overloaded implementations. Class is implemented in System.Globalization namespace.

There are two possible scenarios for the use of FormLanguageSwitchSingleton:

  1. Current thread�s culture is changed by calling ChangeCurrentThreadUICulture method first, followed by call to ChangeLanguage method, like:
    CultureInfo newCulture = new CultureInfo("de");
    FormLanguageSwitchSingleton.Instance.ChangeCurrentThreadUICulture(newCulture);
    FormLanguageSwitchSingleton.Instance.ChangeLanguage(this);
  2. The overloaded version of ChangeLanguage method that accepts additional CultureInfo argument is called only:
    CultureInfo newCulture = new CultureInfo("de");
    FormLanguageSwitchSingleton.Instance.ChangeLanguage(this, newCulture);

In the second scenario, only forms currently opened will be �translated� to the culture provided; all subsequently opened forms will use the culture of the current application thread. The reader may observe the difference between the two scenarios in the TestMDIApp provided as a sample for download; checking/unchecking the option in the Change Language dialog:

The following methods of the singleton class are made virtual to allow users to override them:

  • ReloadTextForSelectedControls; current implementation does this for AxHost, ButtonBase, GroupBox, Label, ScrollableControl, StatusBar, TabControl, ToolBar control types.
  • ReloadControlCommonProperties (implementation given above);
  • ReloadControlSpecificProperties (c.f. above);
  • ScanNonControls (c.f. above);
  • ReloadListItems - reloads items in ComboBox and ListBox controls. Moreover, if items are not sorted, item(s) selection is kept.

FormLanguageSwitchSingleton is compiled to a DLL. To use the class, just append the corresponding class library into the reference list. Examples of use are given in two test samples provided with project source.

Points of Interest

Items in ListBox, ComboBox and DomainUpDown, as well as TreeNodes in TreeView are reloaded by ReloadListBoxItems, ReloadComboBoxItems, ReloadUpDownItems and ReloadTreeViewNodes methods respectively. However, it is important to note that there are no references to individual items/nodes. They are loaded as nameless objects in AddRange methods of Items/Nodes properties of the corresponding method, e.g.:

this.listBox.Items.AddRange(new object[]
    { 
      resources.GetString("listBox.Items.Items"),
      resources.GetString("listBox.Items.Items1"),
      resources.GetString("listBox.Items.Items2")
    }
  );

As seen from the code above, the name of an item is created from the control name, followed by two �Items� strings, a numeric index being appended to the second string for all items except the first one. Therefore, these names have to be created dynamically:

private void ReloadItems(string controlName, IList list, int itemsNumber, 
                       System.Resources.ResourceManager resources) {
  string resourceName = controlName + ".Items.Items";
  list.Clear();
  list.Add(resources.GetString(resourceName, m_cultureInfo));
  for (int i = 1; i < itemsNumber; i++) 
    list.Add(resources.GetString(resourceName + i, m_cultureInfo));
}

History

  • ver. 1.0 - initial release (December 6, 2004).
  • ver. 1.1 - some bug fixes, including those noticed by Gregory Bleiker and Piotr Sielski (March 21, 2005).
  • ver. 1.2 - obtaining resources made safer through call of GetSafeValue method. It looks for selected language localized resource first; if not found, value for default language is returned. Also (hopefully) fixed "Ambiguous match found" error.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here