Thinking about MUI is making me bipolar

by Michael S. Kaplan, published on 2006/12/16 03:01 -05:00, original URI: http://blogs.msdn.com/b/michkap/archive/2006/12/16/1300320.aspx


Speaking of Open it all up, get out of the way, and then what happens?.... 

There are times while I am at work where I occasionally think about how my emotional state keeps going up and down like crazy. Like first I am really pleassed about something and the next moment I am filled with despair, And then back again. And again.

I wonder whether I might be bipolar.

And then I realize it is just that I am thinking about MUI (Multilingual User Interface), pronounced either EM-YOU-EYE or MOO-EE depending on a constellation of factors worthy of their own blog post!

I'll tell you about what I mean, about the bipolar thing (the pronunciation can be for another day).

As an example, just the other day:

I got depressed that MUI is LCID based rather than name based.

But then I got excited when I was pointed at all the new MUI API functions that are name based.

But then I got depressed when I thought about how CurrentUICulture is thread based in .NET yet session-based in Windows.

But then I got excited when others suggested that although the defaults are session based that there are a whole bunch of thread-specific functions that are now a part of the MUI API.

But then I got depressed when I thought about how the list of available UI languages in Vista is limited to the ones that are installed on the machine.

But then I got excited when Mike (over in MUI test) explained to me that the thread-based functions do not have this limitation -- it only applies to the default for OS resource loading.

But then I got depressed when I realized that custom locales are probably not supported.

But then I got excited when Erik (the MUI dev manager) explained to me that one of the biggest reasons that the push to using names happened in the resource loader was to allow support for custom locales. So that this scenario should work.

So at this point I stoped and then I decided it was time to either

Since I doubt my neurologist would write the Rx, I opted for the second choice. :-)

There is way too much confusion both in the documents and among the various experts here. So let's see once and for all what works in Vista.

Off to the code....

First a moment about SetThreadPreferredUILanguages.

Now SetThreadPreferredUILanguages is a fascinating function. It wants a NULL delimited list of locale names, a list which can be of somewhat arbitrary length. It will basically scroll through the list and set a maximum of ten of them, based on the first ten valid ones it can find in the list. It will return TRUE if at least one was set, and it will give you no indication of what may have failed along the way (you have to call GetThreadPreferredUILanguages to find out what was actually set).

Is it just me or should SetThreadPreferredUILanguages have been named TryToSetThreadPreferredUILanguages or AttemptToSetThreadPreferredUILanguages? :-)

Oh well, at least I know the semantic going in.

That word valid sets off some alarm bells so I decide to make sure my sample uses the new in Vista IsValidLocaleName function so I can make sure I am using the NLS understanding of "valid" locales.

So I start writing some code that will use GetThreadPreferredUILanguages and SetThreadPreferredUILanguages. I decide to write it as managed code so I can be easily registering and unregistering custom cultures (which are also custom locales). That whole NULL delimited list will be a little weird but I'll work it out in the sample.

Here is the code I write, designed for the RTM version of Vista:

using System;
using System.Text;
using System.Globalization;
using System.Runtime.InteropServices;

namespace Testing {

    class TestMUI {
        // Some functions from winnls.h in the Vista Platform SDK
        [DllImport("kernel32.dll", CharSet=CharSet.Unicode, ExactSpelling=true, CallingConvention=CallingConvention.StdCall, SetLastError=true)]
        static extern bool IsValidLocaleName(string lpLocaleName);

        [DllImport("kernel32.dll", CharSet=CharSet.Unicode, ExactSpelling=true, CallingConvention=CallingConvention.StdCall, SetLastError=true)]
        static extern bool GetThreadPreferredUILanguages(
          uint dwFlags, ref int pulNumLanguages, [MarshalAs(UnmanagedType.LPWStr)] string pwszLanguagesBuffer, ref int pcchLanguagesBuffer);

        [DllImport("kernel32.dll", CharSet=CharSet.Unicode, ExactSpelling=true, CallingConvention=CallingConvention.StdCall, SetLastError=true)]
        static extern bool SetThreadPreferredUILanguages(uint dwFlags, string pwszLanguagesBuffer, ref int pulNumLanguages); 

        // Some constants and functions from winnls.h in the Vista Platform SDK
        private const uint MUI_LANGUAGE_ID = 0x4; // Use traditional language ID convention
        private const uint MUI_LANGUAGE_NAME = 0x8; // Use ISO language (culture) name convention
        private const uint MUI_MERGE_SYSTEM_FALLBACK = 0x10; // GetThreadPreferredUILanguages merges in parent and base languages
        private const uint MUI_MERGE_USER_FALLBACK = 0x20; // GetThreadPreferredUILanguages merges in user preferred languages
        private const uint MUI_THREAD_LANGUAGES = 0x40; // GetThreadPreferredUILanguages merges in thread preferred languages
        private const uint MUI_CONSOLE_FILTER = 0x100; // SetThreadPreferredUILanguages takes on console specific behavior
        private const uint MUI_COMPLEX_SCRIPT_FILTER = 0x200; // SetThreadPreferredUILanguages takes on complex script specific behavior
        private const uint MUI_RESET_FILTERS = 0x001; // Reset MUI_CONSOLE_FILTER and MUI_COMPLEX_SCRIPT_FILTER

