Changing CurrentUICulture at runtime in a Localizable Form in WinForms

3.5k views Asked by At

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/library/system.threading.thread.currentuiculture(v=vs.110).aspx

This is to set the language of the form, but this needs to be set before we instantiate the form. This cannot be called after this event.

Searching for information, I have seen the following question: https://stackoverflow.com/a/11738932/3286975 but, as a comment said, I have controls inside of a TabControl and a MenuStrip, so they aren't affected.

I have tried to modify this, by getting all the controls of the Form without luck.

enter image description here

enter image description here

In this menu I call the following callback:

    private void englishToolStripMenuItem_Click_1(object sender, EventArgs e)
    {
        string lang = (string) ((ToolStripMenuItem) sender).Tag;
        base.Culture = CultureInfo.CreateSpecificCulture(lang);
    }

    private void spanishToolStripMenuItem_Click(object sender, EventArgs e)
    {
        string lang = (string) ((ToolStripMenuItem) sender).Tag;
        base.Culture = CultureInfo.CreateSpecificCulture(lang);
    }

I change the Culture by using the Tag.

When I click it nothing happens. Also, I have modified a little bit the ApplyResources method from the mentioned answer.

private void ApplyResources(Control parent, CultureInfo culture)
{
        this.resManager.ApplyResources(parent, parent.Name, culture);

        foreach (Control ctl in parent.IterateAllChildren())
        {
            //this.ApplyResources(ctl, culture);
            this.resManager.ApplyResources(ctl, ctl.Name, culture);
        }
}

Where IterateAllChildren is the following: https://stackoverflow.com/a/16725020/3286975

Also, I tried with (System.LINQ): Controls.OfType<Label>() (because I have one Label to test this) without luck...

enter image description here

enter image description here

But when I select the Spanish language, no text is changed.

So maybe, I'm failling with the childrens. Or maybe by calling the method CreateCulture, I don't know.

Thanks in advance!

EDIT:

I have tested to get the Resource Manager of my form by the Culture Info and it returns the default one everytime:

 ResourceSet resourceSet = new ResourceManager(typeof(frmCredentials)).GetResourceSet(new CultureInfo(lang), true, true);
            foreach (DictionaryEntry entry in resourceSet)
            {
                string resourceKey = entry.Key.ToString();
                object resource = entry.Value; //resourceSet.GetString(resourceKey);
                if (resource.GetType().Equals(typeof(string)))
                    Console.WriteLine("Key: {0}\nValue: {1}\n\n", resourceKey, (string) resource);
            }

Where new CultureInfo(lang), I haved tested also: new CultureInfo("es") & Thread.CurrentThread.CurrentCulture (CurrentUICulture) without luck. Is like it never exists or is replaced, but in my design and file explorer I can see the files.

EDIT2:

Maybe is because I'm using ILMerge to merge all dlls in a unique one. I'm reviewing this: Single-assembly multi-language Windows Forms deployment (ILMerge and satellite assemblies / localization) - possible?

Reply to EDIT2:

Yep, deleting ILMerge the problem is solved, and the first solution I gave resolves this. But for some reason, Spanish language is taken as the default language, and when I tried to get the resourceset from it, it didn't return me nothing.

Aso, I have set the Localizable attribute to false, and it didn't created a default resx file with values. I don't know if this is a good practice.

I will try something new...

3

There are 3 answers

0
z3nth10n On BEST ANSWER

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!

5
Reza Aghaei On

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:

2
Fabio On

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.