Skip to content

Commit 8dde317

Browse files
authored
Merge e429066 into 8d8f606
2 parents 8d8f606 + e429066 commit 8dde317

File tree

8 files changed

+207
-60
lines changed

8 files changed

+207
-60
lines changed

appveyor/scripts/installNVDA.ps1

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,21 @@ $outputDir=$(resolve-path .\testOutput)
1010
$installerLogFilePath="$outputDir\nvda_install.log"
1111
$installerProcess=start-process -FilePath "$nvdaLauncherFile" -ArgumentList "--install-silent --debug-logging --log-file $installerLogFilePath" -passthru
1212
try {
13-
$installerProcess | wait-process -Timeout 180
13+
$installerProcess | wait-process -Timeout 180 -ErrorAction Stop
1414
$errorCode=$installerProcess.ExitCode
1515
} catch {
1616
echo "NVDA installer process timed out"
1717
$errorCode=1
1818
Add-AppveyorMessage "Unable to install NVDA prior to tests."
19+
$installerProcess | Stop-Process -Force
20+
Start-Sleep 33
21+
Get-Content $installerLogFilePath
1922
}
20-
Push-AppveyorArtifact $installerLogFilePath
23+
if (Test-Path -Path $installerLogFilePath){
24+
echo "Log from installer exists"
25+
}
26+
27+
Push-AppveyorArtifact $installerLogFilePath -Verbosity "Normal"
2128
$crashDump = "$outputDir\nvda_crash.dmp"
2229
if (Test-Path -Path $crashDump){
2330
Push-AppveyorArtifact $crashDump -FileName "nvda_install_crash.dmp"

source/core.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,9 @@ def restart(disableAddons=False, debugLogging=False):
156156
sys.argv.remove(paramToRemove)
157157
except ValueError:
158158
pass
159+
for i, arg in list(enumerate(sys.argv)):
160+
if arg.startswith("--lang="):
161+
del sys.argv[i]
159162
options = []
160163
if not hasattr(sys, "frozen"):
161164
options.append(os.path.basename(sys.argv[0]))
@@ -198,8 +201,11 @@ def resetConfiguration(factoryDefaults=False):
198201
log.debug("Reloading config")
199202
config.conf.reset(factoryDefaults=factoryDefaults)
200203
logHandler.setLogLevelFromConfig()
201-
#Language
202-
lang = config.conf["general"]["language"]
204+
# Language
205+
if languageHandler.isLanguageForced():
206+
lang = globalVars.appArgs.language
207+
else:
208+
lang = config.conf["general"]["language"]
203209
log.debug("setting language to %s"%lang)
204210
languageHandler.setLanguage(lang)
205211
# Addons
@@ -413,8 +419,11 @@ def main():
413419
except:
414420
pass
415421
logHandler.setLogLevelFromConfig()
416-
lang = config.conf["general"]["language"]
417422
import languageHandler
423+
if languageHandler.isLanguageForced():
424+
lang = globalVars.appArgs.language
425+
else:
426+
lang = config.conf["general"]["language"]
418427
log.debug(f"setting language to {lang}")
419428
languageHandler.setLanguage(lang)
420429
log.info(f"Windows version: {winVersion.getWinVer()}")
@@ -602,7 +611,7 @@ def handlePowerStatusChange(self):
602611
log.error("Failed to initialize wx locale",exc_info=True)
603612
finally:
604613
# Revert wx's changes to the python locale
605-
languageHandler.setLocale(languageHandler.curLang)
614+
languageHandler.setLocale(languageHandler.getLanguage())
606615

607616
log.debug("Initializing garbageHandler")
608617
garbageHandler.initialize()

source/gui/settingsDialogs.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -713,19 +713,30 @@ def makeSettings(self, settingsSizer):
713713
settingsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer)
714714
self.languageNames = languageHandler.getAvailableLanguages(presentational=True)
715715
languageChoices = [x[1] for x in self.languageNames]
716+
if languageHandler.isLanguageForced():
717+
cmdLangDescription = [
718+
ld[1] for ld in self.languageNames if ld[0] == globalVars.appArgs.language
719+
][0]
720+
languageChoices.insert(
721+
0,
722+
# Translators: Shown for a language which has been provided from the command line
723+
# 'langDesc' would be replaced with description of the given locale.
724+
_("Command line option: {langDesc}").format(langDesc=cmdLangDescription)
725+
)
726+
self.languageNames.insert(0, "FORCED")
716727
# Translators: The label for a setting in general settings to select NVDA's interface language
717728
# (once selected, NVDA must be restarted; the option user default means the user's Windows language
718729
# will be used).
719730
languageLabelText = _("NVDA &Language (requires restart):")
720731
self.languageList=settingsSizerHelper.addLabeledControl(languageLabelText, wx.Choice, choices=languageChoices)
721732
self.bindHelpEvent("GeneralSettingsLanguage", self.languageList)
722733
self.languageList.SetToolTip(wx.ToolTip("Choose the language NVDA's messages and user interface should be presented in."))
723-
try:
724-
self.oldLanguage=config.conf["general"]["language"]
725-
index=[x[0] for x in self.languageNames].index(self.oldLanguage)
726-
self.languageList.SetSelection(index)
727-
except:
728-
pass
734+
self.oldLanguage = config.conf["general"]["language"]
735+
if languageHandler.isLanguageForced():
736+
index = 0
737+
else:
738+
index = [x[0] for x in self.languageNames].index(self.oldLanguage)
739+
self.languageList.SetSelection(index)
729740
if globalVars.appArgs.secure:
730741
self.languageList.Disable()
731742

