Introduction
This article introduces a way to provide your application some globalization feathers, like different languages. The ui language can be change at runtime. And the language setting file is simple and can be edited by a non-programmer. For programmers, it's simply to use--just calling a couple of functions.
It's applied to .NET WinForm apps, not tried on other platforms.
Demo program-English version (change language by selecting the combobox's items):

Demo program-Chinese(simplified) version:

Background
The
Microsoft way to resolve multi-language problem is using the resource files. But I discard this method for
two reasons: It's not editable by non-professionals, and the resource file cannot be added or edited once the app is published.
Using the code
It's very simple to use the code:
First, you need name you control, and set their properties in the XML file (one language with a XML). If you want to rename you control, you need to modify you XML file too. Second, add a language selection combobox in you form, and
write codes like these:
private void cbLang_SelectedIndexChanged(object sender, EventArgs e)
{
MultilangComboHelper.UpdateSelection(langSetter, cbLang,this);
MultilangComboHelper.UpdateSelection(langSetter, cbLang, frmAbout);
}
The MultilangComboHelper
is just a static helper class:
static class MultilangComboHelper
{
public static void FetchChooser(LanguageSetter langSetter, ComboBox cbox)
{
LanguageSetter.LanguageDesc[] langDesc = langSetter.EnumLanguages();
cbox.Items.Clear();
cbox.DataSource = langDesc;
cbox.DisplayMember = "DisplayName";
cbox.ValueMember = "FileName";
}
public static void UpdateSelection(LanguageSetter langSetter, ComboBox cbox, Control control)
{
if (cbox.SelectedItem == null) return;
string lang = (cbox.SelectedItem as LanguageSetter.LanguageDesc).Language;
langSetter.SetLanguage(lang);
langSetter.UpdateUILanguage(control);
}
}
LanguageSetter
is the core class, it's main function is parse the XML file, and travel a control's sub controls, using
reflection to find control's properties,
then set the control's properties to the corresponded value in XML.
public class LanguageSetter
{
private const string SPLIT_CHAR = ".";
private List<LanguageDesc> _langFile;
private string _LangPath;
public XmlNode _RootNode = null;
static private void SetPropertyValue(object classInstance, string propertyName, object value)
{
Type myType = classInstance.GetType();
PropertyInfo myPropertyInfo = myType.GetProperty(propertyName,
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (myPropertyInfo != null)
{
if (myPropertyInfo.PropertyType.Name.StartsWith("Int"))
myPropertyInfo.SetValue(classInstance, int.Parse(value.ToString()), null);
else
myPropertyInfo.SetValue(classInstance, value, null);
}
}
static private object SetItemsPropertyValueByIndex(object classInstance,
int index, string propertyName, object propertyValue)
{
object[] postedParams = new object[] { index };
Type[] myTypeArr = new Type[1];
myTypeArr.SetValue(typeof(int), 0);
object obj2 = classInstance.GetType().GetProperty("Item",
myTypeArr).GetValue(classInstance, postedParams);
if (string.IsNullOrEmpty(propertyName))
{
}
else if (obj2.GetType().GetProperty(propertyName) != null)
obj2.GetType().GetProperty(propertyName).SetValue(obj2, propertyValue, null);
return obj2;
}
static private object SetItemsPropertyValueByName(object classInstance,
string itemName, string propertyName, object propertyValue)
{
object[] postedParams = new object[] { itemName };
Type[] myTypeArr = new Type[1];
myTypeArr.SetValue(typeof(string), 0);
object obj2 = classInstance.GetType().GetProperty("Item",
myTypeArr).GetValue(classInstance, postedParams);
if (obj2 != null)
{
if (string.IsNullOrEmpty(propertyName))
{
}
else if (obj2.GetType().GetProperty(propertyName) != null)
obj2.GetType().GetProperty(propertyName).SetValue(obj2, propertyValue, null);
}
return obj2;
}
public class LanguageDesc
{
public string FileName { get; set; }
public string Language;
public string DisplayName { get; set; }
public string Version;
public string Company;
public string Author;
public DateTime CreateTime;
public DateTime ModifyTime;
}
public LanguageSetter(string path)
{
if (!Directory.Exists(path))
throw new Exception("Path not exists: " + path);
_LangPath = path;
}
public LanguageDesc[] EnumLanguages(string path)
{
List<LanguageDesc> result = new List<LanguageDesc>();
XmlDocument xmlDoc = new XmlDocument();
DirectoryInfo dInfo = new DirectoryInfo(path);
foreach (FileInfo fi in dInfo.GetFiles("*.xml"))
{
LanguageDesc lDesc = new LanguageDesc();
result.Add(lDesc);
lDesc.FileName = fi.FullName;
xmlDoc.Load(fi.FullName);
XmlElement root = xmlDoc.DocumentElement;
XmlNode xNode = root.SelectSingleNode("language");
if (xNode != null) lDesc.Language = XmlHelper.GetNodeText(xNode);
xNode = root.SelectSingleNode("displayName");
if (xNode != null) lDesc.DisplayName = XmlHelper.GetNodeText(xNode);
xNode = root.SelectSingleNode("version");
if (xNode != null) lDesc.Version = XmlHelper.GetNodeText(xNode);
}
_langFile = result;
return result.ToArray();
}
public LanguageDesc[] EnumLanguages()
{
return EnumLanguages(this._LangPath);
}
public void SetLanguage(string language)
{
string fileName = Path.Combine(_LangPath, language + ".xml");
if (!File.Exists(fileName))
{
if ((_langFile == null) || (_langFile.Count == 0))
{
EnumLanguages();
}
if ((_langFile != null) && (_langFile.Count > 0))
{
fileName = _langFile[0].FileName;
}
if (!File.Exists(fileName))
throw new Exception("Cannot find the specified language file " +
"or it's replacement: " + fileName);
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(fileName);
SetLanguage(xmlDoc.DocumentElement);
}
else
{
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(fileName);
SetLanguage(xmlDoc.DocumentElement);
}
}
public void SetLanguage(XmlNode xNode)
{
if (xNode == null) return;
_RootNode = xNode;
}
public void UpdateUILanguage(Control BaseControl)
{
if (_RootNode == null) return;
System.Reflection.FieldInfo[] fieldInfo =
BaseControl.GetType().GetFields(System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance);
if (BaseControl is Form)
{
string key = BaseControl.GetType().Name;
XmlNode ctlNode = _RootNode.SelectSingleNode(string.Format("//item[@key='{0}']", key));
if (ctlNode != null)
{
if (!string.IsNullOrEmpty(XmlHelper.GetNodeText(ctlNode)))
SetPropertyValue(BaseControl, "Text", XmlHelper.GetNodeText(ctlNode));
foreach (XmlNode nod1 in ctlNode.SelectNodes("prop"))
{
SetProp(BaseControl, nod1);
}
}
}
foreach (Control ctl in BaseControl.Controls)
{
if (ctl is UserControl)
{
UpdateUILanguage(ctl);
}
}
for (int i = 0; i < fieldInfo.Length; i++)
{
DoUpdateUILanguage(BaseControl, fieldInfo[i]);
}
}
protected virtual bool GetControlKey(Control BaseControl, FieldInfo fieldInfo, out string key)
{
key = string.Empty;
object target = null;
target = fieldInfo.GetValue(BaseControl);
if (target == null) return false;
PropertyInfo pInfo = target.GetType().GetProperty("Name");
if (pInfo == null) return false;
string xx = (string)pInfo.GetValue(target, null);
if (!string.IsNullOrEmpty(xx))
{
key = BaseControl.GetType().Name + SPLIT_CHAR + xx;
return true;
}
return false;
}
public virtual void DoUpdateUILanguage(Control BaseControl, FieldInfo fieldInfo)
{
if (_RootNode == null) return;
string key = string.Empty;
object target = null;
target = fieldInfo.GetValue(BaseControl);
if (GetControlKey(BaseControl, fieldInfo, out key))
{
if ((target != null) && (!string.IsNullOrEmpty(key)))
{
XmlNode ctlNode = _RootNode.SelectSingleNode(string.Format("//item[@key='{0}']", key));
if (ctlNode != null)
{
if (!string.IsNullOrEmpty(XmlHelper.GetNodeText(ctlNode)))
SetPropertyValue(target, "Text", XmlHelper.GetNodeText(ctlNode));
foreach (XmlNode nod1 in ctlNode.SelectNodes("prop"))
{
SetProp(target, nod1);
}
}
}
}
}
private void SetProp(Object target, XmlNode nod1)
{
string nodeText = XmlHelper.GetNodeText(nod1);
if (!string.IsNullOrEmpty(nodeText))
{
SetPropertyValue(target, nod1.Attributes["name"].Value, nodeText);
}
else
{
object x = GetPropertyValue(target, nod1.Attributes["name"].Value);
if (x == null) return;
int i = 0;
XmlNodeList xNodeList = nod1.SelectNodes("prop");
foreach (XmlNode nod2 in xNodeList)
{
string name = "";
if (nod2.Attributes["name"] != null) name = nod2.Attributes["name"].Value;
string propName = "Text";
string itemName = name;
string propValue = XmlHelper.GetNodeText(nod2);
object obj2 = SetItemsPropertyValueByName(x, itemName, propName, propValue);
if (obj2 != null)
{
XmlNodeList xNodeList2 = nod2.SelectNodes("prop");
foreach (XmlNode nod3 in xNodeList2)
SetProp(obj2, nod3);
}
i++;
}
}
}
public object GetPropertyValue(object classInstance, string propertyName)
{
Type myType = classInstance.GetType();
PropertyInfo myPropertyInfo = myType.GetProperty(propertyName,
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (myPropertyInfo == null)
return null;
else
return myPropertyInfo.GetValue(classInstance, null);
}
public string GetValue(string key)
{
if (_RootNode != null)
{
XmlNode xNode = _RootNode.SelectSingleNode(string.Format("//item[@key='{0}']", key));
if (xNode != null)
return XmlHelper.GetNodeText(xNode);
else
return key;
}
else
return key;
}
}
The XML file's structure is like this:

Before the setting is the description:
- language: code representing the language, for example: en-US: us English, zh-CN: simplified
Chinese.
displayName
: shows in the language chooser.
Every item represents a object, the key is a string and the value can be referenced in your program by using
GetValue
method:
private void btnOK_Click(object sender, EventArgs e)
{
MessageBox.Show(this.langSetter.GetValue("You click OK!"));
}
Every control that automatically set by LanguageSetter
must have the save name as key in the XML. There are
two types of top control: form, usercontrol.
Sub controls in the top control must have the parent's name as it's key's prefix, followed by a dot and the control's self name.
Points of Interest
There is a interesting thing of listview: column you set by ID does not effect, you must do it
manually. I spend a lot of time to fix this.
lvRegStatics.Columns[0].Name = "colNumber";
lvRegStatics.Columns[1].Name = "colMonth";
lvRegStatics.Columns[2].Name = "colRegCount";
History
- 26 March 2013 - First publishing, Version 1.0.