views:

339

answers:

4

The recommended best practice is to set the current culture of the application's thread to enable resource look ups to use the correct language.

Unfortunately, this does not set the culture for any other threads. This is especially a problem for thread-pool threads.

The question is: how is it possible to set enable string resource lookups to be correctly localised from thread pool threads with the least amount of extra plumbing code?


Edit:

The problem is this code which is generated from the string table.

internal static string IDS_MYSTRING {
    get {
        return ResourceManager.GetString("IDS_MYSTRING", resourceCulture);
    }
}

The 'resourceCulture' in this case is not set correctly for the thread pool thread. I could just call 'ResourceManager.GetString("IDS_MYSTRING", correctCulture);' but that would mean losing the benefits of compile time checking that the string exists.

I'm now wondering whether the fix is to change the string table visibility to public and to set the Culture property of all assemblies enumerated using reflection.

+1  A: 

I am using string resources from the insert... resx file and satellite assemblies. Are you sure you are naming your files correctly?

Resource1.resx:

<!-- snip-->
<data name="foo" xml:space="preserve">
    <value>bar</value>
  </data>

Resource1.FR-fr.resx

<--! le snip -->
  <data name="foo" xml:space="preserve">
    <value>le bar</value>
  </data>

Class1.cs :

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Threading;

namespace Frankenstein
{
    public class Class1
    {



        struct LocalizedCallback
        {
            private WaitCallback localized;

            public LocalizedCallback(WaitCallback user)
            {
                var uiCult = Thread.CurrentThread.CurrentUICulture;

                // wrap
                localized = (state) =>
                {
                    var tp = Thread.CurrentThread;
                    var oldUICult = tp.CurrentUICulture;
                    try
                    {
                        // set the caller thread's culture for lookup
                        Thread.CurrentThread.CurrentUICulture = uiCult;

                        // call the user-supplied callback
                        user(state);
                    }
                    finally
                    {
                        // let's restore the TP thread state
                        tp.CurrentUICulture = oldUICult;
                    }
                };

            }

            public static implicit operator WaitCallback(LocalizedCallback me)
            {
                return me.localized;
            }
        }

        public static void Main(string[] args)
        {

            AutoResetEvent evt = new AutoResetEvent(false);
            WaitCallback worker = state =>
            {
                Console.Out.WriteLine(Resource1.foo);
                evt.Set();
            };

            // use default resource
            Console.Out.WriteLine(">>>>>>>>>>{0}", Thread.CurrentThread.CurrentUICulture);
            Console.Out.WriteLine("without wrapper");
            ThreadPool.QueueUserWorkItem(worker);
            evt.WaitOne();
            Console.Out.WriteLine("with wrapper");
            ThreadPool.QueueUserWorkItem(new LocalizedCallback(worker));
            evt.WaitOne();

            // go froggie
            Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("FR-fr");
            Console.Out.WriteLine(">>>>>>>>>>{0}", Thread.CurrentThread.CurrentUICulture);           
            Console.Out.WriteLine("without wrapper");
            ThreadPool.QueueUserWorkItem(worker);
            evt.WaitOne();
            Console.Out.WriteLine("with wrapper");
            ThreadPool.QueueUserWorkItem(new LocalizedCallback(worker));
            evt.WaitOne();
        }
    }
}

Output:

>>>>>>>>>>en-US
without wrapper
bar
with wrapper
bar
>>>>>>>>>>fr-FR
without wrapper
bar
with wrapper
le bar
Press any key to continue . . .

The reason why this works is that the Resource1.Culture property is always set to null, so it falls back to the default (IE Thread.CurrentThread.UICulture).

To prove it, edit the Resource1.Designer.cs file and remove the following attribute from the class:

//[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]

Then set a breakpoint in the Resource.Culture property accessors and the foo property accessors, and startup the debugger.

Cheers, Florian

Florian Doyon
The real problem is with the string table lookups. I've edited the question to make this clear.
Thomas Bratt
I have modified the sample to work with table lookups and it is working on my machine (2.0 and 3.5)...
Florian Doyon
Thanks, I'll give it a go :)
Thomas Bratt
Is it better to just set the Resource1.Culture property?
Thomas Bratt
The applications I am developing for are doing asynchronous communications using sockets, not explicit thread-pool calls. Sorry - could have made that clearer.
Thomas Bratt
No problems, the concept is still the same. I added another answer that just maps the custom handler factory to an AsyncCallback rather than a WaitCallback. The motus operandi is still the same, all you have to do is use this callback factory when you want to register your localized callbacks.Could make it generic with a little bit of effort, but I can't be bothered :D
Florian Doyon
Actually no, this is just what this is doing. But if you need to have several threads supporting different cultures, the Resource1.Culture is static, so shared by all threads. Cheers!
Florian Doyon
I'm not sure it's the answer I'm looking for but worth an upvote for sure :)
Thomas Bratt
Thanks :) I misunderstood the question and the answer is over-engineered. If you only need to support 1 locale for all the threads -> Set your ResourceManager.Culture property and you're good to go! Cheers!
Florian Doyon
A: 

Have you tried accessing Application.CurrentCulture instead of Thread.CurrentThread.CurrentCulture?