        [STAThread]
        static void Main(string[] args) {
            CultureInfo ci = CultureInfo.CurrentCulture;

            // A name that will be sure to not conflict! 
            string stCulture = "random-piece-of-crap-locale";

            // Create the replacement and fill it
            CultureAndRegionInfoBuilder carib = new CultureAndRegionInfoBuilder(stCulture, CultureAndRegionModifiers.None);
            carib.LoadDataFromCultureInfo(ci);
            carib.LoadDataFromRegionInfo(new RegionInfo(ci.Name)); 

            // Make sure it is not valid, register it, then make sure it has become valid
            Console.WriteLine("Before register, is '{0}' valid? {1}\r\n", stCulture, IsValidLocaleName(stCulture)); 
            carib.Register(); 
            Console.WriteLine("After register, is '{0}' valid? {1}\r\n", stCulture, IsValidLocaleName(stCulture));

            // Try to set a big weird list of langs and then see what was set
            SetLangs(stCulture); 
            GetLangs();

            // Unregister -- cleanup is important in samples
            CultureAndRegionInfoBuilder.Unregister(stCulture);
        } 

        static void SetLangs(string stCulture) {
            int ulNumLanguages = 0; 
            string[] rgst = new string[10];

            // Create a weird list full of valid culture names and invalid ones, with our special custom
            // locale in the middke between two that are definitely valid.
            rgst[0] = "de-DE-crap\u0000"
            rgst[1] = "en-US-junk\u0000"
            rgst[2] = "fr-CA\u0000"
            rgst[3] = stCulture + '\u0000';
            rgst[4] = "en-AU\u0000"
            rgst[5] = "this-is-an-arbitrary\u0000"
            rgst[6] = "strings-that-are\u0000"
            rgst[7] = "obviously-not-locales\u0000"
            rgst[8] = "randomly-interspersed\u0000"
            rgst[9] = "with-two-that-are\u0000"
            ulNumLanguages = rgst.Length;
            Console.WriteLine(string.Concat(rgst) + "\r\n");
            Console.WriteLine("Did setting up our big list work? {0}, {1} entries.\r\n",
              SetThreadPreferredUILanguages(MUI_LANGUAGE_NAME, string.Concat(rgst), ref ulNumLanguages),
              ulNumLanguages);
        }

        static void GetLangs() {
            int ulNumLanguages = 0;
            int cchLanguagesBuffer = 0;

            // First call with a NULL target to get the size of the list
            if(GetThreadPreferredUILanguages(MUI_LANGUAGE_NAME | MUI_THREAD_LANGUAGES, ref ulNumLanguages, null, ref cchLanguagesBuffer)) {
                if(ulNumLanguages == 0) {
                    Console.WriteLine("No thread preferred UI languages.");
                } else {
                    // Success! Allocate a buffer filled with NULLs and have the MUI function fill it in
                    string st = new string('\u0000', cchLanguagesBuffer);
                    if(GetThreadPreferredUILanguages(MUI_LANGUAGE_NAME | MUI_THREAD_LANGUAGES, ref ulNumLanguages, st, ref cchLanguagesBuffer)) {
                        // Success again! Replace the embedded NULLs with CRLF and dump out the list.
                        st = st.Replace("\u0000", "\u000d\u000a");
                        Console.WriteLine(st);
                    }
                }
            } 
        } 
    }
}

 Ok, looks like the code is all set.

The results, when this code is run, are quite depressing, though. :-(

It turns out that not only are all locales that are not valid according to IsValidLocaleName are also invalid to MUI; I kind of expected that, and it is not the most unreasonable restriction.

But it also turns out that the custom locale entitled "random-piece-of-crap-locale" that I created is also not valid. Because it turns out that in some low-level internal function, MUI is pivoting the potential locale name through an LCID value, and since most custom locales don't have unique LCID values, this small extra check is invalidating the custom locale for MUI.

This means of course that if one sets the user default locale to be a custom locale that you can use that locale for the UI language too; however, the notion of an application whose success or failure entirely depends on a specific user locale is a really, really bad idea. Like Unicode lame list bad.

It means that at least in Vista RTM, one cannot use custom locales in MUI for your unmanaged applications (it works just fine in the unmanaged world, even pre-Vista).

Now I have reported this to the folks on the MUI team and I am optimistic that this awful behavior will be addressed as soon as they can address it. This bug, which breaks not only NLS/MUI parity but also managed/unmanaged parity and the whole intent of a new set of functions in the MUI API.

You should feel free to weigh in with your opinion about this bug if you have an opinion; having the customer point of view never hurts in a triage meeting....

If they can get this one fixed, then MUI won't make me bipolar anymore! :-)

 

This post brought to you by (U+2323, a.k.a. SMILE)


no comments

referenced by

2010/04/29 New for Windows 7: The PROCESS to keep MUI from being THREADbare....

2010/02/09 Doing your own LIPs is not as easy for Windows as for a lady

2010/01/16 Culture: don't have none, won't be none!

2007/08/25 MSKLC keyboard layout names in your own language

2007/08/14 Do what Icon, not what I say, aka MUI is still making me bipolar

2007/06/23 Marshaling your resistance

2007/03/13 Track change (a.k.a. A new job that has a few things in common with the old one)

2007/01/31 Info needed from developers and architects about what they are (or want to be) doing with MUI

2007/01/26 If I were Australian I might say that Steve Jobs and the Klingon emporer were mates?

go to newer or older post, or back to index or month or day