@@ -884,8 +895,9 @@ def onCopySettings(self,evt):
884895
gui.messageBox(_("Successfully copied NVDA user settings"),_("Success"),wx.OK|wx.ICON_INFORMATION,self)
885896

886897
def onSave(self):
887-
newLanguage=[x[0] for x in self.languageNames][self.languageList.GetSelection()]
888-
config.conf["general"]["language"]=newLanguage
898+
if not languageHandler.isLanguageForced() or self.languageList.GetSelection() != 0:
899+
newLanguage = [x[0] for x in self.languageNames][self.languageList.GetSelection()]
900+
config.conf["general"]["language"] = newLanguage
889901
config.conf["general"]["saveConfigurationOnExit"]=self.saveOnExitCheckBox.IsChecked()
890902
config.conf["general"]["askToExit"]=self.askToExitCheckBox.IsChecked()
891903
config.conf["general"]["playStartAndExitSounds"]=self.playStartAndExitSoundsCheckBox.IsChecked()

source/languageHandler.py

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# A part of NonVisual Desktop Access (NVDA)
2-
# Copyright (C) 2007-2021 NV access Limited, Joseph Lee
2+
# Copyright (C) 2007-2021 NV access Limited, Joseph Lee, Łukasz Golonka
33
# This file is covered by the GNU General Public License.
44
# See the file COPYING for more details.
55

@@ -18,7 +18,7 @@
1818
import globalVars
1919
from logHandler import log
2020
import winKernel
21-
from typing import Optional
21+
from typing import List, Optional, Tuple
2222

2323
#a few Windows locale constants
2424
LOCALE_USER_DEFAULT = 0x400
@@ -34,8 +34,6 @@
3434
#: or because it is not a legal locale name (e.g. "zzzz").
3535
LCID_NONE = 0 # 0 used instead of None for backwards compatibility.
3636

37-
curLang="en"
38-
3937

4038
class LOCALE(enum.IntEnum):
4139
# Represents NLS constants which can be used with `GetLocaleInfoEx` or `GetLocaleInfoW`
@@ -118,8 +116,12 @@ def windowsLCIDToLocaleName(lcid: int) -> Optional[str]:
118116
if lang:
119117
return normalizeLanguage(lang)
120118

121-
def getLanguageDescription(language):
119+
120+
def getLanguageDescription(language: str) -> Optional[str]:
122121
"""Finds out the description (localized full name) of a given local name"""
122+
if language == "Windows":
123+
# Translators: the label for the Windows default NVDA interface language.
124+
return _("User default")
123125
desc=None
124126
LCID=localeNameToWindowsLCID(language)
125127
if LCID is not LCID_NONE:
@@ -218,21 +220,27 @@ def ansiCodePageFromNVDALocale(localeName: str) -> Optional[str]:
218220
return None
219221

220222