Rakesh Gunijan
The real problem is with the string table lookups. I've edited the question to make this clear.
Thomas Bratt
`Application.CurrentCulture` just delegates to `Thread.CurrentThread.CurrentCulture`.
adrianbanks
A: 

Then if it's a socket handler, just redefine the callback type and register your async callback with the localized handler factory like this:

    struct LocalizedAsyncCallback
    {
        private AsyncCallback localized;

        public LocalizedAsyncCallback(AsyncCallback user)
        {
            var uiCult = Thread.CurrentThread.CurrentUICulture;

            // wrap
            localized = (state) =>
            {
                var tp = Thread.CurrentThread;
                var oldUICult = tp.CurrentUICulture;
                try
                {
                    // set the caller thread's culture for lookup
                    Thread.CurrentThread.CurrentUICulture = uiCult;

                    // call the user-supplied callback
                    user(state);
                }
                finally
                {
                    // let's restore the TP thread state
                    tp.CurrentUICulture = oldUICult;
                }
            };

        }

        public static implicit operator AsyncCallback(LocalizedAsyncCallback me)
        {
            return me.localized;
        }
    }

And here's your async socket handler registration boilerplate:

Socket sock;
AsyncCallback socketCallback = result => { };
sock.BeginReceive(buffer, offset,size, flags, new LocalizedAsyncCallback(socketCallback), state);
Florian Doyon
A: 

For anyone attempting this in the future, I've ended up with the following code:

/// <summary>
/// Encapsulates the culture to use for localisation.
/// This class exists so that the culture to use for
/// localisation is defined in one place.
/// Setting the Culture property will change the culture and language
/// used by all assemblies, whether they are loaded before or after
/// the property is changed.
/// </summary>
public class LocalisationCulture
{
    private CultureInfo                 cultureInfo         = Thread.CurrentThread.CurrentUICulture;
    private static LocalisationCulture  instance            = new LocalisationCulture();
    private List<Assembly>              loadedAssemblies    = new List<Assembly>();
    private static ILog                 logger              = LogManager.GetLogger(typeof(LocalisationCulture));
    private object                      syncRoot            = new object();

    private LocalisationCulture()
    {
        AppDomain.CurrentDomain.AssemblyLoad += new AssemblyLoadEventHandler(this.OnAssemblyLoadEvent);

        lock(this.syncRoot)
        {
            foreach(Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
            {
                if(LocalisationCulture.IsAssemblyResourceContaining(assembly))
                {
                    this.loadedAssemblies.Add(assembly);
                }
            }
        }
    }

    /// <summary>
    /// The singleton instance of the LocalisationCulture class.
    /// </summary>
    public static LocalisationCulture Instance
    {
        get
        {
            return LocalisationCulture.instance;
        }
    }

    /// <summary>
    /// The culture that all loaded assemblies will use for localisation.
    /// Setting the Culture property will change the culture and language
    /// used by all assemblies, whether they are loaded before or after
    /// the property is changed.
    /// </summary>
    public CultureInfo Culture
    {
        get
        {
            return this.cultureInfo;
        }

        set
        {
            // Set the current culture to enable resource look ups to
            // use the correct language.

            Thread.CurrentThread.CurrentUICulture = value;

            // Store the culture info so that it can be retrieved
            // elsewhere throughout the applications.

            this.cultureInfo = value;

            // Set the culture to use for string look ups for all loaded assemblies.

            this.SetResourceCultureForAllLoadedAssemblies();
        }
    }

    private static bool IsAssemblyResourceContaining(Assembly assembly)
    {
        Type[] types = assembly.GetTypes();

        foreach(Type t in types)
        {
            if(     t.IsClass
                &&  t.Name == "Resources")
            {
                return true;
            }
        }

        return false;
    }

    private void OnAssemblyLoadEvent(object sender, AssemblyLoadEventArgs args)
    {
        if(!LocalisationCulture.IsAssemblyResourceContaining(args.LoadedAssembly))
        {
            return;
        }

        lock(this.syncRoot)
        {
            this.loadedAssemblies.Add(args.LoadedAssembly);

            this.SetResourceCultureForAssembly(args.LoadedAssembly);
        }
    }

    private void SetResourceCultureForAllLoadedAssemblies()
    {
        lock(this.syncRoot)
        {
            foreach(Assembly assembly in this.loadedAssemblies)
            {
                this.SetResourceCultureForAssembly(assembly);
            }
        }
    }

    private void SetResourceCultureForAssembly(Assembly assembly)
    {
        Type[] types = assembly.GetTypes();

        foreach(Type t in types)
        {
            if(     t.IsClass
                &&  t.Name == "Resources")
            {
                LocalisationCulture.logger.Debug(String.Format( CultureInfo.InvariantCulture,
                                                                "Using culture '{0}' for assembly '{1}'",
                                                                this.cultureInfo.EnglishName,
                                                                assembly.FullName));

                PropertyInfo propertyInfo = t.GetProperty(  "Culture",
                                                            BindingFlags.GetProperty | BindingFlags.Static | BindingFlags.NonPublic);

                MethodInfo methodInfo = propertyInfo.GetSetMethod(true);

                methodInfo.Invoke(  null,
                                    new object[]{this.cultureInfo} );

                break;
            }
        }
    }
}
Thomas Bratt