What the %$#& is up with localized paths in Vista?

Sorting it all Out
Michael Kaplan's random stuff of dubious value
Be sure to read the disclaimer here first!

What the %$#& is up with localized paths in Vista?

  • Comments 32

You know, South Park was just on, it was the one where they wind up taking a bus to China for the world dodgeball championship. I was trying to decide whether it was a good thing or not that I recognized 野種 on the banner in the back of the gym even though I know very little Chinese, and then I wondered whether KTSW really had cut the scene that had the banners in it or whether I had missed it.

I'm not sure why, but it got me thinking about a question from the Suggestion Box, where ph_arnaud asked:

Hi,

How are applications running on localized Vista supposed to show filepath names to the user with localized folder names?

For example:

At least in German Vista Ultimate and Home, a dir in command prompt shows:

    C:\Program Files
    C:\Users
    C:\Windows

(not showing hidden or system items).

Windows Explorer shows:

    C > Benutzer
    C > Programme
    C > Windows

If UI shows a confirmation message with a path name say to some user application data file using:

    SHGetFolderPath ( CSIDL_MYPICTURES )

this function returns:

    C:\Users\admin\Pictures

(I had hoped this would return the localized name, then all legacy code would show localized UI strings on Vista without more effort... and couldn't the hidden 'junctions' folders actually make the path a valid one?  well a digression, it doesn't seem to work that way...)

Is this purpose of SHGetLocalizedName(), to convert a path such as returned by SHGetFolderPath to something the user would understand browsing with Windows Explorer?

    C:\Betnutzer\admin\Bilder

I've tried using SHGetLocalizedName on the result of SHGetFolderPath but get an error (GetLastError returns 'Invalid window handle')

Is it worth trying to figure this out, because this is the solution to my question, or is another approach (different API function better)?

I would expect this would confuse average users, who don't use the command line, to see English folder names coming from their Vista applications, while Explorer shows localized names for directories.

However I noticed that German WordPad and Paint show the English names of directories in the MRU list in German Vista, which doesn't match my expectation for consistent folder names.

thanks for your insight on this.

Well, first let's put together a call to SHGetLocalizedName that works, just for grins so we know what it does (and that it can do it!):

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

namespace Testing {
    class TestGetLocalizedName {
        [DllImport("shell32.dll", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)]
        internal static extern int SHGetLocalizedName(string pszPath, StringBuilder pszResModule, ref int cch, out int pidsRes);

        [DllImport("user32.dll", EntryPoint="LoadStringW", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)]
        internal static extern int LoadString(IntPtr hModule, int resourceID, StringBuilder resourceValue, int len);

        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, EntryPoint = "LoadLibraryExW")]
        internal static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hFile, uint dwFlags);

        internal const uint DONT_RESOLVE_DLL_REFERENCES = 0x00000001;
        internal const uint LOAD_LIBRARY_AS_DATAFILE = 0x00000002;

        [DllImport("kernel32.dll", ExactSpelling = true)]
        internal static extern int FreeLibrary(IntPtr hModule);

        [DllImport("kernel32.dll", EntryPoint="ExpandEnvironmentStringsW", CharSet=CharSet.Unicode, ExactSpelling=true)]
        internal static extern uint ExpandEnvironmentStrings(string lpSrc, StringBuilder lpDst, int nSize);

        [STAThread]
        static void Main(string[] args) {
            if(args.Length > 0) {
                for(int i=0; i < args.Length; i++) {
                    StringBuilder sb = new StringBuilder(500);
                    int len, id;
                    len = sb.Capacity;

                    if(SHGetLocalizedName(args[i], sb,  ref len, out id) == 0) {
                        Console.Write("Resource is in: \"");
                        Console.Write(sb.ToString());
                        Console.Write("\"; ID to load is: ");
                        Console.WriteLine(id);
                        ExpandEnvironmentStrings(sb.ToString(), sb, sb.Capacity);
                        IntPtr hMod = LoadLibraryEx(sb.ToString(), IntPtr.Zero, DONT_RESOLVE_DLL_REFERENCES | LOAD_LIBRARY_AS_DATAFILE);
                        if(hMod != IntPtr.Zero) {
                            if(LoadString(hMod, id, sb, sb.Capacity) != 0) {
                                Console.Write("which for ");
                                Console.Write(CultureInfo.CurrentUICulture.Name);
                                Console.Write(" is: ");
                                Console.WriteLine(sb.ToString());
                            }
                            FreeLibrary(hMod);
                        }
                        Console.WriteLine("\r\n");
                    } else {
                        Console.Write("The path ");
                        Console.Write(args[i]);
                        Console.Write(" is not localized.");
                        Console.WriteLine("\r\n");
                    }
                }
            }
        }
    }
}

Ok, just a silly little console app that you can pass a bunch of paths to, and it will return some stuff about them. Like here is what it returned for me when I switched my user interface language to French:

E:\>GetLocalizedNameTest.EXE "E:\Program Files" "E:\Users"
Resource is in: "%SystemRoot%\system32\shell32.dll"; ID to load is: 21781
which for fr-FR is: Programmes