221-
def getAvailableLanguages(presentational=False):
222-
"""generates a list of locale names, plus their full localized language and country names.
223-
@param presentational: whether this is meant to be shown alphabetically by language description
224-
@type presentational: bool
225-
@rtype: list of tuples
226-
"""
227-
#Make a list of all the locales found in NVDA's locale dir
223+
def listNVDALocales() -> List[str]:
224+
# Make a list of all the locales found in NVDA's locale dir
228225
localesDir = os.path.join(globalVars.appDir, 'locale')
229226
locales = [
230227
x for x in os.listdir(localesDir) if os.path.isfile(os.path.join(localesDir, x, 'LC_MESSAGES', 'nvda.mo'))
231228
]
232-
#Make sure that en (english) is in the list as it may not have any locale files, but is default
229+
# Make sure that en (english) is in the list as it may not have any locale files, but is default
233230
if 'en' not in locales:
234231
locales.append('en')
235232
locales.sort()
233+
# include a 'user default, windows' language,
234+
# which just represents the default language for this user account
235+
locales.insert(0, "Windows")
236+
return locales
237+
238+
239+
def getAvailableLanguages(presentational: bool = False) -> List[Tuple[str, str]]:
240+
"""generates a list of locale names, plus their full localized language and country names.
241+
@param presentational: whether this is meant to be shown alphabetically by language description
242+
"""
243+
locales = listNVDALocales()
236244
# Prepare a 2-tuple list of language code and human readable language description.
237245
langs = [(lc, getLanguageDescription(lc)) for lc in locales]
238246
# Translators: The pattern defining how languages are displayed and sorted in in the general
@@ -243,13 +251,14 @@ def getAvailableLanguages(presentational=False):
243251
isDescFirst = fullDescPattern.find("{desc}") < fullDescPattern.find("{lc}")
244252
if presentational and isDescFirst:
245253
langs.sort(key=lambda lang: locale.strxfrm(lang[1] if lang[1] else lang[0]))
246-
langs = [(lc, (fullDescPattern.format(desc=desc, lc=lc) if desc else lc)) for lc, desc in langs]
247-
#include a 'user default, windows' language, which just represents the default language for this user account
248-
langs.insert(
249-
0,
250-
# Translators: the label for the Windows default NVDA interface language.
251-
("Windows", _("User default"))
252-
)
254+
# Make sure that the 'user default' language is first in the list.
255+
for index, lang in enumerate(langs):
256+
if lang[0] == "Windows":
257+
break
258+
userDefault = langs.pop(index)
259+
langs = [userDefault] + [
260+
(lc, (fullDescPattern.format(desc=desc, lc=lc) if desc else lc)) for lc, desc in langs
261+
]
253262
return langs
254263

255264

@@ -274,6 +283,12 @@ def pgettext(context, message):
274283
raise ValueError("%s is Not a GNUTranslations or NullTranslations object" % translations)
275284
return pgettext
276285

286+
287+
def isLanguageForced() -> bool:
288+
"""Returns `True` if language is provided from the command line - `False` otherwise."""
289+
return any(filter(lambda elem: elem.startswith("--lang="), sys.argv))
290+
291+
277292
def getWindowsLanguage():
278293
"""
279294
Fetches the locale name of the user's configured language in Windows.
@@ -304,10 +319,9 @@ def setLanguage(lang: str) -> None:
304319
Sets the following using `lang` such as "en", "ru_RU", or "es-ES". Use "Windows" to use the system locale
305320
- the windows locale for the thread (fallback to system locale)
306321
- the translation service (fallback to English)
307-
- languageHandler.curLang (match the translation service)
322+
- globalVars.appArgs.language (match the translation service)
308323
- the python locale for the thread (match the translation service, fallback to system default)
309324
'''
310-
global curLang
311325
if lang == "Windows":
312326
localeName = getWindowsLanguage()
313327
else:
@@ -321,20 +335,20 @@ def setLanguage(lang: str) -> None:
321335

322336
try:
323337
trans = gettext.translation("nvda", localedir="locale", languages=[localeName])
324-
curLang = localeName
338+
globalVars.appArgs.language = localeName
325339
except IOError:
326340
try:
327341
log.debugWarning(f"couldn't set the translation service locale to {localeName}")
328342
localeName = localeName.split("_")[0]
329343
trans = gettext.translation("nvda", localedir="locale", languages=[localeName])
330-
curLang = localeName
344+
globalVars.appArgs.language = localeName
331345
except IOError:
332346
log.debugWarning(f"couldn't set the translation service locale to {localeName}")
333347
trans = gettext.translation("nvda", fallback=True)
334-
curLang = "en"
348+
globalVars.appArgs.language = "en"
335349

336350
trans.install()
337-
setLocale(curLang)
351+
setLocale(getLanguage())
338352
# Install our pgettext function.
339353
builtins.pgettext = makePgettext(trans)
340354

@@ -375,7 +389,8 @@ def _setPythonLocale(localeString: str) -> bool:
375389
def setLocale(localeName: str) -> None:
376390
'''
377391
Set python's locale using a `localeName` such as "en", "ru_RU", or "es-ES".
378-
Will fallback on `curLang` if it cannot be set and finally fallback to the system locale.
392+
Will fallback on `globalVars.appArgs.language` if it cannot be set
393+
and finally fallback to the system locale.
379394
Passing NVDA locales straight to python `locale.setlocale` does now work since it tries to normalize the
380395
parameter using `locale.normalize` which results in locales unknown to Windows (Python issue 37945).
381396
For example executing: `locale.setlocale(locale.LC_ALL, "pl")`
@@ -409,21 +424,21 @@ def setLocale(localeName: str) -> None:
409424
return
410425
# Either Windows does not know the locale, or Python is unable to handle it.
411426
# reset to default locale
412-
if originalLocaleName == curLang:
427+
if originalLocaleName == getLanguage():
413428
# reset to system locale default if we can't set the current lang's locale
414429
locale.setlocale(locale.LC_ALL, "")
415430
log.debugWarning(f"set python locale to system default")
416431
else:
417-
log.debugWarning(f"setting python locale to the current language {curLang}")
432+
log.debugWarning(f"setting python locale to the current language {getLanguage()}")
418433
# fallback and try to reset the locale to the current lang
419-
setLocale(curLang)
434+
setLocale(getLanguage())
420435

421436

422437
def getLanguage() -> str:
423-
return curLang
438+
return globalVars.appArgs.language
424439

425440

426-
def normalizeLanguage(lang) -> Optional[str]:
441+
def normalizeLanguage(lang: str) -> Optional[str]:
427442
"""
428443
Normalizes a language-dialect string in to a standard form we can deal with.
429444
Converts any dash to underline, and makes sure that language is lowercase and dialect is upercase.

