Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 31 additions & 148 deletions source/languageHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@
LCID_NONE = 0 # 0 used instead of None for backwards compatibility.

LANGS_WITHOUT_TRANSLATIONS: FrozenSet[str] = frozenset(("en",))
LCIDS_TO_TRANSLATED_LOCALES = {
# Windows maps this to "ku-Arab-IQ", however a translation is added for
# Central Kurdish in localesData.LANG_NAMES_TO_LOCALIZED_DESCS["ckb"]
# and NVDA may drop "Arab-IQ" from this locale to get the language.
1170: 'ckb'
}
"""
Map Windows locale identifiers to language codes.
These are Windows LCIDs that are used in NVDA but are not found in locale.windows_locale.
These have been added when new locales have been introduced to the translation system and
we cannot use the results from the Windows function LCIDToLocaleName.
"""


class LOCALE(enum.IntEnum):
Expand Down Expand Up @@ -104,16 +116,22 @@ def localeNameToWindowsLCID(localeName: str) -> int:

def windowsLCIDToLocaleName(lcid: int) -> Optional[str]:
"""
gets a normalized locale from a lcid
Gets a normalized locale from a Windows LCID.

NVDA should avoid relying on LCIDs in future, as they have been deprecated by MS:
https://docs.microsoft.com/en-us/globalization/locale/locale-names
"""
# Look up a full locale name (language + country)
try:
lang = locale.windows_locale[lcid]
except KeyError:
# Or at least just a language-only locale name
lang=windowsPrimaryLCIDsToLocaleNames[lcid]
if lang:
return normalizeLanguage(lang)
# From the locale.windows_locale in-line code documentation: (#4203)
# This list has been updated to include every locale up to Windows Vista.
# NOTE: this mapping is incomplete.
localeName = locale.windows_locale.get(lcid)
# Check a manual mapping before using Windows to look up the correct LCID locale name.
if not localeName:
localeName = LCIDS_TO_TRANSLATED_LOCALES.get(lcid)
if not localeName:
localeName = winKernel.LCIDToLocaleName(lcid)
if localeName:
return normalizeLanguage(localeName)


