Click here to Skip to main content
15,860,972 members
Articles / Desktop Programming / Windows Forms
Article

Instantly Changing Language in the Form

Rate me:
Please Sign up or sign in to vote.
4.93/5 (48 votes)
21 Jan 2007Zlib6 min read 296.7K   12.6K   202   69
How to switch language on the form instantly.

Image 1

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:

C#
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:

C#
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:

C#
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.

C#
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:

C#
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:

C#
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):

C#
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:
    C#
    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:
    C#
    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:

Image 2

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.:

C#
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:

C#
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, along with any associated source code and files, is licensed under The zlib/libpng License


Written By
Software Developer (Senior)
Croatia Croatia
Graduated at the Faculty of Electrical Engineering and Computing, University of Zagreb (Croatia) and received M.Sc. degree in electronics. For several years he was research and lecturing assistant in the fields of solid state electronics and electronic circuits, published several scientific and professional papers, as well as a book "Physics of Semiconductor Devices - Solved Problems with Theory" (in Croatian).
During that work he gained interest in C++ programming language and have co-written "C++ Demystified" (in Croatian), 1st edition published in 1997, 2nd in 2001, 3rd in 2010, 4th in 2014.
After book publication, completely switched to software development, programming mostly in C++ and in C#.
In 2016 coauthored the book "Python for Curious" (in Croatian).

Comments and Discussions

 
QuestionChanging controls text only Pin
stanzanim25-Oct-17 3:58
stanzanim25-Oct-17 3:58 
AnswerRe: Changing controls text only Pin
Julijan Sribar30-Oct-17 20:42
Julijan Sribar30-Oct-17 20:42 
QuestionMultilingual App in C# Pin
diwesh14-May-14 15:19
diwesh14-May-14 15:19 
QuestionFantastic, but! Pin
Kilazur21-Apr-14 22:16
Kilazur21-Apr-14 22:16 
AnswerRe: Fantastic, but! Pin
Julijan Sribar21-Apr-14 22:29
Julijan Sribar21-Apr-14 22:29 
QuestionHow to add more language Pin
Snehasish_Nandy10-Feb-14 22:28
professionalSnehasish_Nandy10-Feb-14 22:28 
AnswerRe: How to add more language Pin
Julijan Sribar10-Feb-14 23:16
Julijan Sribar10-Feb-14 23:16 
QuestionHave a problem to start the source code Pin
Ekrem SABAN26-Aug-11 0:28
Ekrem SABAN26-Aug-11 0:28 
AnswerRe: Have a problem to start the source code Pin
Julijan Sribar30-Aug-11 9:28
Julijan Sribar30-Aug-11 9:28 
GeneralMy vote of 5 Pin
marekbar2188-Aug-11 10:36
marekbar2188-Aug-11 10:36 
GeneralVisual inheritance Pin
sfk_ers25-Aug-08 2:42
sfk_ers25-Aug-08 2:42 
QuestionHow to add a .resx file under a .cs file?like FormMain.en.resx for FormMain.cs. Pin
whChina5-Aug-08 17:20
whChina5-Aug-08 17:20 
AnswerRe: How to add a .resx file under a .cs file?like FormMain.en.resx for FormMain.cs. Pin
Julijan Sribar5-Aug-08 23:50
Julijan Sribar5-Aug-08 23:50 
GeneralLanguage fallback Pin
fesnow25-Nov-07 23:36
fesnow25-Nov-07 23:36 
Questionwhat about grid ? Pin
Fenasi Kerim6-Nov-07 2:19
Fenasi Kerim6-Nov-07 2:19 
AnswerRe: what about grid ? Pin
Julijan Sribar6-Nov-07 10:57
Julijan Sribar6-Nov-07 10:57 
GeneralProblem with menu items Pin
Rudolf Jan10-Mar-07 5:22
Rudolf Jan10-Mar-07 5:22 
GeneralRe: Problem with menu items Pin
Julijan Sribar10-Mar-07 11:50
Julijan Sribar10-Mar-07 11:50 
GeneralProblem representing untranslated texts Pin
Rudolf Jan16-Jan-07 9:58
Rudolf Jan16-Jan-07 9:58 
GeneralRe: Problem representing untranslated texts Pin
Julijan Sribar16-Jan-07 12:47
Julijan Sribar16-Jan-07 12:47 
AnswerWorks now, but take care ... Pin
Rudolf Jan20-Jan-07 1:49
Rudolf Jan20-Jan-07 1:49 
GeneralRe: Works now, but take care ... Pin
Julijan Sribar20-Jan-07 4:12
Julijan Sribar20-Jan-07 4:12 
AnswerRe: Works now, but take care ... Pin
Rudolf Jan20-Jan-07 4:48
Rudolf Jan20-Jan-07 4:48 
GeneralRe: Works now, but take care ... Pin
Julijan Sribar20-Jan-07 6:56
Julijan Sribar20-Jan-07 6:56 
Generalit looks good but Pin
omni9628-Oct-06 8:59
omni9628-Oct-06 8:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.