source/nvda.pyw

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ if not winVersion.isSupportedOS():
112112
winUser.MessageBox(0, ctypes.FormatError(winUser.ERROR_OLD_WIN_VERSION), None, winUser.MB_ICONERROR)
113113
sys.exit(1)
114114

115+
115116
def stringToBool(string):
116117
"""Wrapper for configobj.validate.is_boolean to raise the proper exception for wrong values."""
117118
from configobj.validate import is_boolean, ValidateError
@@ -121,6 +122,26 @@ def stringToBool(string):
121122
raise argparse.ArgumentTypeError(e.message)
122123

123124

125+
def stringToLang(value: str) -> str:
126+
"""Perform basic case normalization for ease of use.
127+
"""
128+
import languageHandler
129+
if value.casefold() == "Windows".casefold():
130+
normalizedLang = "Windows"
131+
else:
132+
normalizedLang = languageHandler.normalizeLanguage(value)
133+
possibleLangNames = languageHandler.listNVDALocales()
134+
if normalizedLang is not None and normalizedLang in possibleLangNames:
135+
return normalizedLang
136+
raise argparse.ArgumentTypeError(
137+
(
138+
"Language code should be \"Windows\","
139+
" of the forms \"en\" or \"pt_BR\""
140+
f" or one of:\n {', '.join(possibleLangNames)}."
141+
)
142+
)
143+
144+
124145
#Process option arguments
125146
parser=NoConsoleOptionParser()
126147
quitGroup = parser.add_mutually_exclusive_group()
@@ -129,6 +150,16 @@ parser.add_argument('-k','--check-running',action="store_true",dest='check_runni
129150
parser.add_argument('-f','--log-file',dest='logFileName',type=str,help="The file where log messages should be written to")
130151
parser.add_argument('-l','--log-level',dest='logLevel',type=int,default=0,choices=[10, 12, 15, 20, 30, 40, 50, 100],help="The lowest level of message logged (debug 10, input/output 12, debugwarning 15, info 20, warning 30, error 40, critical 50, off 100), default is info")
131152
parser.add_argument('-c','--config-path',dest='configPath',default=None,type=str,help="The path where all settings for NVDA are stored")
153+
parser.add_argument(
154+
'--lang',
155+
dest='language',
156+
default="en",
157+
type=stringToLang,
158+
help=(
159+
"Override the configured NVDA language."
160+
" Set to \"Windows\" for current user default, \"en\" for English, etc."
161+
)
162+
)
132163
parser.add_argument('-m','--minimal',action="store_true",dest='minimal',default=False,help="No sounds, no interface, no start message etc")
133164
parser.add_argument('-s','--secure',action="store_true",dest='secure',default=False,help="Secure mode (disable Python console)")
134165
parser.add_argument('--disable-addons',action="store_true",dest='disableAddons',default=False,help="Disable all add-ons")
@@ -342,6 +373,7 @@ if logHandler.log.getEffectiveLevel() is log.DEBUG:
342373
log.debug("Provided arguments: {}".format(sys.argv[1:]))
343374
import buildVersion
344375
log.info("Starting NVDA version %s" % buildVersion.version)
376+
345377
log.debug("Debug level logging enabled")
346378
if customVenvDetected:
347379
log.warning("NVDA launched using a custom Python virtual environment.")

0 commit comments

Comments
 (0)