def getLanguageDescription(language: str) -> Optional[str]:
Expand Down Expand Up @@ -309,22 +327,10 @@ def getWindowsLanguage():
"""
windowsLCID=ctypes.windll.kernel32.GetUserDefaultUILanguage()
localeName = windowsLCIDToLocaleName(windowsLCID)
if not localeName:
# #4203: some locale identifiers from Windows 8 do not exist in Python's list.
# Therefore use windows' own function to get the locale name.
# Eventually this should probably be used all the time.
bufSize=32
buf=ctypes.create_unicode_buffer(bufSize)
dwFlags=0
try:
ctypes.windll.kernel32.LCIDToLocaleName(windowsLCID,buf,bufSize,dwFlags)
except AttributeError:
pass
localeName=buf.value
if localeName:
localeName=normalizeLanguage(localeName)
else:
localeName="en"
if localeName:
localeName = normalizeLanguage(localeName)
else:
localeName = "en"
return localeName


Expand Down Expand Up @@ -484,126 +490,3 @@ def useImperialMeasurements() -> bool:
return buf.value == '1'


# Map Windows primary locale identifiers to locale names
# Note these are only primary language codes (I.e. no country information)
# For full locale identifiers we use Python's own locale.windows_locale.
# Generated from: {x&0x3ff:y.split('_')[0] for x,y in locale.windows_locale.iteritems()}
windowsPrimaryLCIDsToLocaleNames={
1:'ar',
2:'bg',
3:'ca',
4:'zh',
5:'cs',
6:'da',
7:'de',
8:'el',
9:'en',
10:'es',
11:'fi',
12:'fr',
13:'he',
14:'hu',
15:'is',
16:'it',
17:'ja',
18:'ko',
19:'nl',
20:'nb',
21:'pl',
22:'pt',
23:'rm',
24:'ro',
25:'ru',
26:'sr',
27:'sk',
28:'sq',
29:'sv',
30:'th',
31:'tr',
32:'ur',
33:'id',
34:'uk',
35:'be',
36:'sl',
37:'et',
38:'lv',
39:'lt',
40:'tg',
41:'fa',
42:'vi',
43:'hy',
44:'az',
45:'eu',
46:'wen',
47:'mk',
50:'tn',
52:'xh',
53:'zu',
54:'af',
55:'ka',
56:'fo',
57:'hi',
58:'mt',
59:'sms',
60:'ga',
62:'ms',
63:'kk',
64:'ky',
65:'sw',
66:'tk',
67:'uz',
68:'tt',
69:'bn',
70:'pa',
71:'gu',
72:'or',
73:'ta',
74:'te',
75:'kn',
76:'ml',
77:'as',
78:'mr',
79:'sa',
80:'mn',
81:'bo',
82:'cy',
83:'kh',
84:'lo',
86:'gl',
87:'kok',
90:'syr',
91:'si',
93:'iu',
94:'am',
95:'tmz',
97:'ne',
98:'fy',
99:'ps',
100:'fil',
101:'div',
104:'ha',
106:'yo',
107:'quz',
108:'ns',
109:'ba',
110:'lb',
111:'kl',
120:'ii',
122:'arn',
124:'moh',
126:'br',
128:'ug',
129:'mi',
130:'oc',
131:'co',
132:'gsw',
133:'sah',
134:'qut',
135:'rw',
136:'wo',
140: 'gbz',
1170: 'ckb',
1109: 'my',
1143: 'so',
9242: 'sr',
}
26 changes: 23 additions & 3 deletions source/winKernel.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2006-2021 NV Access Limited, Rui Batista, Aleksey Sadovoy, Peter Vagner,
# Copyright (C) 2006-2022 NV Access Limited, Rui Batista, Aleksey Sadovoy, Peter Vagner,
# Mozilla Corporation, Babbage B.V., Joseph Lee, Łukasz Golonka
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

"""Functions that wrap Windows API functions from kernel32.dll and advapi32.dll"""

from typing import Union
from typing import Optional, Union
import contextlib
import ctypes
import ctypes.wintypes
from ctypes import byref, c_byte, POINTER, sizeof, Structure, windll, WinError
from ctypes.wintypes import BOOL, DWORD, HANDLE, LARGE_INTEGER, LPWSTR, LPVOID, WORD
from ctypes.wintypes import BOOL, DWORD, HANDLE, LARGE_INTEGER, LCID, LPWSTR, LPVOID, WORD

kernel32=ctypes.windll.kernel32
advapi32 = windll.advapi32
Expand Down Expand Up @@ -479,3 +479,23 @@ def SetThreadExecutionState(esFlags):
if not res:
raise WinError()
return res


def LCIDToLocaleName(windowsLCID: LCID) -> Optional[str]:
# NVDA cannot run with this imported at the top level
from logHandler import log
bufSize = 32
Comment thread
feerrenrut marked this conversation as resolved.
Outdated
buf = ctypes.create_unicode_buffer(bufSize)
dwFlags = 0
try:
kernel32.LCIDToLocaleName(windowsLCID, buf, bufSize, dwFlags)
except AttributeError:
# This exception was previously passed.
# More information is needed on the cause of this exception
# for improving this comment or handling.
log.exception("Unexpected AttributeError raised:", exc_info=True)
return None
# An empty string is returned for unknown LCIDs
if not buf.value:
return None
Comment thread
feerrenrut marked this conversation as resolved.
return buf.value
6 changes: 3 additions & 3 deletions tests/unit/test_languageHandler.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# A part of NonVisual Desktop Access (NVDA)
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# Copyright (C) 2017-2021 NV Access Limited, Łukasz Golonka
# Copyright (C) 2017-2022 NV Access Limited, Łukasz Golonka

"""Unit tests for the languageHandler module.
"""

import unittest
import languageHandler
from languageHandler import LCID_NONE, windowsPrimaryLCIDsToLocaleNames
from languageHandler import LCID_NONE, LCIDS_TO_TRANSLATED_LOCALES
from localesData import LANG_NAMES_TO_LOCALIZED_DESCS
import locale
import ctypes
Expand All @@ -29,7 +29,7 @@ def generateUnsupportedWindowsLocales():
LCID_ENGLISH_US = 0x0409
UNSUPPORTED_WIN_LANGUAGES = generateUnsupportedWindowsLocales()
TRANSLATABLE_LANGS = set(l[0] for l in languageHandler.getAvailableLanguages()) - {"Windows"}
WINDOWS_LANGS = set(locale.windows_locale.values()).union(windowsPrimaryLCIDsToLocaleNames.values())
WINDOWS_LANGS = set(locale.windows_locale.values()).union(LCIDS_TO_TRANSLATED_LOCALES.values())


class TestLocaleNameToWindowsLCID(unittest.TestCase):
Expand Down