I have been searching about how to change the language of a Form that has the Localizable
attribute set to true.
https://msdn.microsoft.com/en-us/librar
After some tries, I have realized that several things are failing.
I have to say also that all the given help in this question is greatly appreciated, and also that the LocalizedForm snippet is very useful.
The first issue is that I have realized is that the controls that are under a level of more than 1 in the child hierarchy with this solution doesn't work.
And iterating over all the controls is a expensive task. Maybe mine is worst but has less iterations (because I only seach for Controls with Text)
/// <summary>
/// Current culture of this form
/// </summary>
[Browsable(false)]
[Description("Current culture of this form")]
[EditorBrowsable(EditorBrowsableState.Never)]
public CultureInfo Culture
{
get { return this.culture; }
set
{
if (this.culture != value)
{
ResourceSet resourceSet = new ComponentResourceManager(GetType()).GetResourceSet(value, true, true);
IEnumerable<DictionaryEntry> entries = resourceSet
.Cast<DictionaryEntry>()
.Where(x => x.Key.ToString().Contains(".Text"))
.Select(x => { x.Key = x.Key.ToString().Replace(">", "").Split('.')[0]; return x; });
foreach (DictionaryEntry entry in entries)
{
if (!entry.Value.GetType().Equals(typeof(string))) return;
string Key = entry.Key.ToString(),
Value = (string) entry.Value;
try
{
Control c = Controls.Find(Key, true).SingleOrDefault();
c.Text = Value;
}
catch
{
Console.WriteLine("Control {0} is null in form {1}!", Key, GetType().Name);
}
}
this.culture = value;
this.OnCultureChanged();
}
}
}
What I do is the following, first, I search for the ResourceManager, take care! Because here is the second issue and is that if you use CultureInfo.CreateSpecificCulture
instead CultureInfo.GetCultureInfo
in some cases a new culture will be created and default values will be returned (Form1.resx
values instead of Form1.es.resx
values (for example)).
Once we have loaded all the value from the resx file, we iterate over all of them, and we delete the double >> (it appears in some cases) and we get the name of those Controls that only have declared the Text attribute.
The next step is find the Control and replace its Text...
Well, I have a little mess with the derived classes, that's why I created a try-catch system, because, Controls.Find search in all the Derived Classes I would prefer to be a little bit more specific but I don't know how... (That's why I created this question)
With this we haven't to save any object because we won't clear and recreate them.
The main problem here wasn't the way I was doing this, because it was correct. The problem is that Assemblies merged do weird things when you call for example this:
ResourceSet resourceSet = new ComponentResourceManager(GetType()).GetResourceSet(value, true, true);
The value
will be the default... Something like that this Culture doesn't exist, and is because, the merged Assembly can't find the resx file.
So, I will try AppDomain.CurrentDomain.AssemblyResolve
that @ScottChamberlain has suggested to me. Or ILRepack maybe.
Any help for optimization or ideas (in comments) of why this doesn't work will be appreciated!
MVVM (Model - View - ViewModel) approach have some benefits which can be useful in your case.
Create new resource files for languages which you will use for localization. Working with form's own resource files can be little bid tricky because it regenerated every time you make change in the designer - so I think own resource file will be easier to maintain and even share with other forms and even projects.
LocalizationValues.resx // (default english), set Access Modifier to "Internal" or "Public"
"Title": "Title"
"Description": "Description"
LocalizationValues.es.resx
"Title": "Título"
"Description": "Descripción"
Visual Studio generate static class LocalizationValues
with properties as keys of .resx file. So "Title" can be accessed as LocalizationValues.Title
Create "viewmodel" class which represents all texts you are using in localization. Class should implements INotifyPropertyChanged
interface.
public class LocalizationViewModel : INotifyPropertyChanged
{
public string Title
{
get
{
return LocalizationValues.Title;
}
}
public string Description
{
get
{
return LocalizationValues.Description;
}
}
public void SetLanguage(string language)
{
var culture = new CultureInfo(language);
Thread.CurrentThread.CurrentUICulture = culture;
// This is important,
// By raising PropertyChanged you notify Form to update bounded controls
NotifyAllPropertyChanged();
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyAllPropertyChanged()
{
// Passing empty string as propertyName
// will indicate that all properties of viewmodel have changed
NotifyPropertyChanged(string.Empty);
}
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Then in Form bind viewmodel to controls
public partial class YourForm : Form
{
private LocalizationViewModel _viewmodel;
public YourForm()
{
InitializeComponent();
_viewmodel = new LocalizationViewModel();
// Bound controls to correspondent viewmodel's properties
LblTitle.DataBindings.Add("Text", _viewmodel, "Title", true);
LblDescription.DataBindings.Add("Text", _viewmodel, "Description", true);
}
// Menu buttons to change language
private void SpanishToolStripMenuItem_Click(object sender, EventArgs e)
{
_viewmodel.SetLanguage("es");
}
private void EnglishToolStripMenuItem_Click(object sender, EventArgs e)
{
_viewmodel.SetLanguage("en");
}
}
Approach above will provide more benefits then only updating controls. You get clearly separated parts of your application, which can be tested independently from each other.
There are different solutions to solve the problem, including the MVVM that mentioned in other answers. But you can consider some other options as well.
Call ApplyResource on all controls
You can set the currunt UI culture of the current thread and the call ApplyResource
on all controls. To do so you need to create a method to return all controls, then just call ApplyResource
on all controls, for examle:
private void englishToolStripMenuItem_Click(object sender, EventArgs e)
{
SetCulture("en-US");
}
private void persianToolStripMenuItem_Click(object sender, EventArgs e)
{
SetCulture("fa-IR");
}
public void SetCulture(string cultureName)
{
System.Threading.Thread.CurrentThread.CurrentUICulture =
System.Globalization.CultureInfo.GetCultureInfo(cultureName);
var resources = new System.ComponentModel.ComponentResourceManager(this.GetType());
GetChildren(this).ToList().ForEach(c => {
resources.ApplyResources(c, c.Name);
});
}
public IEnumerable<Control> GetChildren(Control control)
{
var controls = control.Controls.Cast<Control>();
return controls.SelectMany(ctrl => GetChildren(ctrl)).Concat(controls);
}
Creating a Text Localization Extender Component
You also can create an extender component that can be used at design-time as well as run-time and assign some resource file and resource keys to controls. This way you can simply switch between languages by changing the current UI culture of the current thread. Just to see an example of the idea, take a look at this post: