microsoft / node-native-keymap

Provide OS keyboard layout functionality as a nodejs module
MIT License
136 stars 37 forks source link

BCP-47 for keyboard language on Windows #20

Open PastaJ36 opened 4 years ago

PastaJ36 commented 4 years ago

Thanks for this library, it makes cross platform keyboard stuff a lot easier.

I was wondering if it's possible to switch to CurrentInputMethodLanguageTag. It's a little painful to write the conversion from the hex identifiers.

Is there anything holding back from using this API? I'm not too familiar with the WinRT API's. Thanks!

DJm00n commented 2 years ago

You can easily convert HKL/LANG/LCID to language tag with a call to LCIDToLocaleName.

HKL hkl = GetKeyboardLayout();
WORD lgid = LOWORD(hkl);
DWORD lcid = MAKELCID(lgid, SORT_DEFAULT);

// Locale names should follow the BCP47 recommendations and typically
// include language, script, regional variant, and perhaps additional specifiers.
// BCP47 allows some variation, eg: en-US is preferred to en-Latn-US.
WCHAR localeName[LOCALE_NAME_MAX_LENGTH];
LCIDToLocaleName(lcid, localeName, LOCALE_NAME_MAX_LENGTH, 0);

WCHAR name[255];
GetLocaleInfoEx(localeName, LOCALE_SENGLISHDISPLAYNAME, name, 255); // Display name (language + country/region usually) in English, eg "German (Germany)"

WCHAR lang[9];
GetLocaleInfoEx(localeName, LOCALE_SISO639LANGNAME2, lang, 9); // 3 character ISO abbreviated language name, eg "eng"
beldenfox commented 11 months ago

Just ran across this same problem and did some test code. Turns out CurrentInputMethodLanguageTag isn't what you want and the sample code @DJm00n provided isn't either. The Microsoft docs are really not helpful here.

On my Windows 11 system I'm running English with three keyboards installed (U.S. English, French, and German). No matter which keyboard I've chosen the LANGID in the HKL is English and CurrentInputMethodLanguageTag is always en-US. The reliable way to track the keyboard language is to call GetKeyboardLayoutName and parse the resulting hex codes. The LANGID in the HKL only changes if I install another language (say, Hebrew or Japanese) and switch to one of those keyboard layouts.

DJm00n commented 11 months ago

@beldenfox you're right LOWORD(hkl) - will return input language, and it will not distinguish between several installed keyboards under this language. If you need to extract keyboard layout id then GetKeyboardLayoutName() will help. Or you can try to parse HKL by yourself to extract keyboard layout id from it like I did here.

But please, do not try to parse value returned from GetKeyboardLayoutName() - just threat it as KLID (keyboard layout id) string. This is because newer keyboards does not have associated LCID (LCIDs are obsolete and not assigned anymore) and have 0x0c00 (LOCALE_CUSTOM_USER_DEFAULT) value in lower word of their KLID. Here is the list of keyboard layouts with their KLIDs.

If you need to get display name of the keyboard layout then you can get it from registry key HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layouts\{KLID}. This is documented here and implemented here.

Get-WinUserLanguageList PowerShell command can help you understand how this works in modern Windows: image

beldenfox commented 11 months ago

@DJm00n Thanks for writing all this up! I was specifically tasked with querying the current keyboard layout and mapping that to an ISO language code (like "en" or "fr"). It looks like there's no good way of doing that without using undocumented behavior. I will push back on the request. In the end I suspect what the client really wants to know if any right-to-left keyboard layouts are installed in order to enable some specific shortcuts. I should be able to determine that by looking at the HKL list.

DJm00n commented 11 months ago

@beldenfox AFAIK you can only install Arabic keyboards under user language with corresponding ScriptName (seen on my PowerShell screenshot):

image

So to detect RTL language you can do something like this:

HKL hkl = GetKeyboardLayout();

WCHAR localeName[LOCALE_NAME_MAX_LENGTH];
LCIDToLocaleName(MAKELCID(LOWORD(hkl), SORT_DEFAULT), localeName, LOCALE_NAME_MAX_LENGTH, 0);

DWORD value;
GetLocaleInfoEx(localeName, LOCALE_IREADINGLAYOUT | LOCALE_RETURN_NUMBER, (LPWSTR)&value, sizeof(value) / sizeof(WCHAR));
if (value == 1) // see possible values here: https://learn.microsoft.com/windows/win32/intl/locale-ireadinglayout
   return true;
beldenfox commented 11 months ago

@DJm00n I dropped it into my test app and verified that it works correctly for Hebrew on my Win 11 system. So thanks again, you just saved me a lot of work!