by Michael S. Kaplan, published on 2007/01/18 00:01 -08:00, original URI: http://blogs.msdn.com/michkap/archive/2007/01/18/1487464.aspx
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)
# Björn on Thursday, January 18, 2007 5:11 AM:
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...
# Michael S. Kaplan on Thursday, January 18, 2007 7:35 AM:
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....
# Dean Harding on Thursday, January 18, 2007 7:53 PM:
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!
# Pavanaja U B on Friday, January 19, 2007 12:39 AM:
I have larger issue. Indic does not work on the console :(
-Pavanaja
# Michael S. Kaplan on Friday, January 19, 2007 1:59 AM:
You have to try the new console for that!
# ph_arnaud on Friday, January 19, 2007 10:48 AM:
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.
# Pavanaja UB on Friday, January 19, 2007 10:18 PM:
> You have to try the new console for that!
Where is that?
Rgds,
Pavanaja
# Michael S. Kaplan on Saturday, January 20, 2007 12:48 AM:
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?)
# James S. on Saturday, January 20, 2007 4:14 PM:
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 :)
# Michael S. Kaplan on Monday, January 22, 2007 2:38 AM:
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?
# Centaur on Monday, January 22, 2007 4:23 AM:
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.
# Michael S. Kaplan on Monday, January 22, 2007 5:15 AM:
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.
# c2j2 on Monday, January 22, 2007 5:27 AM:
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
# Michael S. Kaplan on Monday, January 22, 2007 5:41 AM:
Hey c2j2, see this post for why the shorter names happened....
# c2j2 on Monday, January 22, 2007 7:34 AM:
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
# Michael S. Kaplan on Monday, January 22, 2007 8:07 AM:
Hi Christian,
You are actually in your argument using two entirely unrelated issues to try to argue against the problems of the one bring discussed here. We have:
1) The top level, virtual "My Documents" which maps to the longer path -- a feature that has been around since at least Win2000 and has not led to a huge number of people creating phatom new directories. I doubt thast renasming it will change this.
2) The localized path issue beung discussed here.
I would argue that the design of a textbox you type into is a mistake and I ususlly disable the textbox and force them to go through the fileopen dialog. I would recommend that you do the same here, frankly, as this works round 90% of the issues that concern you? :-)
# c2j2 on Monday, January 22, 2007 9:13 AM:
Hi MichKa,
What you describe (or I assume you describe) is correct, that these folders have been around a long time. I cannot create a "my documents\x.txt" as the folder is so pure virtual, it does not reside in my "c:\" root ;-) Try 'echo Hallo ">my documents\x.txt"'. No user can expect these paths in an edit control that expects a filesystem path.
Once he enters a physical (or seemingly physical) path, that is, a path that he "sees" as a subdirectory on the harddisk, it's different.
I mean the shortcut folders you described in the other post. As they are virtual, localized, but on the harddisk's tree, I would expect consitency. I can create a file "x.txt" in "c:\dokumente" in the Explorer, but I cannot in the command shell ('echo Hallo MichKa >c:\dokumente\x.txt") and also not in an application that enables fast copy&paste of paths (which is used by prof\b\b\b\busers who shudder at the slowness and awquardness of having to click to open a dialog to enable them to insert a path thay could insert much faster directly).
I don't have a localized Vista here, only an US RC1, so I cannot test what will happen.
Christian
# Michael S. Kaplan on Monday, January 22, 2007 9:18 AM:
Again Christian , this is 100% unrelated to the localized dirs issue, happens on English too, and has been around since win2000. Please take it to the shell blog as it is their design and has nothing to do with the localized directories.
# Installers? on Monday, January 22, 2007 4:54 PM:
Virtually every installer created thus far presents an 'install location' choice to end users, with an edit box and a Browse button.
Am I correct that this can (and will) happen?
1. User on a French machine installing some English shareware
2. Installer prompts for install location
3. It reads: C:\Program Files\Appname
4. User thinks that it's a non-internationalized installer (with accent, exclaming "stupid Americans!")
5. User changes the path to C:\Programmes\Appname
Now the user has both a C:\Program Files folder and a C:\Programmes folder? Does Vista smart enough to handle this? What happens when they enter C:\Programmes into the address bar? Which folder is loaded?
# ph_arnaud on Tuesday, January 23, 2007 5:42 AM:
Hi,
I don't think you need English shareware to see the difference. You could use French or English designed for XP software, or even designed for Vista...
The German Office 2007 installer doesn't show the installation path in the 'default' installation steps - there are no practically no options and the software is just installed.
Only when you choose 'custom', then it does show the English name 'Program Files' as part of path in an edit box (enabled for typing). There is a browse button next to the edit box, which shows a tree structure with localized directory names. Selecting the localized name, then ok, does not change the content of the edit box, which will still contain the English name 'Program Files'. In the Office 2007 installer if you do replace the 'Program Files' with 'Programme' in the install path edit box, it doesn't seem to be an issue, the files are still really installed to Program Files - probably because of the Programme folder junction that exists and redirects? - and of course in Windows Explorer it appears to exist in Programme, whether you put 'Program Files' or 'Programme' in that install edit box)
Beyond installation (which is one time, and perhaps not seen by all users), is run-time usage.
Word 2007 in some cases appears to avoid showing complete paths in UI. For example on the titlebar for the active document, or save confirmation dialogs, it seems to show just the filename - thus avoiding the issue. I don't have any insight if this is to avoid this English/localized foldername issue, or done for other reasons.
German Word 2007 is not completely able to avoid showing full paths. In some cases like hyperlink name and tooltips, the English directory names, when contained in a path, do display.
# Michael S. Kaplan on Tuesday, January 23, 2007 5:52 AM:
And everything still works, then. Some confusion? Sure. But that is preferred to some stuff being broken....
# Phil Jollans on Tuesday, February 13, 2007 8:05 AM:
Michka, I think localized directory names were a bad idea from the word go. I think that they were introduced in Windows 95, so the design decision was probably about 13 years ago.
You say that it is too late to chuck it now. It seems to me that the basic idea has in fact been chucked. What remains is some cosmetics in the windows explorer.
Localizing the names of directories, which under the surface actually retain their original names, seems so obviously a be bad idea, that the best approach is probably to boycott it. Working with filenames is a basic operation in many applications and the best way to handle it consistently is work with the real filenames.
I cannot imagine the localized aliases being a success and I fully expect Microsoft to abandon them in the following version of Windows.
Phil
# Michael S. Kaplan on Tuesday, February 13, 2007 9:13 AM:
The real question is whether more has to be done other than what has already been done -- why not keep the small amount of cosmetic work? :-)
# i.g. on Thursday, February 15, 2007 4:01 AM:
So, how can we convert
C:\Users\admin\Pictures
into
C:\Betnutzer\admin\Bilder
?
The SHGetLocalizedName() function works only for the first part (i.e. converting C:\Users into C:\Benutzer is easy, just like shown in the example code)... but how can we get the rest?
# Michael S. Kaplan on Thursday, February 15, 2007 8:36 AM:
If I knew, I'd have posted it -- but thus far no one seems to know the answer....
# Ming Zhu on Wednesday, February 21, 2007 1:00 PM:
I am from the Windows Shell team. I have recently come across a customer question with the link to this blog. I can see many confusion here and I feel that I can share the discussion here.
Actually, SHGetLocalizedName API is NOT designed to work with folders. The MSDN document already mentioned that it is used to retrieve localized names for files. Use it on folders has in-consistent result - for some folders it will work but for some it does not.
The official way to get a localized name for a folder is through the IShellItem::GetDisplayName or IShellFolder::GetDisplayNameOf method, it works on both pre-Vista and Vista systems. And, it works for all shell folders as well, include those non-file-sytem folders such as "Control Panel".
Here's some code example:
//
// Get the display name of a folder in the shell namespace according to the pidl
//
HRESULT GetDisplayNameFromPidl(LPCITEMIDLIST pidl, __out_ecount(cchName) LPWSTR pszName, UINT cchName) {
IShellFolder *psf = NULL;
LPCITEMIDLIST pidlRelative = NULL;
HRESULT hr = SHBindToParent(pidl, IID_PPV_ARGS(&psf), &pidlRelative);
if (SUCCEEDED(hr)) {
STRRET sr;
hr = psf->GetDisplayNameOf(pidlRelative, SHGDN_NORMAL, &sr);
if (SUCCEEDED(hr)) {
hr = StrRetToBuf(&sr, pidl, pszName, cchName);
if (FAILED(hr)) {
Trace(Warning, "StrRetToBuf failed, hr = %08X", hr);
}
} else {
Trace(Warning, "GetDisplayNameOf failed, hr = %08X", hr);
}
psf->Release();
} else {
Trace(Warning, "SHBindToParent failed, hr = %08X", hr);
}
return hr;
}
//
// Get the a string form of the PIDL by concatenate the display names of the folders in the PIDL
//
HRESULT GetPidlStringFromPidl(LPCITEMIDLIST pidl, __out_ecount(cchPidlString) LPWSTR pszPidlString, size_t cchPidlString) {
HRESULT hr = S_OK;
// First, get the display name of each level of the pidl, and combine them together.
// Since we used ILRemoveLastID() recursively, the order is actually bottom to top.
WCHAR szReversedString[MAX_PATH] = L"";
LPITEMIDLIST pidlClone = ILClone(pidl);
if (pidlClone) {
while (SUCCEEDED(hr)) {
WCHAR szOneLevel[MAX_PATH];
hr = GetDisplayNameFromPidl(pidlClone, szOneLevel, ARRAYSIZE(szOneLevel));
if (SUCCEEDED(hr)) {
StringCchCat(szReversedString, ARRAYSIZE(szReversedString), L"\\");
StringCchCat(szReversedString, ARRAYSIZE(szReversedString), szOneLevel);
if (!ILRemoveLastID(pidlClone)) {
break;
}
}
}
ILFree(pidlClone);
} else {
hr = E_OUTOFMEMORY;
}
// Now, we are gonna reverse the path in szReverseName, to get a regular top to bottom path
*pszPidlString = 0;
PWSTR pszLast = szReversedString;
while (pszLast) {
pszLast = StrRChr(szReversedString, NULL, L'\\');
if (pszLast) {
*pszLast = 0;
pszLast++;
if (pszLast) {
StringCchCat(pszPidlString, cchPidlString, L"\\");
StringCchCat(pszPidlString, cchPidlString, pszLast);
}
}
}
return hr;
}
You can see the above code works with PIDLs - the shell equivelant of paths. You can get the PIDL from many APIs such as SHGetFolderLocation, ILCreateFromPath etc.
Hope this helps.
# ph_arnaud on Wednesday, February 21, 2007 5:40 PM:
Thanks for the precise example!
Now let's see if we can get some recommendations from the Vista User Experience Guidelines team.
This is worth a topic in there. Do we do this mapping only in 'shell explorer' types of interfaces, or should any path shown in a Windows application (list of paths in compression tools, paths in confirmation dialogs, installation paths, etc.) be mapped to a localized folder paths.
I think they've got a link on their page for feedback...
mailto:winui@microsoft.com?subject=Windows Vista User Experience Guidelines feedback
referenced by