Resource is in: "%SystemRoot%\system32\shell32.dll"; ID to load is: 21813
which for fr-FR is: Utilisateurs

Now anything that returns failure you can just use the path you had (though I noticed the function fails with paths that have trailing backslashes, which seems like a bug to me, and paths without a localized name, which does not, and mixed paths that have localized and nolocalized elements, which does since the function is documented as requiring full paths).

Ok, so it works. Kind of (note that the mixed path case is the one that ph_arnaud was running into).

Though it does leave a person wondering how they are supposed to get the full path name, doesn't it? It must work, since (after all) it works for the Shell. It is just not obvious how. Hmmm....

Never mind, let's move to the rest of the question, since I think otherwise the superficial functional difficulties here might distract us from the fundamental design problems. :-)

Once upon a time, when each language SKU had its own set of paths which might well be different from what they might be on an en-US copy, MUI simply left some of these "SKU-specific items" whose underlying resources did not change when the UI language did, people complained (with good reason, in my opinion) that localized systems were not the same as an MUI system set to that user interface language.

Functions like SHGetFolderPath had the job of giving you a functioning folder you could use in your code (because if you hard coded the folder names they might fail completely!). Their central intent was not so much for nice display (though that worked too) as it was for core functionality of paths resolving at all.

This is an important point so please read the previous paragraph again.

But is this always true? Weren't there some folders which did vary with UI language that this function did show those changes for? Perhaps I am misremembering. I don't have an XP MUI machine right in front of ne to test this on. Hmmm....

Now of course, when every SKU is the same, one has "lost" the "feature" of a command prompt that gave localized names. And one has to decide whether it is most evil to break the ability of SHGetFolderPath to return properly localized paths or whether it is insdtead most evil for it to return paths that will function.

They decided to go for the functionality, it would seem.

Though at the same time some functionality has been lost here in each individual SKUs for there to be a better overall architecture.

For what it is worth, I agree with ph_arnaud that there could have been some better design choices here. And I had trouble getting nested paths that involved both localized and non-localized elements to work properly, which I assume was simply due to the fact that I threw the code together at 11:30pm for a blog post that I was trying to get up around midnight.

(Before I forget, I need to remember to send email to the WMI folks to let them know a bunch of their pinvoke declarations are incorrect!)

I also agree that the inconsistencies are weird, just like they are in this other post.

I'll be talking more about some of the issues here (both the ones problems that were addressed and the problems that have been introduced) in upcoming posts....

 

This post brought to you by  (U+1368, a.k.a. ETHIOPIC PARAGRAPH SEPARATOR)

Comment on the blather
Leave a Comment
  • Please add 8 and 5 and type the answer here:
  • Post
Blog - Comment List
  • So, it might be due to the current state of caffeination of myself, but what is the solution for the mix path case? And is his non-localized element the user name in the path? And if, so, what is the solution to get "Bilder"? Will it be presented in an upcoming post? Will I continue to end sentence in this comment with U+003F a.k.a. QUESTION MARK? No, I guess I will not :)

    Sidenote: Wouldn't it be easier and more readable to use C++ (or C++/CLI) for such samples - the interop declarations add a lot of clutter; maybe syntax highlighting could mitigate it...

  • Actually Björn, that is not at all known or obvious, at the moment. :-)

    I think it was mainly the fact that I was pretty tight for time that led to the language choice for the sample,  though for this case losing the pinvoke declarations actually would have (in my opinion) required me talking more about the functions I was calling that were unfamiliar and their params....

  • Hmm, I'm not really sure about this "feature" actually. (That is, having the C:\Users folder untranslated for everybody, and only "displaying" a translated version)

    I mean, I understand that for MUI and all that, you've got to have SOME physical folder name which may be different to what the user expects, but couldn't that name have been the localized version of "Users" (or whatever) when the OS was being installed (so if I installed in a French language, I'd get a physical folder name of "C:\Utilisateurs").

    You'd still use SHGetLocalizedName to get the real localized name, but since 99% of people would be USING Vista in the same language that they INSTALLED it, most of the time it'd be exactly the same as the physical name.

    As it is now, anywhere I want to display a path, I HAVE to use SHGetLocalizedName (a Vista-only function, of course), otherwise non-English users are going to be rather confused... Isn't it better to be wrong for 1% of users than it is to be wrong for 50% of users (whatever the proportion of non-English-speaking people use Windows)?

    </end-rant> Ah well, I guess the boat as sailed on this one!

  • I have larger issue. Indic does not work on the console :(

    -Pavanaja

  • You have to try the new console for that!

  • Thanks for getting some research and discussion going on this topic.

    I also discovered that the function does seem to work for short paths and with trailing backslash.

    c:\Program Files\ works.  (implemented in a tiny MFC dialog app)

    I am still interested to hear the solution for longer paths, as the average case may be to put in a titlebar a full path to a file, or a message box confirming save/delete/etc, with full long path name - with as you mention a mix of folders with no localized name available and a few folders with localized names.

    For example:

    C:\Users\admin\Pictures\bike.jpg

    Just in case I tried to pass in some paths like:

    Pictures

    %USERPROFILE%\Pictures

    with and without a real filename that exists in that directory, didn't get any productive result.

    This function SHGetLocalizedName seems like it can't work for a full long path, because it in fact returns a 'path'  (dll name, and res id) to find one localized directory name.

    I believe pretty much the same issue existed on the multilingual version of Windows XP (enu with language packs), when switched to a language pack, (Explorer would show some folder names localized, vs true English names in the file system).  But maybe no one really noticed as the ratio of 'full-localized' XP to enu + language pack XP was vastly skewed to full-localized XP as that was the consumer version available to most end-users.

    Looking forward to more info on this topic.

  • > You have to try the new console for that!

    Where is that?

    Rgds,

    Pavanaja

  • It's called PowerShell? You have probably heard tell of it in other blogs; if not a search should be pretty easy....

    (let's get this blog and its comment back on topic, kay?)

  • The behaviour of the SHGetLocalizedName function seems correct to me (although the documentation for it is rubbish - the third parameter can't seem to decide whether it's in or out and the type is wrong). The string "C:\Users\Bloke" specifies a specific directory; that directory does not have a localised name. On the other hand, "C:\Users" specifies a different directory, which perhaps does.

    Isn't the problem here that what we think we want is a function CreateLocalisedPath which maps each path component to its localised value? We could easily write one :)

    Really though, the problem is that this functionality creates a one-way mapping from files to filenames, when most software and users expect bidirectional mappings (even if there are many of them). The user won't be able to type "C:\Utilisateurs", because it doesn't exist. It might even be a different directory! And of course, the vast majority of software won't bother with this particular tax, because the developers won't know about it.

    Windows XP had this problem with the start menu. The default folders had desktop.ini files pointing to localised names. This was terribly confusing to anyone who tried to rename the items on the start menu, and doubly so if their user account's start menu folder lacked desktop.ini but the All Users folder did. "Fortunately", most users never organised their start menu, leading to the new search-based thingy in Vista (which I still consider to be a crime against the organised, but I'm hoping I'll get used to it).

    Of course, junctions wouldn't help because there would need to be an infinite number of them :(

    The real answer must be to use cryptic names like usr and bin to distract people from the issue :)

  • For me the big problem is that this function (while probably useful in the context in which the Shell calls it) is mostly useless for anyone outside of MS (and even for most people within MS). And there is no clear indication of the correct function to be calling here, so there is no hint on where to go from here?

  • Why was it decided that names should be localized? Is it based on the assumption that users will not be able to learn a few foreign words? Personally I find this assumption insulting.

    The abstraction that this function creates is inherently leaky. Users will not be able to use these localized names in contexts where they expect to be able directory names. They will say, “Why is there a ‘buffalo’ caption on the elephant’s cage? Computers are *so* hard to use”. They will learn not to trust the shell.

  • You mean the original decison long before MUI and back when the first localized versions of Windows were being created? When not every user wanted to be required to learn English words, and not everyone at Microsoft wanted to force them to do so?

    This was a decision made more than ten versions ago, so it is hard to pin down exactly why, but it's a little late to chuck it all now, IMHO.

  • BTW: With the new Vista abbreviated names (c:\Documents == c:\Documents and Settings\[user name]\My Documents), it will be more likely for users to enter the path directly in an edit control (falling on the nose) than to select the path using CommonDialog (using a "browse" button after the edit control).

    That entry form (edit control with a "browse button") is pretty much standard.

    Thus for experienced users, they will experience more trouble ;-)

    Will there PLEASE be a thorough manual on how to handle these path mappings correctly, in BOTH directions?

    Christian

  • Hey c2j2, see this post for why the shorter names happened....

  • Thanks, MichKa, actually that's where I have seen them first ;-) But my problem is that the paths are so short that some users will enter them in the path edit box faster as selecting them though a commdlg LoadFile().

    So what happens if a user enters "c:\dokumente\x.txt" in the edit box to save a file in the "c:\Documents and Settings\[user name]\My Documents" folder, as the knows "c:\dokumente" exists (from explorer)?

    "File open error" as the directory does not exist!

    a) my program expects this, creates the directory as needed and writes the file - will it ever be found again (for sure not in c:\Documents and Settings\[user name]\My Documents, but also "c:\dokumente" will not be visible in Explorer, will it? Will there be two paths of the same name, one my new directory and the other pointing to the "old", long-named path?)

    b) creating the directory fails -> It will confuse the user as the directory does exist for the user (he can see it in the Explorer)

    Anyway, the damage is done, and I fear it will cause some support contacts itself... so for compatibility reasons (or whatever reason is given for the localized paths) a lot of developers actually need to make their applications vista-compatible! Duh!

    So all us application developers need to find an easy way for filename conversions, for example SHConvertFSNameToUIName() and SHConvertUINameToFSName()).

    Christian

Page 1 of 3 (32 items) 123