The Wiert Corner – irregular stream of stuff

Jeroen W. Pluimers on .NET, C#, Delphi, databases, and personal interests

  • My badges

  • Twitter Updates

  • My Flickr Stream

  • Pages

  • All categories

  • Enter your email address to subscribe to this blog and receive notifications of new posts by email.

    Join 4,259 other subscribers

Delphi: a few short notes on LoadString and loading shell resource strings for specific LCIDs

Posted by jpluimers on 2014/07/17

I’m not a real expert on LCID (the values like 1033 (aka 0x409 or $409) and 1043 (aka 0x413 or $413), but here are a few notes on stuff that I wrote a while ago to obtain shell32.dll resource strings for various LCIDs.

The most often used way to load resource strings is by calling the LoadString Windows API call which loads the string for the currently defined LCID.

To get them for a different LCID, there are two ways:

  1. Set the LCID for the current thread (don’t forget to notify the Delphi RTL you did this, and update FormatSettings)
  2. Write an alternative for LoadString that gets a string for a specific LCID (so you can keep the current thread in a different LCID)

The first method – altering the LCID of the current thread – is done using SetThreadLocale in Windows XP or earlier, and SetThreadUILanguage in Windows Vista/2008 and up (I’m not sure on the timeline of Windows Server versions, but I guess the split is between 2003 and 2008) as mentioned at SetThreadLocale and SetThreadUILanguage for Localization on Windows XP and Vista | words.

SetThreadLocale is deprecated, as Windows has started switching from LCID to Locale Names. This can cause odd behaviour in at least Delphi versions 2010, XE and XE2. See the answers at delphi – GetThreadLocale returns different value than GetUserDefaultLCID? for more information.

But even on XP it has the potential drawback of selecting language ID 0 (LANG_NEUTRAL) which selects the English language if it is available (as that is in the default search order). Both Writing Win32 Multilingual User Interface Applications and the answers to LoadString works only if I don’t have an English string table and Windows skipping language-specific resources and the Embarcadero Discussion Forums: How to load specific locale settingsd thread that describe this behaviour.

To work around that, you can do two things: store your resource strings in locale dependent DLLs, or (if you don’t write those DLLs yourself), write an alternative for LoadString.

I’ve done the latter for Delphi, so I could load strings for a specific LCID from the Shell32.dll.

For a full overview of all these strings, see http://www.angelfire.com/space/ceedee/shell32stringtables.txt

A few pieces of code.

You can get the full code at the BeSharp – Source Code Changeset 100520 (now at bitbucket too).

First a wrapper around LoadString that returns a Delphi String: pretty basic stuff like most Windows API wrappers returning a string with a character buffer and a “SetString(Result, …)” construct.

class function TStringResources.LoadString(const hInstance: HMODULE; const uId: UINT): string;
var
  Buffer: array [0 .. 1023] of char;  // reasonable length; might increase for really long resource strings.
  StringLength: Integer;
begin
  StringLength := Winapi.Windows.LoadString(hInstance, uId, Buffer, Length(Buffer));
  SetString(Result, Buffer, StringLength);
end;

Now the functions that loads the string for a specific ID and a specific LCID.

This one is a bit complex, and based on these posts (some as old as 2004):

Within the bucket, each string consists of a 2 byte length, followed (if length is bigger than zero) by length number of 2-byte characters.

A few more remarks that have nothing to do with STRINGTABLE, but more about how anciant 16-bit Windows API calls translate into the modern world:

  • LockResource is not a lock, but translates a handle into a memory pointer.
  • UnlockResource is a NOP in Win32, so no need to check result and perform RaiseLastOSError();
  • FreeResource in Win32 will return false, so no need to check result and perform RaiseLastOSError();
class function TStringResources.FindStringResourceEx(const hInstance: HMODULE; const uId, langId: UINT): string;
const
  StringsPerBucket = 16;
var
  BucketWideCharsPointer: LPCWSTR;
  BucketResourceHandle: HRSRC;
  BucketGlobalHandle: HGLOBAL;
  BucketPointer: Pointer;
  i: UINT;
  BucketNumber: Cardinal;
  BucketIntResource: PWideChar;
  IndexInBucket: UINT;
  StringLengthPointer: PWord;
  StringLength: Word;
begin
  Result := ''; // assume failure
  // Convert the string ID into a bundle number
  BucketNumber := uId div StringsPerBucket + 1;
  BucketIntResource := MAKEINTRESOURCE(BucketNumber);
  BucketResourceHandle := FindResourceEx(hInstance, RT_STRING, BucketIntResource, langId);
  if (BucketResourceHandle <> 0) then
  begin
    BucketGlobalHandle := LoadResource(hInstance, BucketResourceHandle);
    if (BucketGlobalHandle <> 0) then
    begin
      BucketPointer := LockResource(BucketGlobalHandle);
      BucketWideCharsPointer := LPCWSTR(BucketPointer);
      if (BucketWideCharsPointer <> nil) then
      begin
        // okay now walk the string table
        IndexInBucket := (uId and (StringsPerBucket - 1));
        for i := 1 to IndexInBucket do // skip n-1 entries
        begin
          StringLengthPointer := PWord(BucketWideCharsPointer);
          StringLength := StringLengthPointer^;
          Inc(BucketWideCharsPointer); // skip the length Word
          Inc(BucketWideCharsPointer, StringLength); // skip the content
        end;
        StringLengthPointer := PWord(BucketWideCharsPointer);
        StringLength := StringLengthPointer^;
        Inc(BucketWideCharsPointer); // skip the length Word
        if StringLength <> 0 then
          Result := Copy(BucketWideCharsPointer, 1, StringLength);
        UnlockResource(BucketGlobalHandle)
      end;
      FreeResource(BucketGlobalHandle);
    end;
  end;
end;

Finally a function that tries to find an ID for a string and a specific LCID.

The logic is pretty simple: try all IDs, and find a string that matches the ID. If it matches, return that ID.

class function TStringResources.FindResourceStringId(const resource_handle : HMODULE; const search_resource_string: string; const langId: UINT): UINT;
var
  resource_id: UINT;
  i: Word;
  resource_string: string;
  compare_string: string;
begin
  resource_id := High(resource_id);
  for i := Low(i) to High(i) do
  begin
    resource_string := FindStringResourceEx(resource_handle, i, langId);
    compare_string := Copy(resource_string, Length(search_resource_string));
    if (resource_string <> '') and (SameStr(resource_string, compare_string))
    then
      resource_id := i;
  end;
  Result := resource_id;
end;

Finally a few notes on LCIDs, not the least so see how scattered the information is:

  1. You can add an Excel specific LCID (locale identifier) to a format. Without it, it will use the user’s locale settings.
  2. You can ommit the language hint (like [ENG]) from the formatting.
  3. The Excel LCID is very similar to the LCID Structure using hexadecimal values from the (old now defunct Locale ID Chart and replaced by the new) Microsoft Locale ID Values,  Language Identifier Constants and Strings table or list of Locale IDs Assigned by Microsoft, but with a few twists.
  4. A Locale ID consists of 4 bytes: 16 bits for the Language ID, 4 bits for the Sort ID, and 12 unused bits: Locale
    Identifiers (Windows)
    .
    The MAKELCID macro takes the Language ID and Sort ID to create a LCID.
    The LANGIDFROMLCID macro and SORTIDFROMLCID macro extracts the 2 IDs from the LCID.
  5. The language itself consists of a primary language (10 bits) and a sublanguage (5 bits) as shown in Language Identifiers (Windows).
    You assemble them using the MAKELANGID macro.
    The PRIMARYLANGID macro and SUBLANGID macro brings you back the parts.
    A list of primary and sublanguages is at Language Identifier Constants and Strings (Windows).
  6. The LCID structure (it actually is a 4 byte structure containing a Sort ID and a LanguageID) is explained at 2.2 LCID Structure together with list of Sort IDs and Language IDs.
  7. A list of LCID and supported operating systems (from Windows NT 3.51 until Windows 8) is at 5 Appendix A: Product Behavior with columns Language, Location (or type), LCID, Name and Supported Windows/ELK Version.
  8. A 2005 list of Microsoft Locale ID Values.
  9. Locale IDs Assigned by Microsoft.
  10. Table of LCID, language name, etc for various versions of Windows (7, Vista, 2003, XP) at National Language Support (NLS) API Reference.
  11. LCIDs supported on Older Versions of Windows (Windows 2000 back to DOS 6.x).

And one more: if you really like i18n, L10n and such: read Michael Kaplan’s blog.

Edit: Michael’s MSDN blog is officially dead, but there are the nice web archive and web cache virtues:

–jeroen

4 Responses to “Delphi: a few short notes on LoadString and loading shell resource strings for specific LCIDs”

  1. […] while ago, Tim mentioned that Michael Kaplan’s blog “Sorting it All Out” on MSDN was […]

  2. Tim said

    You cannot have discovered that Michael Kaplan’s blog was shut down, against his wishes, earlier this year. The link you gave will therefore not find it. Luckily even those who did this do not have the ability similarly to censor the Internet archive.

    See http://sortingtherestallout.blogspot.co.uk/2014/02/sorting-it-all-out-is-officially-dead.html

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.