diff --git a/ftl/core/profiles.ftl b/ftl/core/profiles.ftl index 46b17545b..8347ece7e 100644 --- a/ftl/core/profiles.ftl +++ b/ftl/core/profiles.ftl @@ -8,3 +8,8 @@ profiles-profile-corrupt = Profile Corrupt profiles-profiles = Profiles profiles-quit = Quit profiles-user-1 = User 1 +profiles-confirm-lang-choice = Are you sure you wish to display Anki's interface in { $lang }? +profiles-could-not-create-data-folder = Anki could not create its data folder. Please see the File Locations section of the manual, and ensure that location is not read-only. +profiles-prefs-corrupt-title = Preferences Corrupt +profiles-prefs-file-is-corrupt = Anki's prefs21.db file was corrupt and has been recreated. If you were using multiple profiles, please add them back using the same names to recover your cards. +profiles-profile-does-not-exist = Requested profile does not exist. diff --git a/ftl/qt/qt-misc.ftl b/ftl/qt/qt-misc.ftl index f7bf66c60..e1de29fbe 100644 --- a/ftl/qt/qt-misc.ftl +++ b/ftl/qt/qt-misc.ftl @@ -61,6 +61,12 @@ qt-misc-would-you-like-to-download-it = Would you like to download it now? qt-misc-your-collection-file-appears-to-be = Your collection file appears to be corrupt. This can happen when the file is copied or moved while Anki is open, or when the collection is stored on a network or cloud drive. If problems persist after restarting your computer, please open an automatic backup from the profile screen. qt-misc-your-computers-storage-may-be-full = Your computer's storage may be full. Please delete some unneeded files, then try again. qt-misc-your-firewall-or-antivirus-program-is = Your firewall or antivirus program is preventing Anki from creating a connection to itself. Please add an exception for Anki. +qt-misc-error = Error +qt-misc-no-temp-folder = No usable temporary folder found. Make sure C:\\temp exists or TEMP in your environment points to a valid, writable folder. +qt-misc-incompatible-video-driver = Your video driver is incompatible. Please start Anki again, and Anki will switch to a slower, more compatible mode. +qt-misc-error-loading-graphics-driver = Error loading '{ $mode }' graphics driver. Please start Anki again to try next driver. { $context } +qt-misc-anki-is-running = Anki Already Running +qt-misc-if-instance-is-not-responding = If the existing instance of Anki is not responding, please close it using your task manager, or restart your computer. qt-misc-second = { $count -> [one] { $count } second diff --git a/pylib/anki/lang.py b/pylib/anki/lang.py index 74496d1f5..28b717e84 100644 --- a/pylib/anki/lang.py +++ b/pylib/anki/lang.py @@ -3,8 +3,9 @@ from __future__ import annotations +import locale import re -from typing import Optional +from typing import Optional, Tuple import anki @@ -171,6 +172,36 @@ def set_lang(lang: str, locale_dir: str) -> None: locale_folder = locale_dir +def get_def_lang(lang: Optional[str] = None) -> Tuple[int, str]: + """Return lang converted to name used on disk and its index, defaulting to system language + or English if not available.""" + try: + (sys_lang, enc) = locale.getdefaultlocale() + except: + # fails on osx + sys_lang = "en_US" + user_lang = lang + if user_lang in compatMap: + user_lang = compatMap[user_lang] + idx = None + lang = None + en = None + for l in (user_lang, sys_lang): + for c, (name, code) in enumerate(langs): + if code == "en_US": + en = c + if code == l: + idx = c + lang = l + if idx is not None: + break + # if the specified language and the system language aren't available, revert to english + if idx is None: + idx = en + lang = "en_US" + return (idx, lang) + + def is_rtl(lang: str) -> bool: return lang in ("he", "ar", "fa") diff --git a/qt/aqt/__init__.py b/qt/aqt/__init__.py index 2317b4c67..20d4c4d34 100644 --- a/qt/aqt/__init__.py +++ b/qt/aqt/__init__.py @@ -17,7 +17,7 @@ from anki.consts import HELP_SITE from anki.rsbackend import RustBackend from anki.utils import checksum, isLin, isMac from aqt.qt import * -from aqt.utils import locale_dir +from aqt.utils import TR, locale_dir, tr # we want to be able to print unicode debug info to console without # fear of a traceback on systems with the console set to ASCII @@ -175,7 +175,10 @@ _qtrans: Optional[QTranslator] = None def setupLangAndBackend( - pm: ProfileManager, app: QApplication, force: Optional[str] = None + pm: ProfileManager, + app: QApplication, + force: Optional[str] = None, + firstTime: bool = False, ) -> RustBackend: global _qtrans try: @@ -198,12 +201,16 @@ def setupLangAndBackend( builtins.__dict__["ngettext"] = fn_ngettext # get lang and normalize into ja/zh-CN form - lang = force or pm.meta["defaultLang"] + if firstTime: + lang = pm.meta["defaultLang"] + else: + lang = force or pm.meta["defaultLang"] lang = anki.lang.lang_to_disk_lang(lang) - # set active language ldir = locale_dir() - anki.lang.set_lang(lang, ldir) + if not firstTime: + # set active language + anki.lang.set_lang(lang, ldir) # switch direction for RTL languages if anki.lang.is_rtl(lang): @@ -269,8 +276,8 @@ class AnkiApp(QApplication): # existing instance running but hung QMessageBox.warning( None, - "Anki Already Running", - "If the existing instance of Anki is not responding, please close it using your task manager, or restart your computer.", + tr(TR.QT_MISC_ANKI_IS_RUNNING), + tr(TR.QT_MISC_IF_INSTANCE_IS_NOT_RESPONDING), ) sys.exit(1) @@ -356,8 +363,10 @@ def setupGL(pm): if "Failed to create OpenGL context" in msg: QMessageBox.critical( None, - "Error", - f"Error loading '{mode}' graphics driver. Please start Anki again to try next driver. {context}", + tr(TR.QT_MISC_ERROR), + tr( + TR.QT_MISC_ERROR_LOADING_GRAPHICS_DRIVER, mode=mode, context=context + ), ) pm.nextGlMode() return @@ -427,6 +436,10 @@ def _run(argv=None, exec=True): profiler = cProfile.Profile() profiler.enable() + # default to specified/system language before getting user's preference so that we can localize some more strings + lang = anki.lang.get_def_lang(opts.lang) + anki.lang.set_lang(lang[1], locale_dir()) + # profile manager pm = None try: @@ -475,10 +488,8 @@ def _run(argv=None, exec=True): if not pm: QMessageBox.critical( None, - "Error", - """\ -Anki could not create its data folder. Please see the File Locations \ -section of the manual, and ensure that location is not read-only.""", + tr(TR.QT_MISC_ERROR), + tr(TR.PROFILES_COULD_NOT_CREATE_DATA_FOLDER), ) return @@ -517,29 +528,26 @@ section of the manual, and ensure that location is not read-only.""", except: QMessageBox.critical( None, - "Error", - """\ -No usable temporary folder found. Make sure C:\\temp exists or TEMP in your \ -environment points to a valid, writable folder.""", + tr(TR.QT_MISC_ERROR), + tr(TR.QT_MISC_NO_TEMP_FOLDER), ) return if pmLoadResult.firstTime: - pm.setDefaultLang() + pm.setDefaultLang(lang[0]) if pmLoadResult.loadError: QMessageBox.warning( None, - "Preferences Corrupt", - """Anki's prefs21.db file was corrupt and has been recreated. If you were using multiple \ - profiles, please add them back using the same names to recover your cards.""", + tr(TR.PROFILES_PREFS_CORRUPT_TITLE), + tr(TR.PROFILES_PREFS_FILE_IS_CORRUPT), ) if opts.profile: pm.openProfile(opts.profile) # i18n & backend - backend = setupLangAndBackend(pm, app, opts.lang) + backend = setupLangAndBackend(pm, app, opts.lang, pmLoadResult.firstTime) if isLin and pm.glMode() == "auto": from aqt.utils import gfxDriverIsBroken @@ -548,8 +556,8 @@ environment points to a valid, writable folder.""", pm.nextGlMode() QMessageBox.critical( None, - "Error", - "Your video driver is incompatible. Please start Anki again, and Anki will switch to a slower, more compatible mode.", + tr(TR.QT_MISC_ERROR), + tr(TR.QT_MISC_INCOMPATIBLE_VIDEO_DRIVER), ) sys.exit(1) diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index d2c3355b4..9a4c13994 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -97,7 +97,9 @@ class ProfileManager: def openProfile(self, profile) -> None: if profile: if profile not in self.profiles(): - QMessageBox.critical(None, "Error", "Requested profile does not exist.") + QMessageBox.critical( + None, tr(TR.QT_MISC_ERROR), tr(TR.PROFILES_PROFILE_DOES_NOT_EXIST) + ) sys.exit(1) try: self.load(profile) @@ -482,7 +484,7 @@ create table if not exists profiles ###################################################################### # On first run, allow the user to choose the default language - def setDefaultLang(self) -> None: + def setDefaultLang(self, idx: int) -> None: # create dialog class NoCloseDiag(QDialog): def reject(self): @@ -490,28 +492,9 @@ create table if not exists profiles d = self.langDiag = NoCloseDiag() f = self.langForm = aqt.forms.setlang.Ui_Dialog() + f.setupUi(d) qconnect(d.accepted, self._onLangSelected) qconnect(d.rejected, lambda: True) - # default to the system language - try: - (lang, enc) = locale.getdefaultlocale() - except: - # fails on osx - lang = "en_US" - # find index - idx = None - en = None - for c, (name, code) in enumerate(anki.lang.langs): - if code == "en_US": - en = c - if code == lang: - idx = c - # if the system language isn't available, revert to english - if idx is None: - idx = en - lang = "en_US" - anki.lang.set_lang(lang, locale_dir()) - f.setupUi(d) # update list f.lang.addItems([x[0] for x in anki.lang.langs]) f.lang.setCurrentRow(idx) @@ -522,12 +505,11 @@ create table if not exists profiles obj = anki.lang.langs[f.lang.currentRow()] code = obj[1] name = obj[0] - en = "Are you sure you wish to display Anki's interface in %s?" r = QMessageBox.question( - None, "Anki", en % name, QMessageBox.Yes | QMessageBox.No, QMessageBox.No # type: ignore + None, "Anki", tr(TR.PROFILES_CONFIRM_LANG_CHOICE, lang=name), QMessageBox.Yes | QMessageBox.No, QMessageBox.No # type: ignore ) if r != QMessageBox.Yes: - return self.setDefaultLang() + return self.setDefaultLang(f.lang.currentRow()) self.setLang(code) def setLang(self, code) -> None: