merge bulk of qt/ - designer files still to do

This commit is contained in:
Damien Elmes 2020-11-17 17:42:43 +10:00
parent 6a5c397866
commit 6418993840
49 changed files with 1094 additions and 545 deletions

View file

@ -9,7 +9,7 @@ from anki.lang import _
from anki.utils import versionWithBuild from anki.utils import versionWithBuild
from aqt.addons import AddonManager, AddonMeta from aqt.addons import AddonManager, AddonMeta
from aqt.qt import * from aqt.qt import *
from aqt.utils import supportText, tooltip from aqt.utils import TR, supportText, tooltip, tr
class ClosableQDialog(QDialog): class ClosableQDialog(QDialog):
@ -83,9 +83,9 @@ def show(mw):
""" """
info = " " + " ".join(info.splitlines(True)) info = " " + " ".join(info.splitlines(True))
QApplication.clipboard().setText(info) QApplication.clipboard().setText(info)
tooltip(_("Copied to clipboard"), parent=dialog) tooltip(tr(TR.ABOUT_COPIED_TO_CLIPBOARD), parent=dialog)
btn = QPushButton(_("Copy Debug Info")) btn = QPushButton(tr(TR.ABOUT_COPY_DEBUG_INFO))
qconnect(btn.clicked, onCopy) qconnect(btn.clicked, onCopy)
abt.buttonBox.addButton(btn, QDialogButtonBox.ActionRole) abt.buttonBox.addButton(btn, QDialogButtonBox.ActionRole)
abt.buttonBox.button(QDialogButtonBox.Ok).setFocus() abt.buttonBox.button(QDialogButtonBox.Ok).setFocus()
@ -101,13 +101,13 @@ system. It's free and open source."
"Anki is licensed under the AGPL3 license. Please see " "Anki is licensed under the AGPL3 license. Please see "
"the license file in the source distribution for more information." "the license file in the source distribution for more information."
) )
abouttext += "<p>" + _("Version %s") % versionWithBuild() + "<br>" abouttext += "<p>" + tr(TR.ABOUT_VERSION, val="%s") % versionWithBuild() + "<br>"
abouttext += ("Python %s Qt %s PyQt %s<br>") % ( abouttext += ("Python %s Qt %s PyQt %s<br>") % (
platform.python_version(), platform.python_version(),
QT_VERSION_STR, QT_VERSION_STR,
PYQT_VERSION_STR, PYQT_VERSION_STR,
) )
abouttext += (_("<a href='%s'>Visit website</a>") % aqt.appWebsite) + "</span>" abouttext += (tr(TR.ABOUT_VISIT_WEBSITE, val="%s") % aqt.appWebsite) + "</span>"
# automatically sorted; add new lines at the end # automatically sorted; add new lines at the end
allusers = sorted( allusers = sorted(

View file

@ -16,6 +16,7 @@ from aqt.main import ResetReason
from aqt.qt import * from aqt.qt import *
from aqt.sound import av_player from aqt.sound import av_player
from aqt.utils import ( from aqt.utils import (
TR,
addCloseShortcut, addCloseShortcut,
askUser, askUser,
downArrow, downArrow,
@ -25,6 +26,7 @@ from aqt.utils import (
shortcut, shortcut,
showWarning, showWarning,
tooltip, tooltip,
tr,
) )
@ -35,7 +37,7 @@ class AddCards(QDialog):
self.mw = mw self.mw = mw
self.form = aqt.forms.addcards.Ui_Dialog() self.form = aqt.forms.addcards.Ui_Dialog()
self.form.setupUi(self) self.form.setupUi(self)
self.setWindowTitle(_("Add")) self.setWindowTitle(tr(TR.ACTIONS_ADD))
self.setMinimumHeight(300) self.setMinimumHeight(300)
self.setMinimumWidth(400) self.setMinimumWidth(400)
self.setupChoosers() self.setupChoosers()
@ -67,26 +69,26 @@ class AddCards(QDialog):
bb = self.form.buttonBox bb = self.form.buttonBox
ar = QDialogButtonBox.ActionRole ar = QDialogButtonBox.ActionRole
# add # add
self.addButton = bb.addButton(_("Add"), ar) self.addButton = bb.addButton(tr(TR.ACTIONS_ADD), ar)
qconnect(self.addButton.clicked, self.addCards) qconnect(self.addButton.clicked, self.addCards)
self.addButton.setShortcut(QKeySequence("Ctrl+Return")) self.addButton.setShortcut(QKeySequence("Ctrl+Return"))
self.addButton.setToolTip(shortcut(_("Add (shortcut: ctrl+enter)"))) self.addButton.setToolTip(shortcut(tr(TR.ADDING_ADD_SHORTCUT_CTRLANDENTER)))
# close # close
self.closeButton = QPushButton(_("Close")) self.closeButton = QPushButton(tr(TR.ACTIONS_CLOSE))
self.closeButton.setAutoDefault(False) self.closeButton.setAutoDefault(False)
bb.addButton(self.closeButton, QDialogButtonBox.RejectRole) bb.addButton(self.closeButton, QDialogButtonBox.RejectRole)
# help # help
self.helpButton = QPushButton(_("Help"), clicked=self.helpRequested) # type: ignore self.helpButton = QPushButton(tr(TR.ACTIONS_HELP), clicked=self.helpRequested) # type: ignore
self.helpButton.setAutoDefault(False) self.helpButton.setAutoDefault(False)
bb.addButton(self.helpButton, QDialogButtonBox.HelpRole) bb.addButton(self.helpButton, QDialogButtonBox.HelpRole)
# history # history
b = bb.addButton(_("History") + " " + downArrow(), ar) b = bb.addButton(tr(TR.ADDING_HISTORY) + " " + downArrow(), ar)
if isMac: if isMac:
sc = "Ctrl+Shift+H" sc = "Ctrl+Shift+H"
else: else:
sc = "Ctrl+H" sc = "Ctrl+H"
b.setShortcut(QKeySequence(sc)) b.setShortcut(QKeySequence(sc))
b.setToolTip(_("Shortcut: %s") % shortcut(sc)) b.setToolTip(tr(TR.ADDING_SHORTCUT, val="%s") % shortcut(sc))
qconnect(b.clicked, self.onHistory) qconnect(b.clicked, self.onHistory)
b.setEnabled(False) b.setEnabled(False)
self.historyButton = b self.historyButton = b
@ -151,7 +153,7 @@ class AddCards(QDialog):
a = m.addAction(line) a = m.addAction(line)
qconnect(a.triggered, lambda b, nid=nid: self.editHistory(nid)) qconnect(a.triggered, lambda b, nid=nid: self.editHistory(nid))
else: else:
a = m.addAction(_("(Note deleted)")) a = m.addAction(tr(TR.ADDING_NOTE_DELETED))
a.setEnabled(False) a.setEnabled(False)
gui_hooks.add_cards_will_show_history_menu(self, m) gui_hooks.add_cards_will_show_history_menu(self, m)
m.exec_(self.historyButton.mapToGlobal(QPoint(0, 0))) m.exec_(self.historyButton.mapToGlobal(QPoint(0, 0)))
@ -166,7 +168,7 @@ class AddCards(QDialog):
ret = note.dupeOrEmpty() ret = note.dupeOrEmpty()
problem = None problem = None
if ret == 1: if ret == 1:
problem = _("The first field is empty.") problem = tr(TR.ADDING_THE_FIRST_FIELD_IS_EMPTY)
problem = gui_hooks.add_cards_will_add_note(problem, note) problem = gui_hooks.add_cards_will_add_note(problem, note)
if problem is not None: if problem is not None:
showWarning(problem, help="AddItems#AddError") showWarning(problem, help="AddItems#AddError")
@ -199,7 +201,7 @@ class AddCards(QDialog):
# workaround for PyQt focus bug # workaround for PyQt focus bug
self.editor.hideCompleters() self.editor.hideCompleters()
tooltip(_("Added"), period=500) tooltip(tr(TR.ADDING_ADDED), period=500)
av_player.stop_and_clear_queue() av_player.stop_and_clear_queue()
self.onReset(keep=True) self.onReset(keep=True)
self.mw.col.autosave() self.mw.col.autosave()
@ -229,7 +231,7 @@ class AddCards(QDialog):
def ifCanClose(self, onOk: Callable) -> None: def ifCanClose(self, onOk: Callable) -> None:
def afterSave(): def afterSave():
ok = self.editor.fieldsAreBlank(self.previousNote) or askUser( ok = self.editor.fieldsAreBlank(self.previousNote) or askUser(
_("Close and lose current input?"), defaultno=True tr(TR.ADDING_CLOSE_AND_LOSE_CURRENT_INPUT), defaultno=True
) )
if ok: if ok:
onOk() onOk()

View file

@ -1,7 +1,6 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations from __future__ import annotations
import io import io
@ -310,7 +309,7 @@ and have been disabled: %(found)s"
meta = self.addon_meta(dir) meta = self.addon_meta(dir)
name = meta.human_name() name = meta.human_name()
if not meta.enabled: if not meta.enabled:
name += _(" (disabled)") name += tr(TR.ADDONS_DISABLED)
return name return name
# Conflict resolution # Conflict resolution
@ -423,10 +422,7 @@ and have been disabled: %(found)s"
return True return True
except OSError as e: except OSError as e:
showWarning( showWarning(
_( tr(TR.ADDONS_UNABLE_TO_UPDATE_OR_DELETE_ADDON, val="%s") % e,
"Unable to update or delete add-on. Please start Anki while holding down the shift key to temporarily disable add-ons, then try again.\n\nDebug info: %s"
)
% e,
textFormat="plain", textFormat="plain",
) )
return False return False
@ -468,16 +464,16 @@ and have been disabled: %(found)s"
) -> List[str]: ) -> List[str]:
messages = { messages = {
"zip": _("Corrupt add-on file."), "zip": tr(TR.ADDONS_CORRUPT_ADDON_FILE),
"manifest": _("Invalid add-on manifest."), "manifest": tr(TR.ADDONS_INVALID_ADDON_MANIFEST),
} }
msg = messages.get(result.errmsg, _("Unknown error: {}".format(result.errmsg))) msg = messages.get(result.errmsg, _("Unknown error: {}".format(result.errmsg)))
if mode == "download": # preserve old format strings for i18n if mode == "download": # preserve old format strings for i18n
template = _("Error downloading <i>%(id)s</i>: %(error)s") template = tr(TR.ADDONS_ERROR_DOWNLOADING_IDS_ERRORS)
else: else:
template = _("Error installing <i>%(base)s</i>: %(error)s") template = tr(TR.ADDONS_ERROR_INSTALLING_BASES_ERRORS)
name = base name = base
@ -488,24 +484,22 @@ and have been disabled: %(found)s"
) -> List[str]: ) -> List[str]:
if mode == "download": # preserve old format strings for i18n if mode == "download": # preserve old format strings for i18n
template = _("Downloaded %(fname)s") template = tr(TR.ADDONS_DOWNLOADED_FNAMES)
else: else:
template = _("Installed %(name)s") template = tr(TR.ADDONS_INSTALLED_NAMES)
name = result.name or base name = result.name or base
strings = [template % dict(name=name, fname=name)] strings = [template % dict(name=name, fname=name)]
if result.conflicts: if result.conflicts:
strings.append( strings.append(
_("The following conflicting add-ons were disabled:") tr(TR.ADDONS_THE_FOLLOWING_CONFLICTING_ADDONS_WERE_DISABLED)
+ " " + " "
+ ", ".join(self.addonName(f) for f in result.conflicts) + ", ".join(self.addonName(f) for f in result.conflicts)
) )
if not result.compatible: if not result.compatible:
strings.append( strings.append(tr(TR.ADDONS_THIS_ADDON_IS_NOT_COMPATIBLE_WITH))
_("This add-on is not compatible with your version of Anki.")
)
return strings return strings
@ -743,9 +737,13 @@ class AddonsDialog(QDialog):
name = addon.human_name() name = addon.human_name()
if not addon.enabled: if not addon.enabled:
return name + " " + _("(disabled)") return name + " " + tr(TR.ADDONS_DISABLED2)
elif not addon.compatible(): elif not addon.compatible():
return name + " " + _("(requires %s)") % self.compatible_string(addon) return (
name
+ " "
+ tr(TR.ADDONS_REQUIRES, val="%s") % self.compatible_string(addon)
)
return name return name
@ -804,7 +802,7 @@ class AddonsDialog(QDialog):
def onlyOneSelected(self) -> Optional[str]: def onlyOneSelected(self) -> Optional[str]:
dirs = self.selectedAddons() dirs = self.selectedAddons()
if len(dirs) != 1: if len(dirs) != 1:
showInfo(_("Please select a single add-on first.")) showInfo(tr(TR.ADDONS_PLEASE_SELECT_A_SINGLE_ADDON_FIRST))
return None return None
return dirs[0] return dirs[0]
@ -820,7 +818,7 @@ class AddonsDialog(QDialog):
if re.match(r"^\d+$", addon): if re.match(r"^\d+$", addon):
openLink(aqt.appShared + "info/{}".format(addon)) openLink(aqt.appShared + "info/{}".format(addon))
else: else:
showWarning(_("Add-on was not downloaded from AnkiWeb.")) showWarning(tr(TR.ADDONS_ADDON_WAS_NOT_DOWNLOADED_FROM_ANKIWEB))
def onViewFiles(self) -> None: def onViewFiles(self) -> None:
# if nothing selected, open top level folder # if nothing selected, open top level folder
@ -865,13 +863,13 @@ class AddonsDialog(QDialog):
if log: if log:
show_log_to_user(self, log) show_log_to_user(self, log)
else: else:
tooltip(_("No updates available.")) tooltip(tr(TR.ADDONS_NO_UPDATES_AVAILABLE))
def onInstallFiles(self, paths: Optional[List[str]] = None) -> Optional[bool]: def onInstallFiles(self, paths: Optional[List[str]] = None) -> Optional[bool]:
if not paths: if not paths:
key = _("Packaged Anki Add-on") + " (*{})".format(self.mgr.ext) key = tr(TR.ADDONS_PACKAGED_ANKI_ADDON) + " (*{})".format(self.mgr.ext)
paths = getFile( paths = getFile(
self, _("Install Add-on(s)"), None, key, key="addons", multi=True self, tr(TR.ADDONS_INSTALL_ADDONS), None, key, key="addons", multi=True
) )
if not paths: if not paths:
return False return False
@ -882,7 +880,7 @@ class AddonsDialog(QDialog):
return None return None
def check_for_updates(self) -> None: def check_for_updates(self) -> None:
tooltip(_("Checking...")) tooltip(tr(TR.ADDONS_CHECKING))
check_and_prompt_for_updates(self, self.mgr, self.after_downloading) check_and_prompt_for_updates(self, self.mgr, self.after_downloading)
def onConfig(self) -> None: def onConfig(self) -> None:
@ -899,7 +897,7 @@ class AddonsDialog(QDialog):
conf = self.mgr.getConfig(addon) conf = self.mgr.getConfig(addon)
if conf is None: if conf is None:
showInfo(_("Add-on has no configuration.")) showInfo(tr(TR.ADDONS_ADDON_HAS_NO_CONFIGURATION))
return return
ConfigEditor(self, addon, conf) ConfigEditor(self, addon, conf)
@ -919,7 +917,7 @@ class GetAddons(QDialog):
self.form = aqt.forms.getaddons.Ui_Dialog() self.form = aqt.forms.getaddons.Ui_Dialog()
self.form.setupUi(self) self.form.setupUi(self)
b = self.form.buttonBox.addButton( b = self.form.buttonBox.addButton(
_("Browse Add-ons"), QDialogButtonBox.ActionRole tr(TR.ADDONS_BROWSE_ADDONS), QDialogButtonBox.ActionRole
) )
qconnect(b.clicked, self.onBrowse) qconnect(b.clicked, self.onBrowse)
restoreGeom(self, "getaddons", adjustSize=True) restoreGeom(self, "getaddons", adjustSize=True)
@ -934,7 +932,7 @@ class GetAddons(QDialog):
try: try:
ids = [int(n) for n in self.form.code.text().split()] ids = [int(n) for n in self.form.code.text().split()]
except ValueError: except ValueError:
showWarning(_("Invalid code.")) showWarning(tr(TR.ADDONS_INVALID_CODE))
return return
self.ids = ids self.ids = ids
@ -1007,21 +1005,22 @@ def describe_log_entry(id_and_entry: DownloadLogEntry) -> str:
if isinstance(entry, DownloadError): if isinstance(entry, DownloadError):
if entry.status_code is not None: if entry.status_code is not None:
if entry.status_code in (403, 404): if entry.status_code in (403, 404):
buf += _( buf += tr(TR.ADDONS_INVALID_CODE_OR_ADDON_NOT_AVAILABLE)
"Invalid code, or add-on not available for your version of Anki."
)
else:
buf += _("Unexpected response code: %s") % entry.status_code
else: else:
buf += ( buf += (
_("Please check your internet connection.") tr(TR.QT_MISC_UNEXPECTED_RESPONSE_CODE, val="%s")
% entry.status_code
)
else:
buf += (
tr(TR.ADDONS_PLEASE_CHECK_YOUR_INTERNET_CONNECTION)
+ "\n\n" + "\n\n"
+ str(entry.exception) + str(entry.exception)
) )
elif isinstance(entry, InstallError): elif isinstance(entry, InstallError):
buf += entry.errmsg buf += entry.errmsg
else: else:
buf += _("Installed successfully.") buf += tr(TR.ADDONS_INSTALLED_SUCCESSFULLY)
return buf return buf
@ -1091,7 +1090,7 @@ class DownloaderInstaller(QObject):
# and "%(kb)0.2f" is the number of downloaded # and "%(kb)0.2f" is the number of downloaded
# kilobytes. This lead for example to "Downloading 3/5 # kilobytes. This lead for example to "Downloading 3/5
# (27KB)" # (27KB)"
label=_("Downloading %(a)d/%(b)d (%(kb)0.2fKB)...") label=tr(TR.ADDONS_DOWNLOADING_ADBD_KB02FKB)
% dict(a=len(self.log) + 1, b=len(self.ids), kb=self.dl_bytes / 1024) % dict(a=len(self.log) + 1, b=len(self.ids), kb=self.dl_bytes / 1024)
) )
@ -1110,9 +1109,9 @@ def show_log_to_user(parent: QWidget, log: List[DownloadLogEntry]) -> None:
have_problem = download_encountered_problem(log) have_problem = download_encountered_problem(log)
if have_problem: if have_problem:
text = _("One or more errors occurred:") text = tr(TR.ADDONS_ONE_OR_MORE_ERRORS_OCCURRED)
else: else:
text = _("Download complete. Please restart Anki to apply changes.") text = tr(TR.ADDONS_DOWNLOAD_COMPLETE_PLEASE_RESTART_ANKI_TO)
text += "<br><br>" + download_log_to_html(log) text += "<br><br>" + download_log_to_html(log)
if have_problem: if have_problem:
@ -1265,7 +1264,7 @@ def prompt_to_update(
) -> None: ) -> None:
names = map(lambda x: mgr.addonName(str(x)), ids) names = map(lambda x: mgr.addonName(str(x)), ids)
if not askUser( if not askUser(
_("The following add-ons have updates available. Install them now?") tr(TR.ADDONS_THE_FOLLOWING_ADDONS_HAVE_UPDATES_AVAILABLE)
+ "\n\n" + "\n\n"
+ "\n".join(names) + "\n".join(names)
): ):
@ -1307,7 +1306,7 @@ class ConfigEditor(QDialog):
def onRestoreDefaults(self) -> None: def onRestoreDefaults(self) -> None:
default_conf = self.mgr.addonConfigDefaults(self.addon) default_conf = self.mgr.addonConfigDefaults(self.addon)
self.updateText(default_conf) self.updateText(default_conf)
tooltip(_("Restored defaults"), parent=self) tooltip(tr(TR.ADDONS_RESTORED_DEFAULTS), parent=self)
def setupFonts(self) -> None: def setupFonts(self) -> None:
font_mono = QFontDatabase.systemFont(QFontDatabase.FixedFont) font_mono = QFontDatabase.systemFont(QFontDatabase.FixedFont)
@ -1373,11 +1372,11 @@ class ConfigEditor(QDialog):
showInfo(msg) showInfo(msg)
return return
except Exception as e: except Exception as e:
showInfo(_("Invalid configuration: ") + repr(e)) showInfo(tr(TR.ADDONS_INVALID_CONFIGURATION) + repr(e))
return return
if not isinstance(new_conf, dict): if not isinstance(new_conf, dict):
showInfo(_("Invalid configuration: top level object must be a map")) showInfo(tr(TR.ADDONS_INVALID_CONFIGURATION_TOP_LEVEL_OBJECT_MUST))
return return
if new_conf != self.conf: if new_conf != self.conf:
@ -1417,7 +1416,7 @@ def installAddonPackages(
not showInfo( not showInfo(
q, q,
parent=parent, parent=parent,
title=_("Install Anki add-on"), title=tr(TR.ADDONS_INSTALL_ANKI_ADDON),
type="warning", type="warning",
customBtns=[QMessageBox.No, QMessageBox.Yes], customBtns=[QMessageBox.No, QMessageBox.Yes],
) )
@ -1430,9 +1429,7 @@ def installAddonPackages(
if log: if log:
log_html = "<br>".join(log) log_html = "<br>".join(log)
if advise_restart: if advise_restart:
log_html += "<br><br>" + _( log_html += "<br><br>" + tr(TR.ADDONS_PLEASE_RESTART_ANKI_TO_COMPLETE_THE)
"<b>Please restart Anki to complete the installation.</b>"
)
if len(log) == 1 and not strictly_modal: if len(log) == 1 and not strictly_modal:
tooltip(log_html, parent=parent) tooltip(log_html, parent=parent)
else: else:
@ -1440,15 +1437,15 @@ def installAddonPackages(
log_html, log_html,
parent=parent, parent=parent,
textFormat="rich", textFormat="rich",
title=_("Installation complete"), title=tr(TR.ADDONS_INSTALLATION_COMPLETE),
) )
if errs: if errs:
msg = _("Please report this to the respective add-on author(s).") msg = tr(TR.ADDONS_PLEASE_REPORT_THIS_TO_THE_RESPECTIVE)
showWarning( showWarning(
"<br><br>".join(errs + [msg]), "<br><br>".join(errs + [msg]),
parent=parent, parent=parent,
textFormat="rich", textFormat="rich",
title=_("Add-on installation error"), title=tr(TR.ADDONS_ADDON_INSTALLATION_ERROR),
) )
return not errs return not errs

View file

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations from __future__ import annotations
import html import html
@ -34,6 +33,7 @@ from aqt.qt import *
from aqt.sidebar import NewSidebarTreeView, SidebarItemType, SidebarTreeViewBase from aqt.sidebar import NewSidebarTreeView, SidebarItemType, SidebarTreeViewBase
from aqt.theme import theme_manager from aqt.theme import theme_manager
from aqt.utils import ( from aqt.utils import (
TR,
MenuList, MenuList,
SubMenu, SubMenu,
askUser, askUser,
@ -169,7 +169,7 @@ class DataModel(QAbstractTableModel):
break break
# give the user a hint an invalid column was added by an add-on # give the user a hint an invalid column was added by an add-on
if not txt: if not txt:
txt = _("Add-on") txt = tr(TR.BROWSING_ADDON)
return txt return txt
else: else:
return return
@ -331,13 +331,13 @@ class DataModel(QAbstractTableModel):
return c.model()["name"] return c.model()["name"]
elif type == "cardIvl": elif type == "cardIvl":
if c.type == CARD_TYPE_NEW: if c.type == CARD_TYPE_NEW:
return _("(new)") return tr(TR.BROWSING_NEW)
elif c.type == CARD_TYPE_LRN: elif c.type == CARD_TYPE_LRN:
return _("(learning)") return tr(TR.BROWSING_LEARNING)
return self.col.format_timespan(c.ivl * 86400) return self.col.format_timespan(c.ivl * 86400)
elif type == "cardEase": elif type == "cardEase":
if c.type == CARD_TYPE_NEW: if c.type == CARD_TYPE_NEW:
return _("(new)") return tr(TR.BROWSING_NEW)
return "%d%%" % (c.factor / 10) return "%d%%" % (c.factor / 10)
elif type == "deck": elif type == "deck":
if c.odid: if c.odid:
@ -366,7 +366,7 @@ class DataModel(QAbstractTableModel):
def nextDue(self, c, index): def nextDue(self, c, index):
if c.odid: if c.odid:
return _("(filtered)") return tr(TR.BROWSING_FILTERED)
elif c.queue == QUEUE_TYPE_LRN: elif c.queue == QUEUE_TYPE_LRN:
date = c.due date = c.due
elif c.queue == QUEUE_TYPE_NEW or c.type == CARD_TYPE_NEW: elif c.queue == QUEUE_TYPE_NEW or c.type == CARD_TYPE_NEW:
@ -621,7 +621,7 @@ class Browser(QMainWindow):
f = self.form f = self.form
qconnect(f.previewButton.clicked, self.onTogglePreview) qconnect(f.previewButton.clicked, self.onTogglePreview)
f.previewButton.setToolTip( f.previewButton.setToolTip(
_("Preview Selected Card (%s)") % shortcut("Ctrl+Shift+P") tr(TR.BROWSING_PREVIEW_SELECTED_CARD, val="%s") % shortcut("Ctrl+Shift+P")
) )
f.previewButton.setShortcut("Ctrl+Shift+P") f.previewButton.setShortcut("Ctrl+Shift+P")
@ -737,21 +737,21 @@ class Browser(QMainWindow):
def setupColumns(self): def setupColumns(self):
self.columns = [ self.columns = [
("question", _("Question")), ("question", tr(TR.BROWSING_QUESTION)),
("answer", _("Answer")), ("answer", tr(TR.BROWSING_ANSWER)),
("template", _("Card")), ("template", tr(TR.BROWSING_CARD)),
("deck", _("Deck")), ("deck", tr(TR.DECKS_DECK)),
("noteFld", _("Sort Field")), ("noteFld", tr(TR.BROWSING_SORT_FIELD)),
("noteCrt", _("Created")), ("noteCrt", tr(TR.BROWSING_CREATED)),
("noteMod", tr(TR.SEARCH_NOTE_MODIFIED)), ("noteMod", tr(TR.SEARCH_NOTE_MODIFIED)),
("cardMod", tr(TR.SEARCH_CARD_MODIFIED)), ("cardMod", tr(TR.SEARCH_CARD_MODIFIED)),
("cardDue", tr(TR.STATISTICS_DUE_DATE)), ("cardDue", tr(TR.STATISTICS_DUE_DATE)),
("cardIvl", _("Interval")), ("cardIvl", tr(TR.BROWSING_INTERVAL)),
("cardEase", _("Ease")), ("cardEase", tr(TR.BROWSING_EASE)),
("cardReps", _("Reviews")), ("cardReps", tr(TR.SCHEDULING_REVIEWS)),
("cardLapses", _("Lapses")), ("cardLapses", tr(TR.SCHEDULING_LAPSES)),
("noteTags", _("Tags")), ("noteTags", tr(TR.EDITING_TAGS)),
("note", _("Note")), ("note", tr(TR.BROWSING_NOTE)),
] ]
self.columns.sort(key=itemgetter(1)) self.columns.sort(key=itemgetter(1))
@ -762,7 +762,7 @@ class Browser(QMainWindow):
qconnect(self.form.searchButton.clicked, self.onSearchActivated) qconnect(self.form.searchButton.clicked, self.onSearchActivated)
qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated) qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated)
self.form.searchEdit.setCompleter(None) self.form.searchEdit.setCompleter(None)
self._searchPrompt = _("<type here to search; hit enter to show current deck>") self._searchPrompt = tr(TR.BROWSING_TYPE_HERE_TO_SEARCH)
self.form.searchEdit.addItems( self.form.searchEdit.addItems(
[self._searchPrompt] + self.mw.pm.profile["searchHistory"] [self._searchPrompt] + self.mw.pm.profile["searchHistory"]
) )
@ -938,9 +938,7 @@ QTableView {{ gridline-color: {grid} }}
type = self.model.activeCols[idx] type = self.model.activeCols[idx]
noSort = ("question", "answer") noSort = ("question", "answer")
if type in noSort: if type in noSort:
showInfo( showInfo(tr(TR.BROWSING_SORTING_ON_THIS_COLUMN_IS_NOT))
_("Sorting on this column is not supported. Please " "choose another.")
)
type = self.col.conf["sortType"] type = self.col.conf["sortType"]
if self.col.conf["sortType"] != type: if self.col.conf["sortType"] != type:
self.col.conf["sortType"] = type self.col.conf["sortType"] = type
@ -994,7 +992,7 @@ QTableView {{ gridline-color: {grid} }}
if type in self.model.activeCols: if type in self.model.activeCols:
if len(self.model.activeCols) < 2: if len(self.model.activeCols) < 2:
self.model.endReset() self.model.endReset()
return showInfo(_("You must have at least one column.")) return showInfo(tr(TR.BROWSING_YOU_MUST_HAVE_AT_LEAST_ONE))
self.model.activeCols.remove(type) self.model.activeCols.remove(type)
adding = False adding = False
else: else:
@ -1028,7 +1026,7 @@ QTableView {{ gridline-color: {grid} }}
pass pass
def setupSidebar(self) -> None: def setupSidebar(self) -> None:
dw = self.sidebarDockWidget = QDockWidget(_("Sidebar"), self) dw = self.sidebarDockWidget = QDockWidget(tr(TR.BROWSING_SIDEBAR), self)
dw.setFeatures(QDockWidget.DockWidgetClosable) dw.setFeatures(QDockWidget.DockWidgetClosable)
dw.setObjectName("Sidebar") dw.setObjectName("Sidebar")
dw.setAllowedAreas(Qt.LeftDockWidgetArea) dw.setAllowedAreas(Qt.LeftDockWidgetArea)
@ -1105,14 +1103,14 @@ QTableView {{ gridline-color: {grid} }}
def _stdTree(self, root) -> None: def _stdTree(self, root) -> None:
item = SidebarItem( item = SidebarItem(
_("Whole Collection"), tr(TR.BROWSING_WHOLE_COLLECTION),
":/icons/collection.svg", ":/icons/collection.svg",
self._filterFunc(""), self._filterFunc(""),
item_type=SidebarItemType.COLLECTION, item_type=SidebarItemType.COLLECTION,
) )
root.addChild(item) root.addChild(item)
item = SidebarItem( item = SidebarItem(
_("Current Deck"), tr(TR.BROWSING_CURRENT_DECK),
":/icons/deck.svg", ":/icons/deck.svg",
self._filterFunc("deck:current"), self._filterFunc("deck:current"),
item_type=SidebarItemType.CURRENT_DECK, item_type=SidebarItemType.CURRENT_DECK,
@ -1252,41 +1250,44 @@ QTableView {{ gridline-color: {grid} }}
def _commonFilters(self): def _commonFilters(self):
return self._simpleFilters( return self._simpleFilters(
((_("Whole Collection"), ""), (_("Current Deck"), "deck:current")) (
(tr(TR.BROWSING_WHOLE_COLLECTION), ""),
(tr(TR.BROWSING_CURRENT_DECK), "deck:current"),
)
) )
def _todayFilters(self): def _todayFilters(self):
subm = SubMenu(_("Today")) subm = SubMenu(tr(TR.BROWSING_TODAY))
subm.addChild( subm.addChild(
self._simpleFilters( self._simpleFilters(
( (
(_("Added Today"), "added:1"), (tr(TR.BROWSING_ADDED_TODAY), "added:1"),
(_("Studied Today"), "rated:1"), (tr(TR.BROWSING_STUDIED_TODAY), "rated:1"),
(_("Again Today"), "rated:1:1"), (tr(TR.BROWSING_AGAIN_TODAY), "rated:1:1"),
) )
) )
) )
return subm return subm
def _cardStateFilters(self): def _cardStateFilters(self):
subm = SubMenu(_("Card State")) subm = SubMenu(tr(TR.BROWSING_CARD_STATE))
subm.addChild( subm.addChild(
self._simpleFilters( self._simpleFilters(
( (
(_("New"), "is:new"), (tr(TR.ACTIONS_NEW), "is:new"),
(_("Learning"), "is:learn"), (tr(TR.SCHEDULING_LEARNING), "is:learn"),
(_("Review"), "is:review"), (tr(TR.SCHEDULING_REVIEW), "is:review"),
(tr(TR.FILTERING_IS_DUE), "is:due"), (tr(TR.FILTERING_IS_DUE), "is:due"),
None, None,
(_("Suspended"), "is:suspended"), (tr(TR.BROWSING_SUSPENDED), "is:suspended"),
(_("Buried"), "is:buried"), (tr(TR.BROWSING_BURIED), "is:buried"),
None, None,
(_("Red Flag"), "flag:1"), (tr(TR.ACTIONS_RED_FLAG), "flag:1"),
(_("Orange Flag"), "flag:2"), (tr(TR.ACTIONS_ORANGE_FLAG), "flag:2"),
(_("Green Flag"), "flag:3"), (tr(TR.ACTIONS_GREEN_FLAG), "flag:3"),
(_("Blue Flag"), "flag:4"), (tr(TR.ACTIONS_BLUE_FLAG), "flag:4"),
(_("No Flag"), "flag:0"), (tr(TR.BROWSING_NO_FLAG), "flag:0"),
(_("Any Flag"), "-flag:0"), (tr(TR.BROWSING_ANY_FLAG), "-flag:0"),
) )
) )
) )
@ -1296,9 +1297,9 @@ QTableView {{ gridline-color: {grid} }}
return label.replace("&", "&&") return label.replace("&", "&&")
def _tagFilters(self): def _tagFilters(self):
m = SubMenu(_("Tags")) m = SubMenu(tr(TR.EDITING_TAGS))
m.addItem(_("Clear Unused"), self.clearUnusedTags) m.addItem(tr(TR.BROWSING_CLEAR_UNUSED), self.clearUnusedTags)
m.addSeparator() m.addSeparator()
tagList = MenuList() tagList = MenuList()
@ -1316,7 +1317,9 @@ QTableView {{ gridline-color: {grid} }}
fullname = parent_prefix + node.name fullname = parent_prefix + node.name
if node.children: if node.children:
subm = parent.addMenu(escaped_name) subm = parent.addMenu(escaped_name)
subm.addItem(_("Filter"), self._filterFunc("deck", fullname)) subm.addItem(
tr(TR.ACTIONS_FILTER), self._filterFunc("deck", fullname)
)
subm.addSeparator() subm.addSeparator()
addDecks(subm, node.children, fullname + "::") addDecks(subm, node.children, fullname + "::")
else: else:
@ -1326,15 +1329,15 @@ QTableView {{ gridline-color: {grid} }}
ml = MenuList() ml = MenuList()
addDecks(ml, alldecks.children, "") addDecks(ml, alldecks.children, "")
root = SubMenu(_("Decks")) root = SubMenu(tr(TR.ACTIONS_DECKS))
root.addChild(ml.chunked()) root.addChild(ml.chunked())
return root return root
def _noteTypeFilters(self): def _noteTypeFilters(self):
m = SubMenu(_("Note Types")) m = SubMenu(tr(TR.NOTETYPES_NOTE_TYPES))
m.addItem(_("Manage..."), self.mw.onNoteTypes) m.addItem(tr(TR.ACTIONS_MANAGE), self.mw.onNoteTypes)
m.addSeparator() m.addSeparator()
noteTypes = MenuList() noteTypes = MenuList()
@ -1346,14 +1349,16 @@ QTableView {{ gridline-color: {grid} }}
else: else:
subm = noteTypes.addMenu(escaped_nt_name) subm = noteTypes.addMenu(escaped_nt_name)
subm.addItem(_("All Card Types"), self._filterFunc("note", nt["name"])) subm.addItem(
tr(TR.BROWSING_ALL_CARD_TYPES), self._filterFunc("note", nt["name"])
)
subm.addSeparator() subm.addSeparator()
# add templates # add templates
for c, tmpl in enumerate(nt["tmpls"]): for c, tmpl in enumerate(nt["tmpls"]):
# T: name is a card type name. n it's order in the list of card type. # T: name is a card type name. n it's order in the list of card type.
# T: this is shown in browser's filter, when seeing the list of card type of a note type. # T: this is shown in browser's filter, when seeing the list of card type of a note type.
name = _("%(n)d: %(name)s") % dict( name = tr(TR.BROWSING_ND_NAMES) % dict(
n=c + 1, name=self._escapeMenuItem(tmpl["name"]) n=c + 1, name=self._escapeMenuItem(tmpl["name"])
) )
subm.addItem( subm.addItem(
@ -1375,9 +1380,9 @@ QTableView {{ gridline-color: {grid} }}
ml.addSeparator() ml.addSeparator()
if self._currentFilterIsSaved(): if self._currentFilterIsSaved():
ml.addItem(_("Remove Current Filter..."), self._onRemoveFilter) ml.addItem(tr(TR.BROWSING_REMOVE_CURRENT_FILTER), self._onRemoveFilter)
else: else:
ml.addItem(_("Save Current Filter..."), self._onSaveFilter) ml.addItem(tr(TR.BROWSING_SAVE_CURRENT_FILTER), self._onSaveFilter)
saved = self.col.get_config("savedFilters") saved = self.col.get_config("savedFilters")
if not saved: if not saved:
@ -1390,7 +1395,7 @@ QTableView {{ gridline-color: {grid} }}
return ml return ml
def _onSaveFilter(self) -> None: def _onSaveFilter(self) -> None:
name = getOnlyText(_("Please give your filter a name:")) name = getOnlyText(tr(TR.BROWSING_PLEASE_GIVE_YOUR_FILTER_A_NAME))
if not name: if not name:
return return
filt = self.form.searchEdit.lineEdit().text() filt = self.form.searchEdit.lineEdit().text()
@ -1401,7 +1406,9 @@ QTableView {{ gridline-color: {grid} }}
def _onRemoveFilter(self): def _onRemoveFilter(self):
name = self._currentFilterIsSaved() name = self._currentFilterIsSaved()
if not askUser(_("Remove %s from your saved searches?") % name): if not askUser(
tr(TR.BROWSING_REMOVE_FROM_YOUR_SAVED_SEARCHES, val="%s") % name
):
return return
del self.col.conf["savedFilters"][name] del self.col.conf["savedFilters"][name]
self.col.setMod() self.col.setMod()
@ -1490,7 +1497,7 @@ where id in %s"""
% ids2str(sf) % ids2str(sf)
) )
if mods > 1: if mods > 1:
showInfo(_("Please select cards from only one note type.")) showInfo(tr(TR.BROWSING_PLEASE_SELECT_CARDS_FROM_ONLY_ONE))
return return
return sf return sf
@ -1544,7 +1551,7 @@ where id in %s"""
nids = self.selectedNotes() nids = self.selectedNotes()
if not nids: if not nids:
return return
self.mw.checkpoint(_("Delete Notes")) self.mw.checkpoint(tr(TR.BROWSING_DELETE_NOTES))
self.model.beginReset() self.model.beginReset()
# figure out where to place the cursor after the deletion # figure out where to place the cursor after the deletion
curRow = self.form.tableView.selectionModel().currentIndex().row() curRow = self.form.tableView.selectionModel().currentIndex().row()
@ -1590,8 +1597,8 @@ where id in %s"""
ret = StudyDeck( ret = StudyDeck(
self.mw, self.mw,
current=current, current=current,
accept=_("Move Cards"), accept=tr(TR.BROWSING_MOVE_CARDS),
title=_("Change Deck"), title=tr(TR.BROWSING_CHANGE_DECK),
help="browse", help="browse",
parent=self, parent=self,
) )
@ -1600,10 +1607,10 @@ where id in %s"""
did = self.col.decks.id(ret.name) did = self.col.decks.id(ret.name)
deck = self.col.decks.get(did) deck = self.col.decks.get(did)
if deck["dyn"]: if deck["dyn"]:
showWarning(_("Cards can't be manually moved into a filtered deck.")) showWarning(tr(TR.BROWSING_CARDS_CANT_BE_MANUALLY_MOVED_INTO))
return return
self.model.beginReset() self.model.beginReset()
self.mw.checkpoint(_("Change Deck")) self.mw.checkpoint(tr(TR.BROWSING_CHANGE_DECK))
self.col.set_deck(cids, did) self.col.set_deck(cids, did)
self.model.endReset() self.model.endReset()
self.mw.requireReset(reason=ResetReason.BrowserSetDeck, context=self) self.mw.requireReset(reason=ResetReason.BrowserSetDeck, context=self)
@ -1616,7 +1623,7 @@ where id in %s"""
def _addTags(self, tags, label, prompt, func): def _addTags(self, tags, label, prompt, func):
if prompt is None: if prompt is None:
prompt = _("Enter tags to add:") prompt = tr(TR.BROWSING_ENTER_TAGS_TO_ADD)
if tags is None: if tags is None:
(tags, r) = getTag(self, self.col, prompt) (tags, r) = getTag(self, self.col, prompt)
else: else:
@ -1626,7 +1633,7 @@ where id in %s"""
if func is None: if func is None:
func = self.col.tags.bulkAdd func = self.col.tags.bulkAdd
if label is None: if label is None:
label = _("Add Tags") label = tr(TR.BROWSING_ADD_TAGS)
if label: if label:
self.mw.checkpoint(label) self.mw.checkpoint(label)
self.model.beginReset() self.model.beginReset()
@ -1636,9 +1643,12 @@ where id in %s"""
def deleteTags(self, tags=None, label=None): def deleteTags(self, tags=None, label=None):
if label is None: if label is None:
label = _("Delete Tags") label = tr(TR.BROWSING_DELETE_TAGS)
self.addTags( self.addTags(
tags, label, _("Enter tags to delete:"), func=self.col.tags.bulkRem tags,
label,
tr(TR.BROWSING_ENTER_TAGS_TO_DELETE),
func=self.col.tags.bulkRem,
) )
def clearUnusedTags(self): def clearUnusedTags(self):
@ -1731,7 +1741,7 @@ where id in %s"""
+ ids2str(cids) + ids2str(cids)
) )
if not cids2: if not cids2:
return showInfo(_("Only new cards can be repositioned.")) return showInfo(tr(TR.BROWSING_ONLY_NEW_CARDS_CAN_BE_REPOSITIONED))
d = QDialog(self) d = QDialog(self)
d.setWindowModality(Qt.WindowModal) d.setWindowModality(Qt.WindowModal)
frm = aqt.forms.reposition.Ui_Dialog() frm = aqt.forms.reposition.Ui_Dialog()
@ -1741,14 +1751,14 @@ where id in %s"""
) )
pmin = pmin or 0 pmin = pmin or 0
pmax = pmax or 0 pmax = pmax or 0
txt = _("Queue top: %d") % pmin txt = tr(TR.BROWSING_QUEUE_TOP, val="%s") % pmin
txt += "\n" + _("Queue bottom: %d") % pmax txt += "\n" + tr(TR.BROWSING_QUEUE_BOTTOM, val="%s") % pmax
frm.label.setText(txt) frm.label.setText(txt)
frm.start.selectAll() frm.start.selectAll()
if not d.exec_(): if not d.exec_():
return return
self.model.beginReset() self.model.beginReset()
self.mw.checkpoint(_("Reposition")) self.mw.checkpoint(tr(TR.ACTIONS_REPOSITION))
self.col.sched.sortCards( self.col.sched.sortCards(
cids, cids,
start=frm.start.value(), start=frm.start.value(),
@ -1774,7 +1784,7 @@ where id in %s"""
if not d.exec_(): if not d.exec_():
return return
self.model.beginReset() self.model.beginReset()
self.mw.checkpoint(_("Reschedule")) self.mw.checkpoint(tr(TR.BROWSING_RESCHEDULE))
if frm.asNew.isChecked(): if frm.asNew.isChecked():
self.col.sched.forgetCards(self.selectedCards()) self.col.sched.forgetCards(self.selectedCards())
else: else:
@ -1881,7 +1891,7 @@ where id in %s"""
restore_is_checked(frm.ignoreCase, combo + "ignoreCase") restore_is_checked(frm.ignoreCase, combo + "ignoreCase")
frm.find.setFocus() frm.find.setFocus()
allfields = [_("All Fields")] + fields allfields = [tr(TR.BROWSING_ALL_FIELDS)] + fields
frm.field.addItems(allfields) frm.field.addItems(allfields)
restore_combo_index_for_session(frm.field, allfields, combo + "Field") restore_combo_index_for_session(frm.field, allfields, combo + "Field")
qconnect(frm.buttonBox.helpRequested, self.onFindReplaceHelp) qconnect(frm.buttonBox.helpRequested, self.onFindReplaceHelp)
@ -1906,7 +1916,7 @@ where id in %s"""
save_is_checked(frm.re, combo + "Regex") save_is_checked(frm.re, combo + "Regex")
save_is_checked(frm.ignoreCase, combo + "ignoreCase") save_is_checked(frm.ignoreCase, combo + "ignoreCase")
self.mw.checkpoint(_("Find and Replace")) self.mw.checkpoint(tr(TR.BROWSING_FIND_AND_REPLACE))
# starts progress dialog as well # starts progress dialog as well
self.model.beginReset() self.model.beginReset()
@ -1976,7 +1986,9 @@ where id in %s"""
field = fields[frm.fields.currentIndex()] field = fields[frm.fields.currentIndex()]
self.duplicatesReport(frm.webView, field, search_text, frm, web_context) self.duplicatesReport(frm.webView, field, search_text, frm, web_context)
search = frm.buttonBox.addButton(_("Search"), QDialogButtonBox.ActionRole) search = frm.buttonBox.addButton(
tr(TR.ACTIONS_SEARCH), QDialogButtonBox.ActionRole
)
qconnect(search.clicked, onClick) qconnect(search.clicked, onClick)
d.show() d.show()
@ -1985,7 +1997,7 @@ where id in %s"""
res = self.mw.col.findDupes(fname, search) res = self.mw.col.findDupes(fname, search)
if not self._dupesButton: if not self._dupesButton:
self._dupesButton = b = frm.buttonBox.addButton( self._dupesButton = b = frm.buttonBox.addButton(
_("Tag Duplicates"), QDialogButtonBox.ActionRole tr(TR.BROWSING_TAG_DUPLICATES), QDialogButtonBox.ActionRole
) )
qconnect(b.clicked, lambda: self._onTagDupes(res)) qconnect(b.clicked, lambda: self._onTagDupes(res))
t = "" t = ""
@ -1993,7 +2005,7 @@ where id in %s"""
notes = sum(len(r[1]) for r in res) notes = sum(len(r[1]) for r in res)
part1 = ngettext("%d group", "%d groups", groups) % groups part1 = ngettext("%d group", "%d groups", groups) % groups
part2 = ngettext("%d note", "%d notes", notes) % notes part2 = ngettext("%d note", "%d notes", notes) % notes
t += _("Found %(a)s across %(b)s.") % dict(a=part1, b=part2) t += tr(TR.BROWSING_FOUND_AS_ACROSS_BS) % dict(a=part1, b=part2)
t += "<p><ol>" t += "<p><ol>"
for val, nids in res: for val, nids in res:
t += ( t += (
@ -2012,15 +2024,15 @@ where id in %s"""
if not res: if not res:
return return
self.model.beginReset() self.model.beginReset()
self.mw.checkpoint(_("Tag Duplicates")) self.mw.checkpoint(tr(TR.BROWSING_TAG_DUPLICATES))
nids = set() nids = set()
for s, nidlist in res: for s, nidlist in res:
nids.update(nidlist) nids.update(nidlist)
self.col.tags.bulkAdd(list(nids), _("duplicate")) self.col.tags.bulkAdd(list(nids), tr(TR.BROWSING_DUPLICATE))
self.mw.progress.finish() self.mw.progress.finish()
self.model.endReset() self.model.endReset()
self.mw.requireReset(reason=ResetReason.BrowserTagDupes, context=self) self.mw.requireReset(reason=ResetReason.BrowserTagDupes, context=self)
tooltip(_("Notes tagged.")) tooltip(tr(TR.BROWSING_NOTES_TAGGED))
def dupeLinkClicked(self, link): def dupeLinkClicked(self, link):
self.search_for(link) self.search_for(link)
@ -2177,10 +2189,10 @@ class ChangeModel(QDialog):
map = QWidget() map = QWidget()
l = QGridLayout() l = QGridLayout()
combos = [] combos = []
targets = [x["name"] for x in dst] + [_("Nothing")] targets = [x["name"] for x in dst] + [tr(TR.BROWSING_NOTHING)]
indices = {} indices = {}
for i, x in enumerate(src): for i, x in enumerate(src):
l.addWidget(QLabel(_("Change %s to:") % x["name"]), i, 0) l.addWidget(QLabel(tr(TR.BROWSING_CHANGE_TO, val="%s") % x["name"]), i, 0)
cb = QComboBox() cb = QComboBox()
cb.addItems(targets) cb.addItems(targets)
idx = min(i, len(targets) - 1) idx = min(i, len(targets) - 1)
@ -2267,7 +2279,7 @@ Are you sure you want to continue?"""
) )
): ):
return return
self.browser.mw.checkpoint(_("Change Note Type")) self.browser.mw.checkpoint(tr(TR.BROWSING_CHANGE_NOTE_TYPE))
b = self.browser b = self.browser
b.mw.col.modSchema(check=True) b.mw.col.modSchema(check=True)
b.mw.progress.start() b.mw.progress.start()

View file

@ -60,13 +60,15 @@ class CardLayout(QDialog):
self.mobile_emulation_enabled = False self.mobile_emulation_enabled = False
self.have_autoplayed = False self.have_autoplayed = False
self.mm._remove_from_cache(self.model["id"]) self.mm._remove_from_cache(self.model["id"])
self.mw.checkpoint(_("Card Types")) self.mw.checkpoint(tr(TR.CARD_TEMPLATES_CARD_TYPES))
self.change_tracker = ChangeTracker(self.mw) self.change_tracker = ChangeTracker(self.mw)
self.setupTopArea() self.setupTopArea()
self.setupMainArea() self.setupMainArea()
self.setupButtons() self.setupButtons()
self.setupShortcuts() self.setupShortcuts()
self.setWindowTitle(_("Card Types for %s") % self.model["name"]) self.setWindowTitle(
tr(TR.CARD_TEMPLATES_CARD_TYPES_FOR, val="%s") % self.model["name"]
)
v1 = QVBoxLayout() v1 = QVBoxLayout()
v1.addWidget(self.topArea) v1.addWidget(self.topArea)
v1.addWidget(self.mainArea) v1.addWidget(self.mainArea)
@ -108,7 +110,9 @@ class CardLayout(QDialog):
self.topArea.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) self.topArea.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
self.topAreaForm = aqt.forms.clayout_top.Ui_Form() self.topAreaForm = aqt.forms.clayout_top.Ui_Form()
self.topAreaForm.setupUi(self.topArea) self.topAreaForm.setupUi(self.topArea)
self.topAreaForm.templateOptions.setText(_("Options") + " " + downArrow()) self.topAreaForm.templateOptions.setText(
tr(TR.ACTIONS_OPTIONS) + " " + downArrow()
)
qconnect(self.topAreaForm.templateOptions.clicked, self.onMore) qconnect(self.topAreaForm.templateOptions.clicked, self.onMore)
qconnect( qconnect(
self.topAreaForm.templatesBox.currentIndexChanged, self.topAreaForm.templatesBox.currentIndexChanged,
@ -240,7 +244,7 @@ class CardLayout(QDialog):
qconnect(widg.returnPressed, self.on_search_next) qconnect(widg.returnPressed, self.on_search_next)
def setup_cloze_number_box(self): def setup_cloze_number_box(self):
names = (_("Cloze %d") % n for n in self.cloze_numbers) names = (tr(TR.CARD_TEMPLATES_CLOZE, val="%s") % n for n in self.cloze_numbers)
self.pform.cloze_number_combo.addItems(names) self.pform.cloze_number_combo.addItems(names)
try: try:
idx = self.cloze_numbers.index(self.ord + 1) idx = self.cloze_numbers.index(self.ord + 1)
@ -381,28 +385,28 @@ class CardLayout(QDialog):
def setupButtons(self): def setupButtons(self):
l = self.buttons = QHBoxLayout() l = self.buttons = QHBoxLayout()
help = QPushButton(_("Help")) help = QPushButton(tr(TR.ACTIONS_HELP))
help.setAutoDefault(False) help.setAutoDefault(False)
l.addWidget(help) l.addWidget(help)
qconnect(help.clicked, self.onHelp) qconnect(help.clicked, self.onHelp)
l.addStretch() l.addStretch()
self.add_field_button = QPushButton(_("Add Field")) self.add_field_button = QPushButton(tr(TR.FIELDS_ADD_FIELD))
self.add_field_button.setAutoDefault(False) self.add_field_button.setAutoDefault(False)
l.addWidget(self.add_field_button) l.addWidget(self.add_field_button)
qconnect(self.add_field_button.clicked, self.onAddField) qconnect(self.add_field_button.clicked, self.onAddField)
if not self._isCloze(): if not self._isCloze():
flip = QPushButton(_("Flip")) flip = QPushButton(tr(TR.CARD_TEMPLATES_FLIP))
flip.setAutoDefault(False) flip.setAutoDefault(False)
l.addWidget(flip) l.addWidget(flip)
qconnect(flip.clicked, self.onFlip) qconnect(flip.clicked, self.onFlip)
l.addStretch() l.addStretch()
save = QPushButton(_("Save")) save = QPushButton(tr(TR.ACTIONS_SAVE))
save.setAutoDefault(False) save.setAutoDefault(False)
save.setShortcut(QKeySequence("Ctrl+Return")) save.setShortcut(QKeySequence("Ctrl+Return"))
l.addWidget(save) l.addWidget(save)
qconnect(save.clicked, self.accept) qconnect(save.clicked, self.accept)
close = QPushButton(_("Cancel")) close = QPushButton(tr(TR.ACTIONS_CANCEL))
close.setAutoDefault(False) close.setAutoDefault(False)
l.addWidget(close) l.addWidget(close)
qconnect(close.clicked, self.reject) qconnect(close.clicked, self.reject)
@ -548,7 +552,7 @@ class CardLayout(QDialog):
def onRemove(self): def onRemove(self):
if len(self.templates) < 2: if len(self.templates) < 2:
return showInfo(_("At least one card type is required.")) return showInfo(tr(TR.CARD_TEMPLATES_AT_LEAST_ONE_CARD_TYPE_IS))
def get_count(): def get_count():
return self.mm.template_use_count(self.model["id"], self.ord) return self.mm.template_use_count(self.model["id"], self.ord)
@ -558,7 +562,7 @@ class CardLayout(QDialog):
template = self.current_template() template = self.current_template()
cards = ngettext("%d card", "%d cards", card_cnt) % card_cnt cards = ngettext("%d card", "%d cards", card_cnt) % card_cnt
msg = _("Delete the '%(a)s' card type, and its %(b)s?") % dict( msg = tr(TR.CARD_TEMPLATES_DELETE_THE_AS_CARD_TYPE_AND) % dict(
a=template["name"], b=cards a=template["name"], b=cards
) )
if not askUser(msg): if not askUser(msg):
@ -583,7 +587,9 @@ class CardLayout(QDialog):
def onRename(self): def onRename(self):
template = self.current_template() template = self.current_template()
name = getOnlyText(_("New name:"), default=template["name"]).replace('"', "") name = getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=template["name"]).replace(
'"', ""
)
if not name.strip(): if not name.strip():
return return
@ -597,7 +603,8 @@ class CardLayout(QDialog):
template = self.current_template() template = self.current_template()
current_pos = self.templates.index(template) + 1 current_pos = self.templates.index(template) + 1
pos = getOnlyText( pos = getOnlyText(
_("Enter new card position (1...%s):") % n, default=str(current_pos) tr(TR.CARD_TEMPLATES_ENTER_NEW_CARD_POSITION_1, val="%s") % n,
default=str(current_pos),
) )
if not pos: if not pos:
return return
@ -619,7 +626,7 @@ class CardLayout(QDialog):
def _newCardName(self): def _newCardName(self):
n = len(self.templates) + 1 n = len(self.templates) + 1
while 1: while 1:
name = _("Card %d") % n name = tr(TR.CARD_TEMPLATES_CARD, val="%s") % n
if name not in [t["name"] for t in self.templates]: if name not in [t["name"] for t in self.templates]:
break break
n += 1 n += 1
@ -673,29 +680,29 @@ adjust the template manually to switch the question and answer."""
m = QMenu(self) m = QMenu(self)
if not self._isCloze(): if not self._isCloze():
a = m.addAction(_("Add Card Type...")) a = m.addAction(tr(TR.CARD_TEMPLATES_ADD_CARD_TYPE))
qconnect(a.triggered, self.onAddCard) qconnect(a.triggered, self.onAddCard)
a = m.addAction(_("Remove Card Type...")) a = m.addAction(tr(TR.CARD_TEMPLATES_REMOVE_CARD_TYPE))
qconnect(a.triggered, self.onRemove) qconnect(a.triggered, self.onRemove)
a = m.addAction(_("Rename Card Type...")) a = m.addAction(tr(TR.CARD_TEMPLATES_RENAME_CARD_TYPE))
qconnect(a.triggered, self.onRename) qconnect(a.triggered, self.onRename)
a = m.addAction(_("Reposition Card Type...")) a = m.addAction(tr(TR.CARD_TEMPLATES_REPOSITION_CARD_TYPE))
qconnect(a.triggered, self.onReorder) qconnect(a.triggered, self.onReorder)
m.addSeparator() m.addSeparator()
t = self.current_template() t = self.current_template()
if t["did"]: if t["did"]:
s = _(" (on)") s = tr(TR.CARD_TEMPLATES_ON)
else: else:
s = _(" (off)") s = tr(TR.CARD_TEMPLATES_OFF)
a = m.addAction(_("Deck Override...") + s) a = m.addAction(tr(TR.CARD_TEMPLATES_DECK_OVERRIDE) + s)
qconnect(a.triggered, self.onTargetDeck) qconnect(a.triggered, self.onTargetDeck)
a = m.addAction(_("Browser Appearance...")) a = m.addAction(tr(TR.CARD_TEMPLATES_BROWSER_APPEARANCE))
qconnect(a.triggered, self.onBrowserDisplay) qconnect(a.triggered, self.onBrowserDisplay)
m.exec_(self.topAreaForm.templateOptions.mapToGlobal(QPoint(0, 0))) m.exec_(self.topAreaForm.templateOptions.mapToGlobal(QPoint(0, 0)))

View file

@ -1,7 +1,6 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import aqt import aqt
from anki.consts import * from anki.consts import *
from anki.lang import _ from anki.lang import _
@ -50,11 +49,11 @@ class CustomStudy(QDialog):
smin = 1 smin = 1
smax = DYN_MAX_SIZE smax = DYN_MAX_SIZE
sval = 1 sval = 1
post = _("cards") post = tr(TR.CUSTOM_STUDY_CARDS)
tit = "" tit = ""
spShow = True spShow = True
typeShow = False typeShow = False
ok = _("OK") ok = tr(TR.CUSTOM_STUDY_OK)
def plus(num): def plus(num):
if num == 1000: if num == 1000:
@ -68,8 +67,10 @@ class CustomStudy(QDialog):
new, self.conf["new"]["perDay"] - self.deck["newToday"][1] new, self.conf["new"]["perDay"] - self.deck["newToday"][1]
) )
newExceeding = min(new, new - newUnderLearning) newExceeding = min(new, new - newUnderLearning)
tit = _("New cards in deck over today limit: %s") % plus(newExceeding) tit = tr(TR.CUSTOM_STUDY_NEW_CARDS_IN_DECK_OVER_TODAY, val="%s") % plus(
pre = _("Increase today's new card limit by") newExceeding
)
pre = tr(TR.CUSTOM_STUDY_INCREASE_TODAYS_NEW_CARD_LIMIT_BY)
sval = min(new, self.deck.get("extendNew", 10)) sval = min(new, self.deck.get("extendNew", 10))
smin = -DYN_MAX_SIZE smin = -DYN_MAX_SIZE
smax = newExceeding smax = newExceeding
@ -80,27 +81,29 @@ class CustomStudy(QDialog):
rev, self.conf["rev"]["perDay"] - self.deck["revToday"][1] rev, self.conf["rev"]["perDay"] - self.deck["revToday"][1]
) )
revExceeding = min(rev, rev - revUnderLearning) revExceeding = min(rev, rev - revUnderLearning)
tit = _("Reviews due in deck over today limit: %s") % plus(revExceeding) tit = tr(TR.CUSTOM_STUDY_REVIEWS_DUE_IN_DECK_OVER_TODAY, val="%s") % plus(
pre = _("Increase today's review limit by") revExceeding
)
pre = tr(TR.CUSTOM_STUDY_INCREASE_TODAYS_REVIEW_LIMIT_BY)
sval = min(rev, self.deck.get("extendRev", 10)) sval = min(rev, self.deck.get("extendRev", 10))
smin = -DYN_MAX_SIZE smin = -DYN_MAX_SIZE
smax = revExceeding smax = revExceeding
elif idx == RADIO_FORGOT: elif idx == RADIO_FORGOT:
pre = _("Review cards forgotten in last") pre = tr(TR.CUSTOM_STUDY_REVIEW_CARDS_FORGOTTEN_IN_LAST)
post = _("days") post = tr(TR.SCHEDULING_DAYS)
smax = 30 smax = 30
elif idx == RADIO_AHEAD: elif idx == RADIO_AHEAD:
pre = _("Review ahead by") pre = tr(TR.CUSTOM_STUDY_REVIEW_AHEAD_BY)
post = _("days") post = tr(TR.SCHEDULING_DAYS)
elif idx == RADIO_PREVIEW: elif idx == RADIO_PREVIEW:
pre = _("Preview new cards added in the last") pre = tr(TR.CUSTOM_STUDY_PREVIEW_NEW_CARDS_ADDED_IN_THE)
post = _("days") post = tr(TR.SCHEDULING_DAYS)
sval = 1 sval = 1
elif idx == RADIO_CRAM: elif idx == RADIO_CRAM:
pre = _("Select") pre = tr(TR.CUSTOM_STUDY_SELECT)
post = _("cards from the deck") post = tr(TR.CUSTOM_STUDY_CARDS_FROM_THE_DECK)
# tit = _("After pressing OK, you can choose which tags to include.") # tit = _("After pressing OK, you can choose which tags to include.")
ok = _("Choose Tags") ok = tr(TR.CUSTOM_STUDY_CHOOSE_TAGS)
sval = 100 sval = 100
typeShow = True typeShow = True
sp.setVisible(spShow) sp.setVisible(spShow)
@ -138,7 +141,7 @@ class CustomStudy(QDialog):
elif i == RADIO_CRAM: elif i == RADIO_CRAM:
tags = self._getTags() tags = self._getTags()
# the rest create a filtered deck # the rest create a filtered deck
cur = self.mw.col.decks.byName(_("Custom Study Session")) cur = self.mw.col.decks.byName(tr(TR.CUSTOM_STUDY_CUSTOM_STUDY_SESSION))
if cur: if cur:
if not cur["dyn"]: if not cur["dyn"]:
showInfo(tr(TR.CUSTOM_STUDY_MUST_RENAME_DECK)) showInfo(tr(TR.CUSTOM_STUDY_MUST_RENAME_DECK))
@ -150,7 +153,9 @@ class CustomStudy(QDialog):
dyn = cur dyn = cur
self.mw.col.decks.select(cur["id"]) self.mw.col.decks.select(cur["id"])
else: else:
did = self.mw.col.decks.new_filtered(_("Custom Study Session")) did = self.mw.col.decks.new_filtered(
tr(TR.CUSTOM_STUDY_CUSTOM_STUDY_SESSION)
)
dyn = self.mw.col.decks.get(did) dyn = self.mw.col.decks.get(did)
# and then set various options # and then set various options
if i == RADIO_FORGOT: if i == RADIO_FORGOT:
@ -187,7 +192,7 @@ class CustomStudy(QDialog):
# generate cards # generate cards
self.created_custom_study = True self.created_custom_study = True
if not self.mw.col.sched.rebuild_filtered_deck(dyn["id"]): if not self.mw.col.sched.rebuild_filtered_deck(dyn["id"]):
return showWarning(_("No cards matched the criteria you provided.")) return showWarning(tr(TR.CUSTOM_STUDY_NO_CARDS_MATCHED_THE_CRITERIA_YOU))
self.mw.moveToState("overview") self.mw.moveToState("overview")
QDialog.accept(self) QDialog.accept(self)

View file

@ -1,7 +1,6 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations from __future__ import annotations
from copy import deepcopy from copy import deepcopy
@ -16,7 +15,7 @@ from aqt import AnkiQt, gui_hooks
from aqt.qt import * from aqt.qt import *
from aqt.sound import av_player from aqt.sound import av_player
from aqt.toolbar import BottomBar from aqt.toolbar import BottomBar
from aqt.utils import askUser, getOnlyText, openLink, shortcut, showWarning, tr from aqt.utils import TR, askUser, getOnlyText, openLink, shortcut, showWarning, tr
class DeckBrowserBottomBar: class DeckBrowserBottomBar:
@ -79,7 +78,7 @@ class DeckBrowser:
elif cmd == "import": elif cmd == "import":
self.mw.onImport() self.mw.onImport()
elif cmd == "create": elif cmd == "create":
deck = getOnlyText(_("Name for deck:")) deck = getOnlyText(tr(TR.DECKS_NAME_FOR_DECK))
if deck: if deck:
self.mw.col.decks.id(deck) self.mw.col.decks.id(deck)
gui_hooks.sidebar_should_refresh_decks() gui_hooks.sidebar_should_refresh_decks()
@ -144,9 +143,9 @@ class DeckBrowser:
buf = """ buf = """
<tr><th colspan=5 align=start>%s</th><th class=count>%s</th> <tr><th colspan=5 align=start>%s</th><th class=count>%s</th>
<th class=count>%s</th><th class=optscol></th></tr>""" % ( <th class=count>%s</th><th class=optscol></th></tr>""" % (
_("Deck"), tr(TR.DECKS_DECK),
tr(TR.STATISTICS_DUE_COUNT), tr(TR.STATISTICS_DUE_COUNT),
_("New"), tr(TR.ACTIONS_NEW),
) )
buf += self._topLevelDragRow() buf += self._topLevelDragRow()
@ -225,13 +224,13 @@ class DeckBrowser:
def _showOptions(self, did: str) -> None: def _showOptions(self, did: str) -> None:
m = QMenu(self.mw) m = QMenu(self.mw)
a = m.addAction(_("Rename")) a = m.addAction(tr(TR.ACTIONS_RENAME))
qconnect(a.triggered, lambda b, did=did: self._rename(int(did))) qconnect(a.triggered, lambda b, did=did: self._rename(int(did)))
a = m.addAction(_("Options")) a = m.addAction(tr(TR.ACTIONS_OPTIONS))
qconnect(a.triggered, lambda b, did=did: self._options(did)) qconnect(a.triggered, lambda b, did=did: self._options(did))
a = m.addAction(_("Export")) a = m.addAction(tr(TR.ACTIONS_EXPORT))
qconnect(a.triggered, lambda b, did=did: self._export(did)) qconnect(a.triggered, lambda b, did=did: self._export(did))
a = m.addAction(_("Delete")) a = m.addAction(tr(TR.ACTIONS_DELETE))
qconnect(a.triggered, lambda b, did=did: self._delete(int(did))) qconnect(a.triggered, lambda b, did=did: self._delete(int(did)))
gui_hooks.deck_browser_will_show_options_menu(m, int(did)) gui_hooks.deck_browser_will_show_options_menu(m, int(did))
m.exec_(QCursor.pos()) m.exec_(QCursor.pos())
@ -240,10 +239,10 @@ class DeckBrowser:
self.mw.onExport(did=did) self.mw.onExport(did=did)
def _rename(self, did: int) -> None: def _rename(self, did: int) -> None:
self.mw.checkpoint(_("Rename Deck")) self.mw.checkpoint(tr(TR.ACTIONS_RENAME_DECK))
deck = self.mw.col.decks.get(did) deck = self.mw.col.decks.get(did)
oldName = deck["name"] oldName = deck["name"]
newName = getOnlyText(_("New deck name:"), default=oldName) newName = getOnlyText(tr(TR.DECKS_NEW_DECK_NAME), default=oldName)
newName = newName.replace('"', "") newName = newName.replace('"', "")
if not newName or newName == oldName: if not newName or newName == oldName:
return return
@ -277,7 +276,7 @@ class DeckBrowser:
self.show() self.show()
def _delete(self, did): def _delete(self, did):
self.mw.checkpoint(_("Delete Deck")) self.mw.checkpoint(tr(TR.DECKS_DELETE_DECK))
deck = self.mw.col.decks.get(did) deck = self.mw.col.decks.get(did)
if not deck["dyn"]: if not deck["dyn"]:
dids = [did] + [r[1] for r in self.mw.col.decks.children(did)] dids = [did] + [r[1] for r in self.mw.col.decks.children(did)]
@ -293,7 +292,7 @@ class DeckBrowser:
deck["dyn"] deck["dyn"]
or not extra or not extra
or askUser( or askUser(
(_("Are you sure you wish to delete %s?") % deck["name"]) + extra (tr(TR.DECKS_ARE_YOU_SURE_YOU_WISH_TO, val="%s") % deck["name"]) + extra
) )
): ):
self.mw.progress.start() self.mw.progress.start()
@ -305,9 +304,9 @@ class DeckBrowser:
###################################################################### ######################################################################
drawLinks = [ drawLinks = [
["", "shared", _("Get Shared")], ["", "shared", tr(TR.DECKS_GET_SHARED)],
["", "create", _("Create Deck")], ["", "create", tr(TR.DECKS_CREATE_DECK)],
["Ctrl+Shift+I", "import", _("Import File")], ["Ctrl+Shift+I", "import", tr(TR.DECKS_IMPORT_FILE)],
] ]
def _drawButtons(self): def _drawButtons(self):
@ -315,7 +314,7 @@ class DeckBrowser:
drawLinks = deepcopy(self.drawLinks) drawLinks = deepcopy(self.drawLinks)
for b in drawLinks: for b in drawLinks:
if b[0]: if b[0]:
b[0] = _("Shortcut key: %s") % shortcut(b[0]) b[0] = tr(TR.ACTIONS_SHORTCUT_KEY, val="%s") % shortcut(b[0])
buf += """ buf += """
<button title='%s' onclick='pycmd(\"%s\");'>%s</button>""" % tuple( <button title='%s' onclick='pycmd(\"%s\");'>%s</button>""" % tuple(
b b

View file

@ -1,13 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from typing import Any from typing import Any
from anki.lang import _ from anki.lang import _
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.qt import * from aqt.qt import *
from aqt.utils import shortcut from aqt.utils import TR, shortcut, tr
class DeckChooser(QHBoxLayout): class DeckChooser(QHBoxLayout):
@ -26,12 +25,12 @@ class DeckChooser(QHBoxLayout):
def setupDecks(self) -> None: def setupDecks(self) -> None:
if self.label: if self.label:
self.deckLabel = QLabel(_("Deck")) self.deckLabel = QLabel(tr(TR.DECKS_DECK))
self.addWidget(self.deckLabel) self.addWidget(self.deckLabel)
# decks box # decks box
self.deck = QPushButton(clicked=self.onDeckChange) # type: ignore self.deck = QPushButton(clicked=self.onDeckChange) # type: ignore
self.deck.setAutoDefault(False) self.deck.setAutoDefault(False)
self.deck.setToolTip(shortcut(_("Target Deck (Ctrl+D)"))) self.deck.setToolTip(shortcut(tr(TR.QT_MISC_TARGET_DECK_CTRLANDD)))
QShortcut(QKeySequence("Ctrl+D"), self.widget, activated=self.onDeckChange) # type: ignore QShortcut(QKeySequence("Ctrl+D"), self.widget, activated=self.onDeckChange) # type: ignore
self.addWidget(self.deck) self.addWidget(self.deck)
# starting label # starting label
@ -48,11 +47,13 @@ class DeckChooser(QHBoxLayout):
did = c.odid did = c.odid
else: else:
did = 1 did = 1
self.setDeckName(self.mw.col.decks.nameOrNone(did) or _("Default")) self.setDeckName(
self.mw.col.decks.nameOrNone(did) or tr(TR.QT_MISC_DEFAULT)
)
else: else:
self.setDeckName( self.setDeckName(
self.mw.col.decks.nameOrNone(self.mw.col.models.current()["did"]) self.mw.col.decks.nameOrNone(self.mw.col.models.current()["did"])
or _("Default") or tr(TR.QT_MISC_DEFAULT)
) )
# layout # layout
sizePolicy = QSizePolicy(QSizePolicy.Policy(7), QSizePolicy.Policy(0)) sizePolicy = QSizePolicy(QSizePolicy.Policy(7), QSizePolicy.Policy(0))
@ -74,7 +75,7 @@ class DeckChooser(QHBoxLayout):
if not self.mw.col.conf.get("addToCur", True): if not self.mw.col.conf.get("addToCur", True):
self.setDeckName( self.setDeckName(
self.mw.col.decks.nameOrNone(self.mw.col.models.current()["did"]) self.mw.col.decks.nameOrNone(self.mw.col.models.current()["did"])
or _("Default") or tr(TR.QT_MISC_DEFAULT)
) )
def onDeckChange(self) -> None: def onDeckChange(self) -> None:
@ -84,8 +85,8 @@ class DeckChooser(QHBoxLayout):
ret = StudyDeck( ret = StudyDeck(
self.mw, self.mw,
current=current, current=current,
accept=_("Choose"), accept=tr(TR.ACTIONS_CHOOSE),
title=_("Choose Deck"), title=tr(TR.QT_MISC_CHOOSE_DECK),
help="addingnotes", help="addingnotes",
cancel=False, cancel=False,
parent=self.widget, parent=self.widget,

View file

@ -1,7 +1,6 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from operator import itemgetter from operator import itemgetter
from typing import Any, Dict from typing import Any, Dict
@ -13,6 +12,7 @@ from anki.lang import _, ngettext
from aqt import gui_hooks from aqt import gui_hooks
from aqt.qt import * from aqt.qt import *
from aqt.utils import ( from aqt.utils import (
TR,
askUser, askUser,
getOnlyText, getOnlyText,
openHelp, openHelp,
@ -21,6 +21,7 @@ from aqt.utils import (
showInfo, showInfo,
showWarning, showWarning,
tooltip, tooltip,
tr,
) )
@ -34,7 +35,7 @@ class DeckConf(QDialog):
self.form = aqt.forms.dconf.Ui_Dialog() self.form = aqt.forms.dconf.Ui_Dialog()
self.form.setupUi(self) self.form.setupUi(self)
gui_hooks.deck_conf_did_setup_ui_form(self) gui_hooks.deck_conf_did_setup_ui_form(self)
self.mw.checkpoint(_("Options")) self.mw.checkpoint(tr(TR.ACTIONS_OPTIONS))
self.setupCombos() self.setupCombos()
self.setupConfs() self.setupConfs()
self.setWindowModality(Qt.WindowModal) self.setWindowModality(Qt.WindowModal)
@ -44,7 +45,7 @@ class DeckConf(QDialog):
self.form.buttonBox.button(QDialogButtonBox.RestoreDefaults).clicked, self.form.buttonBox.button(QDialogButtonBox.RestoreDefaults).clicked,
self.onRestore, self.onRestore,
) )
self.setWindowTitle(_("Options for %s") % self.deck["name"]) self.setWindowTitle(tr(TR.ACTIONS_OPTIONS_FOR, val="%s") % self.deck["name"])
# qt doesn't size properly with altered fonts otherwise # qt doesn't size properly with altered fonts otherwise
restoreGeom(self, "deckconf", adjustSize=True) restoreGeom(self, "deckconf", adjustSize=True)
gui_hooks.deck_conf_will_show(self) gui_hooks.deck_conf_will_show(self)
@ -86,13 +87,13 @@ class DeckConf(QDialog):
def confOpts(self): def confOpts(self):
m = QMenu(self.mw) m = QMenu(self.mw)
a = m.addAction(_("Add")) a = m.addAction(tr(TR.ACTIONS_ADD))
qconnect(a.triggered, self.addGroup) qconnect(a.triggered, self.addGroup)
a = m.addAction(_("Delete")) a = m.addAction(tr(TR.ACTIONS_DELETE))
qconnect(a.triggered, self.remGroup) qconnect(a.triggered, self.remGroup)
a = m.addAction(_("Rename")) a = m.addAction(tr(TR.ACTIONS_RENAME))
qconnect(a.triggered, self.renameGroup) qconnect(a.triggered, self.renameGroup)
a = m.addAction(_("Set for all subdecks")) a = m.addAction(tr(TR.SCHEDULING_SET_FOR_ALL_SUBDECKS))
qconnect(a.triggered, self.setChildren) qconnect(a.triggered, self.setChildren)
if not self.childDids: if not self.childDids:
a.setEnabled(False) a.setEnabled(False)
@ -118,7 +119,7 @@ class DeckConf(QDialog):
self.form.count.setText(txt) self.form.count.setText(txt)
def addGroup(self) -> None: def addGroup(self) -> None:
name = getOnlyText(_("New options group name:")) name = getOnlyText(tr(TR.SCHEDULING_NEW_OPTIONS_GROUP_NAME))
if not name: if not name:
return return
@ -134,7 +135,7 @@ class DeckConf(QDialog):
def remGroup(self) -> None: def remGroup(self) -> None:
if int(self.conf["id"]) == 1: if int(self.conf["id"]) == 1:
showInfo(_("The default configuration can't be removed."), self) showInfo(tr(TR.SCHEDULING_THE_DEFAULT_CONFIGURATION_CANT_BE_REMOVED), self)
else: else:
gui_hooks.deck_conf_will_remove_config(self, self.deck, self.conf) gui_hooks.deck_conf_will_remove_config(self, self.deck, self.conf)
self.mw.col.modSchema(check=True) self.mw.col.modSchema(check=True)
@ -145,7 +146,7 @@ class DeckConf(QDialog):
def renameGroup(self) -> None: def renameGroup(self) -> None:
old = self.conf["name"] old = self.conf["name"]
name = getOnlyText(_("New name:"), default=old) name = getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=old)
if not name or name == old: if not name or name == old:
return return
@ -156,7 +157,7 @@ class DeckConf(QDialog):
def setChildren(self): def setChildren(self):
if not askUser( if not askUser(
_("Set all decks below %s to this option group?") % self.deck["name"] tr(TR.SCHEDULING_SET_ALL_DECKS_BELOW_TO, val="%s") % self.deck["name"]
): ):
return return
for did in self.childDids: for did in self.childDids:
@ -194,7 +195,7 @@ class DeckConf(QDialog):
lim = x lim = x
else: else:
lim = min(x, lim) lim = min(x, lim)
return _("(parent limit: %d)") % lim return tr(TR.SCHEDULING_PARENT_LIMIT, val="%s") % lim
def loadConf(self): def loadConf(self):
self.conf = self.mw.col.decks.confForDid(self.deck["id"]) self.conf = self.mw.col.decks.confForDid(self.deck["id"])
@ -273,10 +274,10 @@ class DeckConf(QDialog):
ret.append(i) ret.append(i)
except: except:
# invalid, don't update # invalid, don't update
showWarning(_("Steps must be numbers.")) showWarning(tr(TR.SCHEDULING_STEPS_MUST_BE_NUMBERS))
return return
if len(ret) < minSize: if len(ret) < minSize:
showWarning(_("At least one step is required.")) showWarning(tr(TR.SCHEDULING_AT_LEAST_ONE_STEP_IS_REQUIRED))
return return
conf[key] = ret conf[key] = ret

View file

@ -6,7 +6,7 @@ from typing import List, Optional
import aqt import aqt
from anki.lang import _ from anki.lang import _
from aqt.qt import * from aqt.qt import *
from aqt.utils import askUser, openHelp, restoreGeom, saveGeom, showWarning from aqt.utils import TR, askUser, openHelp, restoreGeom, saveGeom, showWarning, tr
class DeckConf(QDialog): class DeckConf(QDialog):
@ -18,14 +18,14 @@ class DeckConf(QDialog):
self.form = aqt.forms.dyndconf.Ui_Dialog() self.form = aqt.forms.dyndconf.Ui_Dialog()
self.form.setupUi(self) self.form.setupUi(self)
if first: if first:
label = _("Build") label = tr(TR.DECKS_BUILD)
else: else:
label = _("Rebuild") label = tr(TR.ACTIONS_REBUILD)
self.ok = self.form.buttonBox.addButton(label, QDialogButtonBox.AcceptRole) self.ok = self.form.buttonBox.addButton(label, QDialogButtonBox.AcceptRole)
self.mw.checkpoint(_("Options")) self.mw.checkpoint(tr(TR.ACTIONS_OPTIONS))
self.setWindowModality(Qt.WindowModal) self.setWindowModality(Qt.WindowModal)
qconnect(self.form.buttonBox.helpRequested, lambda: openHelp("filtered-decks")) qconnect(self.form.buttonBox.helpRequested, lambda: openHelp("filtered-decks"))
self.setWindowTitle(_("Options for %s") % self.deck["name"]) self.setWindowTitle(tr(TR.ACTIONS_OPTIONS_FOR, val="%s") % self.deck["name"])
restoreGeom(self, "dyndeckconf") restoreGeom(self, "dyndeckconf")
self.initialSetup() self.initialSetup()
self.loadConf() self.loadConf()
@ -154,9 +154,9 @@ it?"""
ret.append(i) ret.append(i)
except: except:
# invalid, don't update # invalid, don't update
showWarning(_("Steps must be numbers.")) showWarning(tr(TR.SCHEDULING_STEPS_MUST_BE_NUMBERS))
return None return None
if len(ret) < minSize: if len(ret) < minSize:
showWarning(_("At least one step is required.")) showWarning(tr(TR.SCHEDULING_AT_LEAST_ONE_STEP_IS_REQUIRED))
return None return None
return ret return ret

View file

@ -1,13 +1,12 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import aqt.editor import aqt.editor
from anki.lang import _ from anki.lang import _
from aqt import gui_hooks from aqt import gui_hooks
from aqt.main import ResetReason from aqt.main import ResetReason
from aqt.qt import * from aqt.qt import *
from aqt.utils import restoreGeom, saveGeom, tooltip from aqt.utils import TR, restoreGeom, saveGeom, tooltip, tr
class EditCurrent(QDialog): class EditCurrent(QDialog):
@ -17,7 +16,7 @@ class EditCurrent(QDialog):
self.mw = mw self.mw = mw
self.form = aqt.forms.editcurrent.Ui_Dialog() self.form = aqt.forms.editcurrent.Ui_Dialog()
self.form.setupUi(self) self.form.setupUi(self)
self.setWindowTitle(_("Edit Current")) self.setWindowTitle(tr(TR.EDITING_EDIT_CURRENT))
self.setMinimumHeight(400) self.setMinimumHeight(400)
self.setMinimumWidth(250) self.setMinimumWidth(250)
self.form.buttonBox.button(QDialogButtonBox.Close).setShortcut( self.form.buttonBox.button(QDialogButtonBox.Close).setShortcut(

View file

@ -31,6 +31,7 @@ from aqt.qt import *
from aqt.sound import av_player, getAudio from aqt.sound import av_player, getAudio
from aqt.theme import theme_manager from aqt.theme import theme_manager
from aqt.utils import ( from aqt.utils import (
TR,
getFile, getFile,
openHelp, openHelp,
qtMenuShortcutWorkaround, qtMenuShortcutWorkaround,
@ -40,6 +41,7 @@ from aqt.utils import (
showInfo, showInfo,
showWarning, showWarning,
tooltip, tooltip,
tr,
) )
from aqt.webview import AnkiWebView from aqt.webview import AnkiWebView
@ -122,16 +124,16 @@ class Editor:
self._addButton( self._addButton(
None, None,
"fields", "fields",
_("Customize Fields"), tr(TR.EDITING_CUSTOMIZE_FIELDS),
_("Fields") + "...", tr(TR.EDITING_FIELDS) + "...",
disables=False, disables=False,
rightside=False, rightside=False,
), ),
self._addButton( self._addButton(
None, None,
"cards", "cards",
_("Customize Card Templates (Ctrl+L)"), tr(TR.EDITING_CUSTOMIZE_CARD_TEMPLATES_CTRLANDL),
_("Cards") + "...", tr(TR.EDITING_CARDS) + "...",
disables=False, disables=False,
rightside=False, rightside=False,
), ),
@ -140,22 +142,37 @@ class Editor:
gui_hooks.editor_did_init_left_buttons(lefttopbtns, self) gui_hooks.editor_did_init_left_buttons(lefttopbtns, self)
righttopbtns: List[str] = [ righttopbtns: List[str] = [
self._addButton("text_bold", "bold", _("Bold text (Ctrl+B)"), id="bold"),
self._addButton( self._addButton(
"text_italic", "italic", _("Italic text (Ctrl+I)"), id="italic" "text_bold", "bold", tr(TR.EDITING_BOLD_TEXT_CTRLANDB), id="bold"
), ),
self._addButton( self._addButton(
"text_under", "underline", _("Underline text (Ctrl+U)"), id="underline" "text_italic",
"italic",
tr(TR.EDITING_ITALIC_TEXT_CTRLANDI),
id="italic",
), ),
self._addButton( self._addButton(
"text_super", "super", _("Superscript (Ctrl++)"), id="superscript" "text_under",
"underline",
tr(TR.EDITING_UNDERLINE_TEXT_CTRLANDU),
id="underline",
),
self._addButton(
"text_super",
"super",
tr(TR.EDITING_SUPERSCRIPT_CTRLANDAND),
id="superscript",
),
self._addButton(
"text_sub", "sub", tr(TR.EDITING_SUBSCRIPT_CTRLAND), id="subscript"
),
self._addButton(
"text_clear", "clear", tr(TR.EDITING_REMOVE_FORMATTING_CTRLANDR)
), ),
self._addButton("text_sub", "sub", _("Subscript (Ctrl+=)"), id="subscript"),
self._addButton("text_clear", "clear", _("Remove formatting (Ctrl+R)")),
self._addButton( self._addButton(
None, None,
"colour", "colour",
_("Set foreground colour (F7)"), tr(TR.EDITING_SET_FOREGROUND_COLOUR_F7),
""" """
<div id="forecolor" <div id="forecolor"
style="display: inline-block; background: #000; border-radius: 5px;" style="display: inline-block; background: #000; border-radius: 5px;"
@ -165,17 +182,19 @@ class Editor:
self._addButton( self._addButton(
None, None,
"changeCol", "changeCol",
_("Change colour (F8)"), tr(TR.EDITING_CHANGE_COLOUR_F8),
""" """
<div style="display: inline-block; border-radius: 5px;" <div style="display: inline-block; border-radius: 5px;"
class="topbut rainbow" class="topbut rainbow"
>""", >""",
), ),
self._addButton("text_cloze", "cloze", _("Cloze deletion (Ctrl+Shift+C)")),
self._addButton( self._addButton(
"paperclip", "attach", _("Attach pictures/audio/video (F3)") "text_cloze", "cloze", tr(TR.EDITING_CLOZE_DELETION_CTRLANDSHIFTANDC)
), ),
self._addButton("media-record", "record", _("Record audio (F5)")), self._addButton(
"paperclip", "attach", tr(TR.EDITING_ATTACH_PICTURESAUDIOVIDEO_F3)
),
self._addButton("media-record", "record", tr(TR.EDITING_RECORD_AUDIO_F5)),
self._addButton("more", "more"), self._addButton("more", "more"),
] ]
@ -197,7 +216,7 @@ class Editor:
bgcol = self.mw.app.palette().window().color().name() # type: ignore bgcol = self.mw.app.palette().window().color().name() # type: ignore
# then load page # then load page
self.web.stdHtml( self.web.stdHtml(
_html % (bgcol, bgcol, topbuts, _("Show Duplicates")), _html % (bgcol, bgcol, topbuts, tr(TR.EDITING_SHOW_DUPLICATES)),
css=["css/editor.css"], css=["css/editor.css"],
js=["js/vendor/jquery.js", "js/editor.js"], js=["js/vendor/jquery.js", "js/editor.js"],
context=self, context=self,
@ -571,11 +590,13 @@ class Editor:
tb.setSpacing(12) tb.setSpacing(12)
tb.setContentsMargins(2, 6, 2, 6) tb.setContentsMargins(2, 6, 2, 6)
# tags # tags
l = QLabel(_("Tags")) l = QLabel(tr(TR.EDITING_TAGS))
tb.addWidget(l, 1, 0) tb.addWidget(l, 1, 0)
self.tags = aqt.tagedit.TagEdit(self.widget) self.tags = aqt.tagedit.TagEdit(self.widget)
qconnect(self.tags.lostFocus, self.saveTags) qconnect(self.tags.lostFocus, self.saveTags)
self.tags.setToolTip(shortcut(_("Jump to tags with Ctrl+Shift+T"))) self.tags.setToolTip(
shortcut(tr(TR.EDITING_JUMP_TO_TAGS_WITH_CTRLANDSHIFTANDT))
)
border = theme_manager.str_color("border") border = theme_manager.str_color("border")
self.tags.setStyleSheet(f"border: 1px solid {border}") self.tags.setStyleSheet(f"border: 1px solid {border}")
tb.addWidget(self.tags, 1, 1) tb.addWidget(self.tags, 1, 1)
@ -708,12 +729,12 @@ to a cloze type first, via 'Notes>Change Note Type'"""
extension_filter = " ".join( extension_filter = " ".join(
"*." + extension for extension in sorted(itertools.chain(pics, audio)) "*." + extension for extension in sorted(itertools.chain(pics, audio))
) )
key = _("Media") + " (" + extension_filter + ")" key = tr(TR.EDITING_MEDIA) + " (" + extension_filter + ")"
def accept(file): def accept(file):
self.addMedia(file, canDelete=True) self.addMedia(file, canDelete=True)
file = getFile(self.widget, _("Add Media"), accept, key, key="media") file = getFile(self.widget, tr(TR.EDITING_ADD_MEDIA), accept, key, key="media")
self.parentWindow.activateWindow() self.parentWindow.activateWindow()
def addMedia(self, path, canDelete=False): def addMedia(self, path, canDelete=False):
@ -746,7 +767,7 @@ to a cloze type first, via 'Notes>Change Note Type'"""
file = getAudio(self.widget) file = getAudio(self.widget)
except Exception as e: except Exception as e:
showWarning( showWarning(
_("Couldn't record audio. Have you installed 'lame'?") tr(TR.EDITING_COULDNT_RECORD_AUDIO_HAVE_YOU_INSTALLED)
+ "\n\n" + "\n\n"
+ repr(str(e)) + repr(str(e))
) )
@ -845,13 +866,14 @@ to a cloze type first, via 'Notes>Change Note Type'"""
with client.get(url) as response: with client.get(url) as response:
if response.status_code != 200: if response.status_code != 200:
error_msg = ( error_msg = (
_("Unexpected response code: %s") % response.status_code tr(TR.QT_MISC_UNEXPECTED_RESPONSE_CODE, val="%s")
% response.status_code
) )
return None return None
filecontents = response.content filecontents = response.content
content_type = response.headers.get("content-type") content_type = response.headers.get("content-type")
except (urllib.error.URLError, requests.exceptions.RequestException) as e: except (urllib.error.URLError, requests.exceptions.RequestException) as e:
error_msg = _("An error occurred while opening %s") % e error_msg = tr(TR.EDITING_AN_ERROR_OCCURRED_WHILE_OPENING, val="%s") % e
return None return None
finally: finally:
self.mw.progress.finish() self.mw.progress.finish()
@ -948,13 +970,17 @@ to a cloze type first, via 'Notes>Change Note Type'"""
m = QMenu(self.mw) m = QMenu(self.mw)
for text, handler, shortcut in ( for text, handler, shortcut in (
(_("MathJax inline"), self.insertMathjaxInline, "Ctrl+M, M"), (tr(TR.EDITING_MATHJAX_INLINE), self.insertMathjaxInline, "Ctrl+M, M"),
(_("MathJax block"), self.insertMathjaxBlock, "Ctrl+M, E"), (tr(TR.EDITING_MATHJAX_BLOCK), self.insertMathjaxBlock, "Ctrl+M, E"),
(_("MathJax chemistry"), self.insertMathjaxChemistry, "Ctrl+M, C"), (
(_("LaTeX"), self.insertLatex, "Ctrl+T, T"), tr(TR.EDITING_MATHJAX_CHEMISTRY),
(_("LaTeX equation"), self.insertLatexEqn, "Ctrl+T, E"), self.insertMathjaxChemistry,
(_("LaTeX math env."), self.insertLatexMathEnv, "Ctrl+T, M"), "Ctrl+M, C",
(_("Edit HTML"), self.onHtmlEdit, "Ctrl+Shift+X"), ),
(tr(TR.EDITING_LATEX), self.insertLatex, "Ctrl+T, T"),
(tr(TR.EDITING_LATEX_EQUATION), self.insertLatexEqn, "Ctrl+T, E"),
(tr(TR.EDITING_LATEX_MATH_ENV), self.insertLatexMathEnv, "Ctrl+T, M"),
(tr(TR.EDITING_EDIT_HTML), self.onHtmlEdit, "Ctrl+Shift+X"),
): ):
a = m.addAction(text) a = m.addAction(text)
qconnect(a.triggered, handler) qconnect(a.triggered, handler)
@ -1204,11 +1230,11 @@ class EditorWebView(AnkiWebView):
def contextMenuEvent(self, evt: QContextMenuEvent) -> None: def contextMenuEvent(self, evt: QContextMenuEvent) -> None:
m = QMenu(self) m = QMenu(self)
a = m.addAction(_("Cut")) a = m.addAction(tr(TR.EDITING_CUT))
qconnect(a.triggered, self.onCut) qconnect(a.triggered, self.onCut)
a = m.addAction(_("Copy")) a = m.addAction(tr(TR.ACTIONS_COPY))
qconnect(a.triggered, self.onCopy) qconnect(a.triggered, self.onCopy)
a = m.addAction(_("Paste")) a = m.addAction(tr(TR.EDITING_PASTE))
qconnect(a.triggered, self.onPaste) qconnect(a.triggered, self.onPaste)
gui_hooks.editor_will_show_context_menu(self, m) gui_hooks.editor_will_show_context_menu(self, m)
m.popup(QCursor.pos()) m.popup(QCursor.pos())

View file

@ -77,11 +77,7 @@ your system's temporary folder may be incorrect."""
if "abortSchemaMod" in error: if "abortSchemaMod" in error:
return return
if "10013" in error: if "10013" in error:
return showWarning( return showWarning(tr(TR.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."
)
)
if "no default input" in error.lower(): if "no default input" in error.lower():
return showWarning( return showWarning(
_( _(
@ -94,11 +90,7 @@ your system's temporary folder may be incorrect."""
if "Beautiful Soup is not an HTTP client" in error: if "Beautiful Soup is not an HTTP client" in error:
return return
if "database or disk is full" in error or "Errno 28" in error: if "database or disk is full" in error or "Errno 28" in error:
return showWarning( return showWarning(tr(TR.QT_MISC_YOUR_COMPUTERS_STORAGE_MAY_BE_FULL))
_(
"Your computer's storage may be full. Please delete some unneeded files, then try again."
)
)
if "disk I/O error" in error: if "disk I/O error" in error:
showWarning(markdown(tr(TR.ERRORS_ACCESSING_DB))) showWarning(markdown(tr(TR.ERRORS_ACCESSING_DB)))
return return

View file

@ -14,7 +14,7 @@ from anki import hooks
from anki.exporting import Exporter, exporters from anki.exporting import Exporter, exporters
from anki.lang import _, ngettext from anki.lang import _, ngettext
from aqt.qt import * from aqt.qt import *
from aqt.utils import checkInvalidFilename, getSaveFile, showWarning, tooltip from aqt.utils import TR, checkInvalidFilename, getSaveFile, showWarning, tooltip, tr
class ExportDialog(QDialog): class ExportDialog(QDialog):
@ -49,13 +49,13 @@ class ExportDialog(QDialog):
self.exporterChanged(idx) self.exporterChanged(idx)
# deck list # deck list
if self.cids is None: if self.cids is None:
self.decks = [_("All Decks")] self.decks = [tr(TR.EXPORTING_ALL_DECKS)]
self.decks.extend(d.name for d in self.col.decks.all_names_and_ids()) self.decks.extend(d.name for d in self.col.decks.all_names_and_ids())
else: else:
self.decks = [_("Selected Notes")] self.decks = [tr(TR.EXPORTING_SELECTED_NOTES)]
self.frm.deck.addItems(self.decks) self.frm.deck.addItems(self.decks)
# save button # save button
b = QPushButton(_("Export...")) b = QPushButton(tr(TR.EXPORTING_EXPORT))
self.frm.buttonBox.addButton(b, QDialogButtonBox.AcceptRole) self.frm.buttonBox.addButton(b, QDialogButtonBox.AcceptRole)
# set default option if accessed through deck button # set default option if accessed through deck button
if did: if did:
@ -107,7 +107,7 @@ class ExportDialog(QDialog):
self.exporter.did = self.col.decks.id(name) self.exporter.did = self.col.decks.id(name)
if self.isVerbatim: if self.isVerbatim:
name = time.strftime("-%Y-%m-%d@%H-%M-%S", time.localtime(time.time())) name = time.strftime("-%Y-%m-%d@%H-%M-%S", time.localtime(time.time()))
deck_name = _("collection") + name deck_name = tr(TR.EXPORTING_COLLECTION) + name
else: else:
# Get deck name and remove invalid filename characters # Get deck name and remove invalid filename characters
deck_name = self.decks[self.frm.deck.currentIndex()] deck_name = self.decks[self.frm.deck.currentIndex()]
@ -121,7 +121,7 @@ class ExportDialog(QDialog):
while 1: while 1:
file = getSaveFile( file = getSaveFile(
self, self,
_("Export"), tr(TR.ACTIONS_EXPORT),
"export", "export",
key_str, key_str,
self.exporter.ext, self.exporter.ext,
@ -142,7 +142,7 @@ class ExportDialog(QDialog):
f = open(file, "wb") f = open(file, "wb")
f.close() f.close()
except (OSError, IOError) as e: except (OSError, IOError) as e:
showWarning(_("Couldn't save file: %s") % str(e)) showWarning(tr(TR.EXPORTING_COULDNT_SAVE_FILE, val="%s") % str(e))
else: else:
os.unlink(file) os.unlink(file)
@ -174,7 +174,7 @@ class ExportDialog(QDialog):
def on_export_finished(self): def on_export_finished(self):
if self.isVerbatim: if self.isVerbatim:
msg = _("Collection exported.") msg = tr(TR.EXPORTING_COLLECTION_EXPORTED)
self.mw.reopen() self.mw.reopen()
else: else:
if self.isTextNote: if self.isTextNote:

View file

@ -9,7 +9,7 @@ from anki.rsbackend import TemplateError
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.qt import * from aqt.qt import *
from aqt.schema_change_tracker import ChangeTracker from aqt.schema_change_tracker import ChangeTracker
from aqt.utils import askUser, getOnlyText, openHelp, showWarning, tooltip from aqt.utils import TR, askUser, getOnlyText, openHelp, showWarning, tooltip, tr
class FieldDialog(QDialog): class FieldDialog(QDialog):
@ -20,11 +20,11 @@ class FieldDialog(QDialog):
self.mm = self.mw.col.models self.mm = self.mw.col.models
self.model = nt self.model = nt
self.mm._remove_from_cache(self.model["id"]) self.mm._remove_from_cache(self.model["id"])
self.mw.checkpoint(_("Fields")) self.mw.checkpoint(tr(TR.EDITING_FIELDS))
self.change_tracker = ChangeTracker(self.mw) self.change_tracker = ChangeTracker(self.mw)
self.form = aqt.forms.fields.Ui_Dialog() self.form = aqt.forms.fields.Ui_Dialog()
self.form.setupUi(self) self.form.setupUi(self)
self.setWindowTitle(_("Fields for %s") % self.model["name"]) self.setWindowTitle(tr(TR.FIELDS_FIELDS_FOR, val="%s") % self.model["name"])
self.form.buttonBox.button(QDialogButtonBox.Help).setAutoDefault(False) self.form.buttonBox.button(QDialogButtonBox.Help).setAutoDefault(False)
self.form.buttonBox.button(QDialogButtonBox.Cancel).setAutoDefault(False) self.form.buttonBox.button(QDialogButtonBox.Cancel).setAutoDefault(False)
self.form.buttonBox.button(QDialogButtonBox.Save).setAutoDefault(False) self.form.buttonBox.button(QDialogButtonBox.Save).setAutoDefault(False)
@ -87,14 +87,14 @@ class FieldDialog(QDialog):
if ignoreOrd is not None and f["ord"] == ignoreOrd: if ignoreOrd is not None and f["ord"] == ignoreOrd:
continue continue
if f["name"] == txt: if f["name"] == txt:
showWarning(_("That field name is already used.")) showWarning(tr(TR.FIELDS_THAT_FIELD_NAME_IS_ALREADY_USED))
return return
return txt return txt
def onRename(self): def onRename(self):
idx = self.currentIdx idx = self.currentIdx
f = self.model["flds"][idx] f = self.model["flds"][idx]
name = self._uniqueName(_("New name:"), self.currentIdx, f["name"]) name = self._uniqueName(tr(TR.ACTIONS_NEW_NAME), self.currentIdx, f["name"])
if not name: if not name:
return return
@ -107,7 +107,7 @@ class FieldDialog(QDialog):
self.form.fieldList.setCurrentRow(idx) self.form.fieldList.setCurrentRow(idx)
def onAdd(self): def onAdd(self):
name = self._uniqueName(_("Field name:")) name = self._uniqueName(tr(TR.FIELDS_FIELD_NAME))
if not name: if not name:
return return
if not self.change_tracker.mark_schema(): if not self.change_tracker.mark_schema():
@ -120,10 +120,10 @@ class FieldDialog(QDialog):
def onDelete(self): def onDelete(self):
if len(self.model["flds"]) < 2: if len(self.model["flds"]) < 2:
return showWarning(_("Notes require at least one field.")) return showWarning(tr(TR.FIELDS_NOTES_REQUIRE_AT_LEAST_ONE_FIELD))
count = self.mm.useCount(self.model) count = self.mm.useCount(self.model)
c = ngettext("%d note", "%d notes", count) % count c = ngettext("%d note", "%d notes", count) % count
if not askUser(_("Delete field from %s?") % c): if not askUser(tr(TR.FIELDS_DELETE_FIELD_FROM, val="%s") % c):
return return
if not self.change_tracker.mark_schema(): if not self.change_tracker.mark_schema():
return return
@ -137,7 +137,9 @@ class FieldDialog(QDialog):
def onPosition(self, delta=-1): def onPosition(self, delta=-1):
idx = self.currentIdx idx = self.currentIdx
l = len(self.model["flds"]) l = len(self.model["flds"])
txt = getOnlyText(_("New position (1...%d):") % l, default=str(idx + 1)) txt = getOnlyText(
tr(TR.FIELDS_NEW_POSITION_1, val="%s") % l, default=str(idx + 1)
)
if not txt: if not txt:
return return
try: try:

View file

@ -1,7 +1,6 @@
# coding=utf-8 # coding=utf-8
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import json import json
import os import os
import re import re
@ -43,14 +42,14 @@ class ChangeMap(QDialog):
n = 0 n = 0
setCurrent = False setCurrent = False
for field in self.model["flds"]: for field in self.model["flds"]:
item = QListWidgetItem(_("Map to %s") % field["name"]) item = QListWidgetItem(tr(TR.IMPORTING_MAP_TO, val="%s") % field["name"])
self.frm.fields.addItem(item) self.frm.fields.addItem(item)
if current == field["name"]: if current == field["name"]:
setCurrent = True setCurrent = True
self.frm.fields.setCurrentRow(n) self.frm.fields.setCurrentRow(n)
n += 1 n += 1
self.frm.fields.addItem(QListWidgetItem(_("Map to Tags"))) self.frm.fields.addItem(QListWidgetItem(tr(TR.IMPORTING_MAP_TO_TAGS)))
self.frm.fields.addItem(QListWidgetItem(_("Ignore field"))) self.frm.fields.addItem(QListWidgetItem(tr(TR.IMPORTING_IGNORE_FIELD)))
if not setCurrent: if not setCurrent:
if current == "_tags": if current == "_tags":
self.frm.fields.setCurrentRow(n) self.frm.fields.setCurrentRow(n)
@ -100,7 +99,7 @@ class ImportDialog(QDialog):
self.frm.tagModified.setText(self.mw.pm.profile.get("tagModified", "")) self.frm.tagModified.setText(self.mw.pm.profile.get("tagModified", ""))
self.frm.tagModified.setCol(self.mw.col) self.frm.tagModified.setCol(self.mw.col)
# import button # import button
b = QPushButton(_("Import")) b = QPushButton(tr(TR.ACTIONS_IMPORT))
self.frm.buttonBox.addButton(b, QDialogButtonBox.AcceptRole) self.frm.buttonBox.addButton(b, QDialogButtonBox.AcceptRole)
self.exec_() self.exec_()
@ -156,24 +155,24 @@ you can enter it here. Use \\t to represent tab."""
else: else:
d = self.importer.dialect.delimiter d = self.importer.dialect.delimiter
if d == "\t": if d == "\t":
d = _("Tab") d = tr(TR.IMPORTING_TAB)
elif d == ",": elif d == ",":
d = _("Comma") d = tr(TR.IMPORTING_COMMA)
elif d == " ": elif d == " ":
d = _("Space") d = tr(TR.STUDYING_SPACE)
elif d == ";": elif d == ";":
d = _("Semicolon") d = tr(TR.IMPORTING_SEMICOLON)
elif d == ":": elif d == ":":
d = _("Colon") d = tr(TR.IMPORTING_COLON)
else: else:
d = repr(d) d = repr(d)
txt = _("Fields separated by: %s") % d txt = tr(TR.IMPORTING_FIELDS_SEPARATED_BY, val="%s") % d
self.frm.autoDetect.setText(txt) self.frm.autoDetect.setText(txt)
def accept(self): def accept(self):
self.importer.mapping = self.mapping self.importer.mapping = self.mapping
if not self.importer.mappingOk(): if not self.importer.mappingOk():
showWarning(_("The first field of the note type must be mapped.")) showWarning(tr(TR.IMPORTING_THE_FIRST_FIELD_OF_THE_NOTE))
return return
self.importer.importMode = self.frm.importMode.currentIndex() self.importer.importMode = self.frm.importMode.currentIndex()
self.mw.pm.profile["importMode"] = self.importer.importMode self.mw.pm.profile["importMode"] = self.importer.importMode
@ -186,7 +185,7 @@ you can enter it here. Use \\t to represent tab."""
self.mw.col.models.save(self.importer.model, updateReqs=False) self.mw.col.models.save(self.importer.model, updateReqs=False)
self.mw.col.decks.select(did) self.mw.col.decks.select(did)
self.mw.progress.start() self.mw.progress.start()
self.mw.checkpoint(_("Import")) self.mw.checkpoint(tr(TR.ACTIONS_IMPORT))
def on_done(future: Future): def on_done(future: Future):
self.mw.progress.finish() self.mw.progress.finish()
@ -208,7 +207,7 @@ you can enter it here. Use \\t to represent tab."""
showText(msg) showText(msg)
return return
else: else:
txt = _("Importing complete.") + "\n" txt = tr(TR.IMPORTING_IMPORTING_COMPLETE) + "\n"
if self.importer.log: if self.importer.log:
txt += "\n".join(self.importer.log) txt += "\n".join(self.importer.log)
self.close() self.close()
@ -248,16 +247,16 @@ you can enter it here. Use \\t to represent tab."""
self.grid.setSpacing(6) self.grid.setSpacing(6)
fields = self.importer.fields() fields = self.importer.fields()
for num in range(len(self.mapping)): for num in range(len(self.mapping)):
text = _("Field <b>%d</b> of file is:") % (num + 1) text = tr(TR.IMPORTING_FIELD_OF_FILE_IS, val="%s") % (num + 1)
self.grid.addWidget(QLabel(text), num, 0) self.grid.addWidget(QLabel(text), num, 0)
if self.mapping[num] == "_tags": if self.mapping[num] == "_tags":
text = _("mapped to <b>Tags</b>") text = tr(TR.IMPORTING_MAPPED_TO_TAGS)
elif self.mapping[num]: elif self.mapping[num]:
text = _("mapped to <b>%s</b>") % self.mapping[num] text = tr(TR.IMPORTING_MAPPED_TO, val="%s") % self.mapping[num]
else: else:
text = _("<ignored>") text = tr(TR.IMPORTING_IGNORED)
self.grid.addWidget(QLabel(text), num, 1) self.grid.addWidget(QLabel(text), num, 1)
button = QPushButton(_("Change")) button = QPushButton(tr(TR.IMPORTING_CHANGE))
self.grid.addWidget(button, num, 2) self.grid.addWidget(button, num, 2)
qconnect(button.clicked, lambda _, s=self, n=num: s.changeMappingNum(n)) qconnect(button.clicked, lambda _, s=self, n=num: s.changeMappingNum(n))
@ -308,7 +307,7 @@ def showUnicodeWarning():
def onImport(mw): def onImport(mw):
filt = ";;".join([x[0] for x in importing.Importers]) filt = ";;".join([x[0] for x in importing.Importers])
file = getFile(mw, _("Import"), None, key="import", filter=filt) file = getFile(mw, tr(TR.ACTIONS_IMPORT), None, key="import", filter=filt)
if not file: if not file:
return return
file = str(file) file = str(file)
@ -316,18 +315,10 @@ def onImport(mw):
head, ext = os.path.splitext(file) head, ext = os.path.splitext(file)
ext = ext.lower() ext = ext.lower()
if ext == ".anki": if ext == ".anki":
showInfo( showInfo(tr(TR.IMPORTING_ANKI_FILES_ARE_FROM_A_VERY))
_(
".anki files are from a very old version of Anki. You can import them with Anki 2.0, available on the Anki website."
)
)
return return
elif ext == ".anki2": elif ext == ".anki2":
showInfo( showInfo(tr(TR.IMPORTING_ANKI2_FILES_ARE_NOT_DIRECTLY_IMPORTABLE))
_(
".anki2 files are not directly importable - please import the .apkg or .zip file you have received instead."
)
)
return return
importFile(mw, file) importFile(mw, file)
@ -364,7 +355,7 @@ def importFile(mw, file):
mw.progress.finish() mw.progress.finish()
msg = repr(str(e)) msg = repr(str(e))
if msg == "'unknownFormat'": if msg == "'unknownFormat'":
showWarning(_("Unknown file format.")) showWarning(tr(TR.IMPORTING_UNKNOWN_FILE_FORMAT))
else: else:
msg = tr(TR.IMPORTING_FAILED_DEBUG_INFO) + "\n" msg = tr(TR.IMPORTING_FAILED_DEBUG_INFO) + "\n"
msg += str(traceback.format_exc()) msg += str(traceback.format_exc())
@ -513,7 +504,7 @@ def _replaceWithApkg(mw, filename, backup):
future.result() future.result()
except Exception as e: except Exception as e:
print(e) print(e)
showWarning(_("The provided file is not a valid .apkg file.")) showWarning(tr(TR.IMPORTING_THE_PROVIDED_FILE_IS_NOT_A))
return return
if not mw.loadCollection(): if not mw.loadCollection():
@ -521,6 +512,6 @@ def _replaceWithApkg(mw, filename, backup):
if backup: if backup:
mw.col.modSchema(check=False) mw.col.modSchema(check=False)
tooltip(_("Importing complete.")) tooltip(tr(TR.IMPORTING_IMPORTING_COMPLETE))
mw.taskman.run_in_background(do_import, on_done) mw.taskman.run_in_background(do_import, on_done)

View file

@ -1,7 +1,6 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations from __future__ import annotations
import enum import enum
@ -120,7 +119,9 @@ class AnkiQt(QMainWindow):
self.setupAddons(args) self.setupAddons(args)
self.finish_ui_setup() self.finish_ui_setup()
except: except:
showInfo(_("Error during startup:\n%s") % traceback.format_exc()) showInfo(
tr(TR.QT_MISC_ERROR_DURING_STARTUP, val="%s") % traceback.format_exc()
)
sys.exit(1) sys.exit(1)
# must call this after ui set up # must call this after ui set up
if self.safeMode: if self.safeMode:
@ -291,10 +292,10 @@ class AnkiQt(QMainWindow):
return not checkInvalidFilename(name) and name != "addons21" return not checkInvalidFilename(name) and name != "addons21"
def onAddProfile(self): def onAddProfile(self):
name = getOnlyText(_("Name:")).strip() name = getOnlyText(tr(TR.ACTIONS_NAME)).strip()
if name: if name:
if name in self.pm.profiles(): if name in self.pm.profiles():
return showWarning(_("Name exists.")) return showWarning(tr(TR.QT_MISC_NAME_EXISTS))
if not self.profileNameOk(name): if not self.profileNameOk(name):
return return
self.pm.create(name) self.pm.create(name)
@ -302,13 +303,13 @@ class AnkiQt(QMainWindow):
self.refreshProfilesList() self.refreshProfilesList()
def onRenameProfile(self): def onRenameProfile(self):
name = getOnlyText(_("New name:"), default=self.pm.name).strip() name = getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=self.pm.name).strip()
if not name: if not name:
return return
if name == self.pm.name: if name == self.pm.name:
return return
if name in self.pm.profiles(): if name in self.pm.profiles():
return showWarning(_("Name exists.")) return showWarning(tr(TR.QT_MISC_NAME_EXISTS))
if not self.profileNameOk(name): if not self.profileNameOk(name):
return return
self.pm.rename(name) self.pm.rename(name)
@ -317,7 +318,7 @@ class AnkiQt(QMainWindow):
def onRemProfile(self): def onRemProfile(self):
profs = self.pm.profiles() profs = self.pm.profiles()
if len(profs) < 2: if len(profs) < 2:
return showWarning(_("There must be at least one profile.")) return showWarning(tr(TR.QT_MISC_THERE_MUST_BE_AT_LEAST_ONE))
# sure? # sure?
if not askUser( if not askUser(
_( _(
@ -348,7 +349,7 @@ Replace your collection with an earlier backup?"""
getFile( getFile(
self.profileDiag, self.profileDiag,
_("Revert to backup"), tr(TR.QT_MISC_REVERT_TO_BACKUP),
cb=doOpen, cb=doOpen,
filter="*.colpkg", filter="*.colpkg",
dir=self.pm.backupFolder(), dir=self.pm.backupFolder(),
@ -359,11 +360,7 @@ Replace your collection with an earlier backup?"""
# move the existing collection to the trash, as it may not open # move the existing collection to the trash, as it may not open
self.pm.trashCollection() self.pm.trashCollection()
except: except:
showWarning( showWarning(tr(TR.QT_MISC_UNABLE_TO_MOVE_EXISTING_FILE_TO))
_(
"Unable to move existing file to trash - please try restarting your computer."
)
)
return return
self.pendingImport = path self.pendingImport = path
@ -553,9 +550,9 @@ close the profile or restart Anki."""
if not self.col: if not self.col:
return return
if self.restoringBackup: if self.restoringBackup:
label = _("Closing...") label = tr(TR.QT_MISC_CLOSING)
else: else:
label = _("Backing Up...") label = tr(TR.QT_MISC_BACKING_UP)
self.progress.start(label=label) self.progress.start(label=label)
corrupt = False corrupt = False
try: try:
@ -641,7 +638,7 @@ from the profile screen."
# have two weeks passed? # have two weeks passed?
if (intTime() - self.pm.profile["lastOptimize"]) < 86400 * 14: if (intTime() - self.pm.profile["lastOptimize"]) < 86400 * 14:
return return
self.progress.start(label=_("Optimizing...")) self.progress.start(label=tr(TR.QT_MISC_OPTIMIZING))
self.col.optimize() self.col.optimize()
self.pm.profile["lastOptimize"] = intTime() self.pm.profile["lastOptimize"] = intTime()
self.pm.save() self.pm.save()
@ -672,7 +669,7 @@ from the profile screen."
def _selectedDeck(self) -> Optional[Deck]: def _selectedDeck(self) -> Optional[Deck]:
did = self.col.decks.selected() did = self.col.decks.selected()
if not self.col.decks.nameOrNone(did): if not self.col.decks.nameOrNone(did):
showInfo(_("Please select a deck.")) showInfo(tr(TR.QT_MISC_PLEASE_SELECT_A_DECK))
return None return None
return self.col.decks.get(did) return self.col.decks.get(did)
@ -732,8 +729,8 @@ from the profile screen."
return return
web_context = ResetRequired(self) web_context = ResetRequired(self)
self.web.set_bridge_command(lambda url: self.delayedMaybeReset(), web_context) self.web.set_bridge_command(lambda url: self.delayedMaybeReset(), web_context)
i = _("Waiting for editing to finish.") i = tr(TR.QT_MISC_WAITING_FOR_EDITING_TO_FINISH)
b = self.button("refresh", _("Resume Now"), id="resume") b = self.button("refresh", tr(TR.QT_MISC_RESUME_NOW), id="resume")
self.web.stdHtml( self.web.stdHtml(
""" """
<center><div style="height: 100%%"> <center><div style="height: 100%%">
@ -762,7 +759,7 @@ from the profile screen."
) -> str: ) -> str:
class_ = "but " + class_ class_ = "but " + class_
if key: if key:
key = _("Shortcut key: %s") % key key = tr(TR.ACTIONS_SHORTCUT_KEY, val="%s") % key
else: else:
key = "" key = ""
return """ return """
@ -1033,17 +1030,19 @@ title="%s" %s>%s</button>""" % (
gui_hooks.review_did_undo(cid) gui_hooks.review_did_undo(cid)
else: else:
self.reset() self.reset()
tooltip(_("Reverted to state prior to '%s'.") % n.lower()) tooltip(tr(TR.QT_MISC_REVERTED_TO_STATE_PRIOR_TO, val="%s") % n.lower())
gui_hooks.state_did_revert(n) gui_hooks.state_did_revert(n)
self.maybeEnableUndo() self.maybeEnableUndo()
def maybeEnableUndo(self) -> None: def maybeEnableUndo(self) -> None:
if self.col and self.col.undoName(): if self.col and self.col.undoName():
self.form.actionUndo.setText(_("Undo %s") % self.col.undoName()) self.form.actionUndo.setText(
tr(TR.QT_MISC_UNDO2, val="%s") % self.col.undoName()
)
self.form.actionUndo.setEnabled(True) self.form.actionUndo.setEnabled(True)
gui_hooks.undo_state_did_change(True) gui_hooks.undo_state_did_change(True)
else: else:
self.form.actionUndo.setText(_("Undo")) self.form.actionUndo.setText(tr(TR.QT_MISC_UNDO))
self.form.actionUndo.setEnabled(False) self.form.actionUndo.setEnabled(False)
gui_hooks.undo_state_did_change(False) gui_hooks.undo_state_did_change(False)
@ -1119,7 +1118,7 @@ title="%s" %s>%s</button>""" % (
import aqt.importing import aqt.importing
if not os.path.exists(path): if not os.path.exists(path):
showInfo(_("Please use File>Import to import this file.")) showInfo(tr(TR.QT_MISC_PLEASE_USE_FILEIMPORT_TO_IMPORT_THIS))
return None return None
aqt.importing.importFile(self, path) aqt.importing.importFile(self, path)
@ -1161,9 +1160,9 @@ title="%s" %s>%s</button>""" % (
if not search: if not search:
if not deck["dyn"]: if not deck["dyn"]:
search = 'deck:"%s" ' % deck["name"] search = 'deck:"%s" ' % deck["name"]
while self.col.decks.id_for_name(_("Filtered Deck %d") % n): while self.col.decks.id_for_name(tr(TR.QT_MISC_FILTERED_DECK, val="%s") % n):
n += 1 n += 1
name = _("Filtered Deck %d") % n name = tr(TR.QT_MISC_FILTERED_DECK, val="%s") % n
did = self.col.decks.new_filtered(name) did = self.col.decks.new_filtered(name)
diag = aqt.dyndeckconf.DeckConf(self, first=True, search=search) diag = aqt.dyndeckconf.DeckConf(self, first=True, search=search)
if not diag.ok: if not diag.ok:
@ -1537,7 +1536,7 @@ will be lost. Continue?"""
) )
frm.log.appendPlainText(to_append) frm.log.appendPlainText(to_append)
except UnicodeDecodeError: except UnicodeDecodeError:
to_append = _("<non-unicode text>") to_append = tr(TR.QT_MISC_NON_UNICODE_TEXT)
to_append = gui_hooks.debug_console_did_evaluate_python( to_append = gui_hooks.debug_console_did_evaluate_python(
to_append, text, frm to_append, text, frm
) )
@ -1600,9 +1599,9 @@ will be lost. Continue?"""
return None return None
self.pendingImport = buf self.pendingImport = buf
if is_addon: if is_addon:
msg = _("Add-on will be installed when a profile is opened.") msg = tr(TR.QT_MISC_ADDON_WILL_BE_INSTALLED_WHEN_A)
else: else:
msg = _("Deck will be imported when a profile is opened.") msg = tr(TR.QT_MISC_DECK_WILL_BE_IMPORTED_WHEN_A)
return tooltip(msg) return tooltip(msg)
if not self.interactiveState() or self.progress.busy(): if not self.interactiveState() or self.progress.busy():
# we can't raise the main window while in profile dialog, syncing, etc # we can't raise the main window while in profile dialog, syncing, etc

View file

@ -6,7 +6,7 @@ from typing import Optional
from anki.lang import _ from anki.lang import _
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.qt import * from aqt.qt import *
from aqt.utils import shortcut from aqt.utils import TR, shortcut, tr
class ModelChooser(QHBoxLayout): class ModelChooser(QHBoxLayout):
@ -37,11 +37,11 @@ class ModelChooser(QHBoxLayout):
def setupModels(self) -> None: def setupModels(self) -> None:
if self.label: if self.label:
self.modelLabel = QLabel(_("Type")) self.modelLabel = QLabel(tr(TR.NOTETYPES_TYPE))
self.addWidget(self.modelLabel) self.addWidget(self.modelLabel)
# models box # models box
self.models = QPushButton() self.models = QPushButton()
self.models.setToolTip(shortcut(_("Change Note Type (Ctrl+N)"))) self.models.setToolTip(shortcut(tr(TR.QT_MISC_CHANGE_NOTE_TYPE_CTRLANDN)))
QShortcut(QKeySequence("Ctrl+N"), self.widget, activated=self.on_activated) # type: ignore QShortcut(QKeySequence("Ctrl+N"), self.widget, activated=self.on_activated) # type: ignore
self.models.setAutoDefault(False) self.models.setAutoDefault(False)
self.addWidget(self.models) self.addWidget(self.models)
@ -73,7 +73,7 @@ class ModelChooser(QHBoxLayout):
current = self.deck.models.current()["name"] current = self.deck.models.current()["name"]
# edit button # edit button
edit = QPushButton(_("Manage"), clicked=self.onEdit) # type: ignore edit = QPushButton(tr(TR.QT_MISC_MANAGE), clicked=self.onEdit) # type: ignore
def nameFunc(): def nameFunc():
return sorted(self.deck.models.allNames()) return sorted(self.deck.models.allNames())
@ -81,8 +81,8 @@ class ModelChooser(QHBoxLayout):
ret = StudyDeck( ret = StudyDeck(
self.mw, self.mw,
names=nameFunc, names=nameFunc,
accept=_("Choose"), accept=tr(TR.ACTIONS_CHOOSE),
title=_("Choose Note Type"), title=tr(TR.QT_MISC_CHOOSE_NOTE_TYPE),
help="_notes", help="_notes",
current=current, current=current,
parent=self.widget, parent=self.widget,

View file

@ -14,6 +14,7 @@ from anki.rsbackend import pb
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.qt import * from aqt.qt import *
from aqt.utils import ( from aqt.utils import (
TR,
askUser, askUser,
getText, getText,
maybeHideClose, maybeHideClose,
@ -21,6 +22,7 @@ from aqt.utils import (
restoreGeom, restoreGeom,
saveGeom, saveGeom,
showInfo, showInfo,
tr,
) )
@ -33,7 +35,7 @@ class Models(QDialog):
self.col = mw.col.weakref() self.col = mw.col.weakref()
assert self.col assert self.col
self.mm = self.col.models self.mm = self.col.models
self.mw.checkpoint(_("Note Types")) self.mw.checkpoint(tr(TR.NOTETYPES_NOTE_TYPES))
self.form = aqt.forms.models.Ui_Dialog() self.form = aqt.forms.models.Ui_Dialog()
self.form.setupUi(self) self.form.setupUi(self)
qconnect( qconnect(
@ -54,20 +56,20 @@ class Models(QDialog):
box = f.buttonBox box = f.buttonBox
default_buttons = [ default_buttons = [
(_("Add"), self.onAdd), (tr(TR.ACTIONS_ADD), self.onAdd),
(_("Rename"), self.onRename), (tr(TR.ACTIONS_RENAME), self.onRename),
(_("Delete"), self.onDelete), (tr(TR.ACTIONS_DELETE), self.onDelete),
] ]
if self.fromMain: if self.fromMain:
default_buttons.extend( default_buttons.extend(
[ [
(_("Fields..."), self.onFields), (tr(TR.NOTETYPES_FIELDS), self.onFields),
(_("Cards..."), self.onCards), (tr(TR.NOTETYPES_CARDS), self.onCards),
] ]
) )
default_buttons.append((_("Options..."), self.onAdvanced)) default_buttons.append((tr(TR.NOTETYPES_OPTIONS), self.onAdvanced))
for label, func in gui_hooks.models_did_init_buttons(default_buttons, self): for label, func in gui_hooks.models_did_init_buttons(default_buttons, self):
button = box.addButton(label, QDialogButtonBox.ActionRole) button = box.addButton(label, QDialogButtonBox.ActionRole)
@ -84,7 +86,7 @@ class Models(QDialog):
def onRename(self) -> None: def onRename(self) -> None:
nt = self.current_notetype() nt = self.current_notetype()
txt = getText(_("New name:"), default=nt["name"]) txt = getText(tr(TR.ACTIONS_NEW_NAME), default=nt["name"])
name = txt[0].replace('"', "") name = txt[0].replace('"', "")
if txt[1] and name: if txt[1] and name:
nt["name"] = name nt["name"] = name
@ -120,20 +122,20 @@ class Models(QDialog):
def onAdd(self) -> None: def onAdd(self) -> None:
m = AddModel(self.mw, self).get() m = AddModel(self.mw, self).get()
if m: if m:
txt = getText(_("Name:"), default=m["name"])[0].replace('"', "") txt = getText(tr(TR.ACTIONS_NAME), default=m["name"])[0].replace('"', "")
if txt: if txt:
m["name"] = txt m["name"] = txt
self.saveAndRefresh(m) self.saveAndRefresh(m)
def onDelete(self) -> None: def onDelete(self) -> None:
if len(self.models) < 2: if len(self.models) < 2:
showInfo(_("Please add another note type first."), parent=self) showInfo(tr(TR.NOTETYPES_PLEASE_ADD_ANOTHER_NOTE_TYPE_FIRST), parent=self)
return return
idx = self.form.modelsList.currentRow() idx = self.form.modelsList.currentRow()
if self.models[idx].use_count: if self.models[idx].use_count:
msg = _("Delete this note type and all its cards?") msg = tr(TR.NOTETYPES_DELETE_THIS_NOTE_TYPE_AND_ALL)
else: else:
msg = _("Delete this unused note type?") msg = tr(TR.NOTETYPES_DELETE_THIS_UNUSED_NOTE_TYPE)
if not askUser(msg, parent=self): if not askUser(msg, parent=self):
return return
@ -158,7 +160,7 @@ class Models(QDialog):
frm.latexsvg.setChecked(nt.get("latexsvg", False)) frm.latexsvg.setChecked(nt.get("latexsvg", False))
frm.latexHeader.setText(nt["latexPre"]) frm.latexHeader.setText(nt["latexPre"])
frm.latexFooter.setText(nt["latexPost"]) frm.latexFooter.setText(nt["latexPost"])
d.setWindowTitle(_("Options for %s") % nt["name"]) d.setWindowTitle(tr(TR.ACTIONS_OPTIONS_FOR, val="%s") % nt["name"])
qconnect(frm.buttonBox.helpRequested, lambda: openHelp("math?id=latex")) qconnect(frm.buttonBox.helpRequested, lambda: openHelp("math?id=latex"))
restoreGeom(d, "modelopts") restoreGeom(d, "modelopts")
gui_hooks.models_advanced_will_show(d) gui_hooks.models_advanced_will_show(d)
@ -209,12 +211,12 @@ class AddModel(QDialog):
# standard models # standard models
self.models = [] self.models = []
for (name, func) in stdmodels.get_stock_notetypes(self.col): for (name, func) in stdmodels.get_stock_notetypes(self.col):
item = QListWidgetItem(_("Add: %s") % name) item = QListWidgetItem(tr(TR.NOTETYPES_ADD, val="%s") % name)
self.dialog.models.addItem(item) self.dialog.models.addItem(item)
self.models.append((True, func)) self.models.append((True, func))
# add copies # add copies
for m in sorted(self.col.models.all(), key=itemgetter("name")): for m in sorted(self.col.models.all(), key=itemgetter("name")):
item = QListWidgetItem(_("Clone: %s") % m["name"]) item = QListWidgetItem(tr(TR.NOTETYPES_CLONE, val="%s") % m["name"])
self.dialog.models.addItem(item) self.dialog.models.addItem(item)
self.models.append((False, m)) # type: ignore self.models.append((False, m)) # type: ignore
self.dialog.models.setCurrentRow(0) self.dialog.models.setCurrentRow(0)

View file

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
@ -12,7 +11,7 @@ from anki.lang import _
from aqt import gui_hooks from aqt import gui_hooks
from aqt.sound import av_player from aqt.sound import av_player
from aqt.toolbar import BottomBar from aqt.toolbar import BottomBar
from aqt.utils import askUserDialog, openLink, shortcut, tooltip from aqt.utils import TR, askUserDialog, openLink, shortcut, tooltip, tr
class OverviewBottomBar: class OverviewBottomBar:
@ -67,7 +66,7 @@ class Overview:
self.mw.col.startTimebox() self.mw.col.startTimebox()
self.mw.moveToState("review") self.mw.moveToState("review")
if self.mw.state == "overview": if self.mw.state == "overview":
tooltip(_("No cards are due yet.")) tooltip(tr(TR.STUDYING_NO_CARDS_ARE_DUE_YET))
elif url == "anki": elif url == "anki":
print("anki menu") print("anki menu")
elif url == "opts": elif url == "opts":
@ -128,13 +127,13 @@ class Overview:
info = self.mw.col.sched.congratulations_info() info = self.mw.col.sched.congratulations_info()
if info.have_sched_buried and info.have_user_buried: if info.have_sched_buried and info.have_user_buried:
opts = [ opts = [
_("Manually Buried Cards"), tr(TR.STUDYING_MANUALLY_BURIED_CARDS),
_("Buried Siblings"), tr(TR.STUDYING_BURIED_SIBLINGS),
_("All Buried Cards"), tr(TR.STUDYING_ALL_BURIED_CARDS),
_("Cancel"), tr(TR.ACTIONS_CANCEL),
] ]
diag = askUserDialog(_("What would you like to unbury?"), opts) diag = askUserDialog(tr(TR.STUDYING_WHAT_WOULD_YOU_LIKE_TO_UNBURY), opts)
diag.setDefault(0) diag.setDefault(0)
ret = diag.run() ret = diag.run()
if ret == opts[0]: if ret == opts[0]:
@ -226,13 +225,13 @@ to their original deck."""
</table> </table>
</td><td align=center> </td><td align=center>
%s</td></tr></table>""" % ( %s</td></tr></table>""" % (
_("New"), tr(TR.ACTIONS_NEW),
counts[0], counts[0],
_("Learning"), tr(TR.SCHEDULING_LEARNING),
counts[1], counts[1],
_("To Review"), tr(TR.STUDYING_TO_REVIEW),
counts[2], counts[2],
but("study", _("Study Now"), id="study", extra=" autofocus"), but("study", tr(TR.STUDYING_STUDY_NOW), id="study", extra=" autofocus"),
) )
_body = """ _body = """
@ -249,20 +248,20 @@ to their original deck."""
def _renderBottom(self): def _renderBottom(self):
links = [ links = [
["O", "opts", _("Options")], ["O", "opts", tr(TR.ACTIONS_OPTIONS)],
] ]
if self.mw.col.decks.current()["dyn"]: if self.mw.col.decks.current()["dyn"]:
links.append(["R", "refresh", _("Rebuild")]) links.append(["R", "refresh", tr(TR.ACTIONS_REBUILD)])
links.append(["E", "empty", _("Empty")]) links.append(["E", "empty", tr(TR.STUDYING_EMPTY)])
else: else:
links.append(["C", "studymore", _("Custom Study")]) links.append(["C", "studymore", tr(TR.ACTIONS_CUSTOM_STUDY)])
# links.append(["F", "cram", _("Filter/Cram")]) # links.append(["F", "cram", _("Filter/Cram")])
if self.mw.col.sched.haveBuried(): if self.mw.col.sched.haveBuried():
links.append(["U", "unbury", _("Unbury")]) links.append(["U", "unbury", tr(TR.STUDYING_UNBURY)])
buf = "" buf = ""
for b in links: for b in links:
if b[0]: if b[0]:
b[0] = _("Shortcut key: %s") % shortcut(b[0]) b[0] = tr(TR.ACTIONS_SHORTCUT_KEY, val="%s") % shortcut(b[0])
buf += """ buf += """
<button title="%s" onclick='pycmd("%s")'>%s</button>""" % tuple( <button title="%s" onclick='pycmd("%s")'>%s</button>""" % tuple(
b b

View file

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import anki.lang import anki.lang
import aqt import aqt
from anki.lang import _ from anki.lang import _
@ -69,7 +68,9 @@ class Preferences(QDialog):
def onLangIdxChanged(self, idx): def onLangIdxChanged(self, idx):
code = anki.lang.langs[idx][1] code = anki.lang.langs[idx][1]
self.mw.pm.setLang(code) self.mw.pm.setLang(code)
showInfo(_("Please restart Anki to complete language change."), parent=self) showInfo(
tr(TR.PREFERENCES_PLEASE_RESTART_ANKI_TO_COMPLETE_LANGUAGE), parent=self
)
# Collection options # Collection options
###################################################################### ######################################################################
@ -117,7 +118,7 @@ class Preferences(QDialog):
self.mw.pm.setGlMode("auto") self.mw.pm.setGlMode("auto")
else: else:
self.mw.pm.setGlMode("software") self.mw.pm.setGlMode("software")
showInfo(_("Changes will take effect when you restart Anki.")) showInfo(tr(TR.PREFERENCES_CHANGES_WILL_TAKE_EFFECT_WHEN_YOU))
qc = d.conf qc = d.conf
qc["addToCur"] = not f.useCurrent.currentIndex() qc["addToCur"] = not f.useCurrent.currentIndex()
@ -149,11 +150,7 @@ class Preferences(QDialog):
if haveNew == wantNew: if haveNew == wantNew:
return return
if not askUser( if not askUser(tr(TR.PREFERENCES_THIS_WILL_RESET_ANY_CARDS_IN)):
_(
"This will reset any cards in learning, clear filtered decks, and change the scheduler version. Proceed?"
)
):
return return
if wantNew: if wantNew:
@ -247,4 +244,4 @@ Not currently enabled; click the sync button in the main window to enable."""
self.mw.pm.set_interrupt_audio(self.form.interrupt_audio.isChecked()) self.mw.pm.set_interrupt_audio(self.form.interrupt_audio.isChecked())
if restart_required: if restart_required:
showInfo(_("Changes will take effect when you restart Anki.")) showInfo(tr(TR.PREFERENCES_CHANGES_WILL_TAKE_EFFECT_WHEN_YOU))

View file

@ -1,7 +1,6 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
# mypy: check-untyped-defs # mypy: check-untyped-defs
import json import json
import re import re
import time import time
@ -26,7 +25,7 @@ from aqt.qt import (
from aqt.reviewer import replay_audio from aqt.reviewer import replay_audio
from aqt.sound import av_player, play_clicked_audio from aqt.sound import av_player, play_clicked_audio
from aqt.theme import theme_manager from aqt.theme import theme_manager
from aqt.utils import restoreGeom, saveGeom from aqt.utils import TR, restoreGeom, saveGeom, tr
from aqt.webview import AnkiWebView from aqt.webview import AnkiWebView
@ -62,7 +61,7 @@ class Previewer(QDialog):
self.show() self.show()
def _create_gui(self): def _create_gui(self):
self.setWindowTitle(_("Preview")) self.setWindowTitle(tr(TR.ACTIONS_PREVIEW))
qconnect(self.finished, self._on_finished) qconnect(self.finished, self._on_finished)
self.silentlyClose = True self.silentlyClose = True
@ -73,14 +72,14 @@ class Previewer(QDialog):
self.bbox = QDialogButtonBox() self.bbox = QDialogButtonBox()
self._replay = self.bbox.addButton( self._replay = self.bbox.addButton(
_("Replay Audio"), QDialogButtonBox.ActionRole tr(TR.ACTIONS_REPLAY_AUDIO), QDialogButtonBox.ActionRole
) )
self._replay.setAutoDefault(False) self._replay.setAutoDefault(False)
self._replay.setShortcut(QKeySequence("R")) self._replay.setShortcut(QKeySequence("R"))
self._replay.setToolTip(_("Shortcut key: %s" % "R")) self._replay.setToolTip(_("Shortcut key: %s" % "R"))
qconnect(self._replay.clicked, self._on_replay_audio) qconnect(self._replay.clicked, self._on_replay_audio)
both_sides_button = QCheckBox(_("Back Side Only")) both_sides_button = QCheckBox(tr(TR.QT_MISC_BACK_SIDE_ONLY))
both_sides_button.setShortcut(QKeySequence("B")) both_sides_button.setShortcut(QKeySequence("B"))
both_sides_button.setToolTip(_("Shortcut key: %s" % "B")) both_sides_button.setToolTip(_("Shortcut key: %s" % "B"))
self.bbox.addButton(both_sides_button, QDialogButtonBox.ActionRole) self.bbox.addButton(both_sides_button, QDialogButtonBox.ActionRole)
@ -159,7 +158,7 @@ class Previewer(QDialog):
c = self.card() c = self.card()
func = "_showQuestion" func = "_showQuestion"
if not c: if not c:
txt = _("(please select 1 card)") txt = tr(TR.QT_MISC_PLEASE_SELECT_1_CARD)
bodyclass = "" bodyclass = ""
self._last_state = None self._last_state = None
else: else:
@ -240,12 +239,12 @@ class MultiCardPreviewer(Previewer):
self._prev = self.bbox.addButton("<", QDialogButtonBox.ActionRole) self._prev = self.bbox.addButton("<", QDialogButtonBox.ActionRole)
self._prev.setAutoDefault(False) self._prev.setAutoDefault(False)
self._prev.setShortcut(QKeySequence("Left")) self._prev.setShortcut(QKeySequence("Left"))
self._prev.setToolTip(_("Shortcut key: Left arrow")) self._prev.setToolTip(tr(TR.QT_MISC_SHORTCUT_KEY_LEFT_ARROW))
self._next = self.bbox.addButton(">", QDialogButtonBox.ActionRole) self._next = self.bbox.addButton(">", QDialogButtonBox.ActionRole)
self._next.setAutoDefault(True) self._next.setAutoDefault(True)
self._next.setShortcut(QKeySequence("Right")) self._next.setShortcut(QKeySequence("Right"))
self._next.setToolTip(_("Shortcut key: Right arrow or Enter")) self._next.setToolTip(tr(TR.QT_MISC_SHORTCUT_KEY_RIGHT_ARROW_OR_ENTER))
qconnect(self._prev.clicked, self._on_prev) qconnect(self._prev.clicked, self._on_prev)
qconnect(self._next.clicked, self._on_next) qconnect(self._next.clicked, self._on_next)

View file

@ -1,11 +1,6 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
# Profile handling
##########################################################################
# - Saves in pickles rather than json to easily store Qt window state.
# - Saves in sqlite rather than a flat file so the config can't be corrupted
import io import io
import locale import locale
import pickle import pickle
@ -28,6 +23,12 @@ from aqt import appHelpSite
from aqt.qt import * from aqt.qt import *
from aqt.utils import TR, locale_dir, showWarning, tr from aqt.utils import TR, locale_dir, showWarning, tr
# Profile handling
##########################################################################
# - Saves in pickles rather than json to easily store Qt window state.
# - Saves in sqlite rather than a flat file so the config can't be corrupted
metaConf = dict( metaConf = dict(
ver=0, ver=0,
updates=True, updates=True,
@ -251,7 +252,7 @@ class ProfileManager:
except: except:
QMessageBox.warning( QMessageBox.warning(
None, None,
_("Profile Corrupt"), tr(TR.PROFILES_PROFILE_CORRUPT),
_( _(
"""\ """\
Anki could not read your profile data. Window sizes and your sync login \ Anki could not read your profile data. Window sizes and your sync login \
@ -304,12 +305,13 @@ details have been forgotten."""
oldFolder = midFolder oldFolder = midFolder
else: else:
showWarning( showWarning(
_("Please remove the folder %s and try again.") % midFolder tr(TR.PROFILES_PLEASE_REMOVE_THE_FOLDER_AND, val="%s")
% midFolder
) )
self.name = oldName self.name = oldName
return return
else: else:
showWarning(_("Folder already exists.")) showWarning(tr(TR.PROFILES_FOLDER_ALREADY_EXISTS))
self.name = oldName self.name = oldName
return return
@ -476,7 +478,7 @@ create table if not exists profiles
def _ensureProfile(self) -> None: def _ensureProfile(self) -> None:
"Create a new profile if none exists." "Create a new profile if none exists."
self.create(_("User 1")) self.create(tr(TR.PROFILES_USER_1))
p = os.path.join(self.base, "README.txt") p = os.path.join(self.base, "README.txt")
with open(p, "w", encoding="utf8") as file: with open(p, "w", encoding="utf8") as file:
file.write( file.write(

View file

@ -1,7 +1,6 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations from __future__ import annotations
import time import time
@ -10,6 +9,7 @@ from typing import Optional
import aqt.forms import aqt.forms
from anki.lang import _ from anki.lang import _
from aqt.qt import * from aqt.qt import *
from aqt.utils import TR, tr
# Progress info # Progress info
########################################################################## ##########################################################################
@ -80,7 +80,7 @@ class ProgressManager:
if not parent and self.mw.isVisible(): if not parent and self.mw.isVisible():
parent = self.mw parent = self.mw
label = label or _("Processing...") label = label or tr(TR.QT_MISC_PROCESSING)
self._win = ProgressDialog(parent) self._win = ProgressDialog(parent)
self._win.form.progressBar.setMinimum(min) self._win.form.progressBar.setMinimum(min)
self._win.form.progressBar.setMaximum(max) self._win.form.progressBar.setMaximum(max)

View file

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations from __future__ import annotations
import difflib import difflib
@ -22,7 +21,14 @@ from aqt.qt import *
from aqt.sound import av_player, getAudio, play_clicked_audio from aqt.sound import av_player, getAudio, play_clicked_audio
from aqt.theme import theme_manager from aqt.theme import theme_manager
from aqt.toolbar import BottomBar from aqt.toolbar import BottomBar
from aqt.utils import askUserDialog, downArrow, qtMenuShortcutWorkaround, tooltip from aqt.utils import (
TR,
askUserDialog,
downArrow,
qtMenuShortcutWorkaround,
tooltip,
tr,
)
from aqt.webview import AnkiWebView from aqt.webview import AnkiWebView
@ -92,8 +98,10 @@ class Reviewer:
) )
mins = int(round(elapsed[0] / 60)) mins = int(round(elapsed[0] / 60))
part2 = ngettext("%s minute.", "%s minutes.", mins) % mins part2 = ngettext("%s minute.", "%s minutes.", mins) % mins
fin = _("Finish") fin = tr(TR.STUDYING_FINISH)
diag = askUserDialog("%s %s" % (part1, part2), [_("Continue"), fin]) diag = askUserDialog(
"%s %s" % (part1, part2), [tr(TR.STUDYING_CONTINUE), fin]
)
diag.setIcon(QMessageBox.Information) diag.setIcon(QMessageBox.Information)
if diag.run() == fin: if diag.run() == fin:
return self.mw.moveToState("deckBrowser") return self.mw.moveToState("deckBrowser")
@ -388,7 +396,7 @@ class Reviewer:
Please run Tools>Empty Cards""" Please run Tools>Empty Cards"""
) )
else: else:
warn = _("Type answer: unknown field %s") % fld warn = tr(TR.STUDYING_TYPE_ANSWER_UNKNOWN_FIELD, val="%s") % fld
return re.sub(self.typeAnsPat, warn, buf) return re.sub(self.typeAnsPat, warn, buf)
else: else:
# empty field, remove type answer pattern # empty field, remove type answer pattern
@ -569,9 +577,9 @@ time = %(time)d;
</script> </script>
""" % dict( """ % dict(
rem=self._remaining(), rem=self._remaining(),
edit=_("Edit"), edit=tr(TR.STUDYING_EDIT),
editkey=_("Shortcut key: %s") % "E", editkey=tr(TR.ACTIONS_SHORTCUT_KEY, val="%s") % "E",
more=_("More"), more=tr(TR.STUDYING_MORE),
downArrow=downArrow(), downArrow=downArrow(),
time=self.card.timeTaken() // 1000, time=self.card.timeTaken() // 1000,
) )
@ -581,8 +589,8 @@ time = %(time)d;
<span class=stattxt>%s</span><br> <span class=stattxt>%s</span><br>
<button title="%s" id="ansbut" class="focus" onclick='pycmd("ans");'>%s</button>""" % ( <button title="%s" id="ansbut" class="focus" onclick='pycmd("ans");'>%s</button>""" % (
self._remaining(), self._remaining(),
_("Shortcut key: %s") % _("Space"), tr(TR.ACTIONS_SHORTCUT_KEY, val="%s") % tr(TR.STUDYING_SPACE),
_("Show Answer"), tr(TR.STUDYING_SHOW_ANSWER),
) )
# wrap it in a table so it has the same top margin as the ease buttons # wrap it in a table so it has the same top margin as the ease buttons
middle = ( middle = (
@ -626,17 +634,21 @@ time = %(time)d;
button_count = self.mw.col.sched.answerButtons(self.card) button_count = self.mw.col.sched.answerButtons(self.card)
if button_count == 2: if button_count == 2:
buttons_tuple: Tuple[Tuple[int, str], ...] = ( buttons_tuple: Tuple[Tuple[int, str], ...] = (
(1, _("Again")), (1, tr(TR.STUDYING_AGAIN)),
(2, _("Good")), (2, tr(TR.STUDYING_GOOD)),
) )
elif button_count == 3: elif button_count == 3:
buttons_tuple = ((1, _("Again")), (2, _("Good")), (3, _("Easy"))) buttons_tuple = (
(1, tr(TR.STUDYING_AGAIN)),
(2, tr(TR.STUDYING_GOOD)),
(3, tr(TR.STUDYING_EASY)),
)
else: else:
buttons_tuple = ( buttons_tuple = (
(1, _("Again")), (1, tr(TR.STUDYING_AGAIN)),
(2, _("Hard")), (2, tr(TR.STUDYING_HARD)),
(3, _("Good")), (3, tr(TR.STUDYING_GOOD)),
(4, _("Easy")), (4, tr(TR.STUDYING_EASY)),
) )
buttons_tuple = gui_hooks.reviewer_will_init_answer_buttons( buttons_tuple = gui_hooks.reviewer_will_init_answer_buttons(
buttons_tuple, self, self.card buttons_tuple, self, self.card
@ -657,7 +669,7 @@ time = %(time)d;
%s</button></td>""" % ( %s</button></td>""" % (
due, due,
extra, extra,
_("Shortcut key: %s") % i, tr(TR.ACTIONS_SHORTCUT_KEY, val="%s") % i,
i, i,
i, i,
label, label,
@ -682,9 +694,9 @@ time = %(time)d;
def onLeech(self, card: Card) -> None: def onLeech(self, card: Card) -> None:
# for now # for now
s = _("Card was a leech.") s = tr(TR.STUDYING_CARD_WAS_A_LEECH)
if card.queue < 0: if card.queue < 0:
s += " " + _("It has been suspended.") s += " " + tr(TR.STUDYING_IT_HAS_BEEN_SUSPENDED)
tooltip(s) tooltip(s)
# Context menu # Context menu
@ -695,48 +707,48 @@ time = %(time)d;
currentFlag = self.card and self.card.userFlag() currentFlag = self.card and self.card.userFlag()
opts = [ opts = [
[ [
_("Flag Card"), tr(TR.STUDYING_FLAG_CARD),
[ [
[ [
_("Red Flag"), tr(TR.ACTIONS_RED_FLAG),
"Ctrl+1", "Ctrl+1",
lambda: self.setFlag(1), lambda: self.setFlag(1),
dict(checked=currentFlag == 1), dict(checked=currentFlag == 1),
], ],
[ [
_("Orange Flag"), tr(TR.ACTIONS_ORANGE_FLAG),
"Ctrl+2", "Ctrl+2",
lambda: self.setFlag(2), lambda: self.setFlag(2),
dict(checked=currentFlag == 2), dict(checked=currentFlag == 2),
], ],
[ [
_("Green Flag"), tr(TR.ACTIONS_GREEN_FLAG),
"Ctrl+3", "Ctrl+3",
lambda: self.setFlag(3), lambda: self.setFlag(3),
dict(checked=currentFlag == 3), dict(checked=currentFlag == 3),
], ],
[ [
_("Blue Flag"), tr(TR.ACTIONS_BLUE_FLAG),
"Ctrl+4", "Ctrl+4",
lambda: self.setFlag(4), lambda: self.setFlag(4),
dict(checked=currentFlag == 4), dict(checked=currentFlag == 4),
], ],
], ],
], ],
[_("Mark Note"), "*", self.onMark], [tr(TR.STUDYING_MARK_NOTE), "*", self.onMark],
[_("Bury Card"), "-", self.onBuryCard], [tr(TR.STUDYING_BURY_CARD), "-", self.onBuryCard],
[_("Bury Note"), "=", self.onBuryNote], [tr(TR.STUDYING_BURY_NOTE), "=", self.onBuryNote],
[_("Suspend Card"), "@", self.onSuspendCard], [tr(TR.ACTIONS_SUSPEND_CARD), "@", self.onSuspendCard],
[_("Suspend Note"), "!", self.onSuspend], [tr(TR.STUDYING_SUSPEND_NOTE), "!", self.onSuspend],
[_("Delete Note"), "Ctrl+Delete", self.onDelete], [tr(TR.STUDYING_DELETE_NOTE), "Ctrl+Delete", self.onDelete],
[_("Options"), "O", self.onOptions], [tr(TR.ACTIONS_OPTIONS), "O", self.onOptions],
None, None,
[_("Replay Audio"), "R", self.replayAudio], [tr(TR.ACTIONS_REPLAY_AUDIO), "R", self.replayAudio],
[_("Pause Audio"), "5", self.on_pause_audio], [tr(TR.STUDYING_PAUSE_AUDIO), "5", self.on_pause_audio],
[_("Audio -5s"), "6", self.on_seek_backward], [tr(TR.STUDYING_AUDIO_5S), "6", self.on_seek_backward],
[_("Audio +5s"), "7", self.on_seek_forward], [tr(TR.STUDYING_AUDIO_AND5S), "7", self.on_seek_forward],
[_("Record Own Voice"), "Shift+V", self.onRecordVoice], [tr(TR.STUDYING_RECORD_OWN_VOICE), "Shift+V", self.onRecordVoice],
[_("Replay Own Voice"), "V", self.onReplayRecorded], [tr(TR.STUDYING_REPLAY_OWN_VOICE), "V", self.onReplayRecorded],
] ]
return opts return opts
@ -793,15 +805,15 @@ time = %(time)d;
self._drawMark() self._drawMark()
def onSuspend(self) -> None: def onSuspend(self) -> None:
self.mw.checkpoint(_("Suspend")) self.mw.checkpoint(tr(TR.STUDYING_SUSPEND))
self.mw.col.sched.suspend_cards([c.id for c in self.card.note().cards()]) self.mw.col.sched.suspend_cards([c.id for c in self.card.note().cards()])
tooltip(_("Note suspended.")) tooltip(tr(TR.STUDYING_NOTE_SUSPENDED))
self.mw.reset() self.mw.reset()
def onSuspendCard(self) -> None: def onSuspendCard(self) -> None:
self.mw.checkpoint(_("Suspend")) self.mw.checkpoint(tr(TR.STUDYING_SUSPEND))
self.mw.col.sched.suspend_cards([self.card.id]) self.mw.col.sched.suspend_cards([self.card.id])
tooltip(_("Card suspended.")) tooltip(tr(TR.STUDYING_CARD_SUSPENDED))
self.mw.reset() self.mw.reset()
def onDelete(self) -> None: def onDelete(self) -> None:
@ -809,7 +821,7 @@ time = %(time)d;
# window # window
if self.mw.state != "review" or not self.card: if self.mw.state != "review" or not self.card:
return return
self.mw.checkpoint(_("Delete")) self.mw.checkpoint(tr(TR.ACTIONS_DELETE))
cnt = len(self.card.note().cards()) cnt = len(self.card.note().cards())
self.mw.col.remove_notes([self.card.note().id]) self.mw.col.remove_notes([self.card.note().id])
self.mw.reset() self.mw.reset()
@ -821,16 +833,16 @@ time = %(time)d;
) )
def onBuryCard(self) -> None: def onBuryCard(self) -> None:
self.mw.checkpoint(_("Bury")) self.mw.checkpoint(tr(TR.STUDYING_BURY))
self.mw.col.sched.bury_cards([self.card.id]) self.mw.col.sched.bury_cards([self.card.id])
self.mw.reset() self.mw.reset()
tooltip(_("Card buried.")) tooltip(tr(TR.STUDYING_CARD_BURIED))
def onBuryNote(self) -> None: def onBuryNote(self) -> None:
self.mw.checkpoint(_("Bury")) self.mw.checkpoint(tr(TR.STUDYING_BURY))
self.mw.col.sched.bury_note(self.card.note()) self.mw.col.sched.bury_note(self.card.note())
self.mw.reset() self.mw.reset()
tooltip(_("Note buried.")) tooltip(tr(TR.STUDYING_NOTE_BURIED))
def onRecordVoice(self) -> None: def onRecordVoice(self) -> None:
self._recordedAudio = getAudio(self.mw, encode=False) self._recordedAudio = getAudio(self.mw, encode=False)
@ -838,6 +850,6 @@ time = %(time)d;
def onReplayRecorded(self) -> None: def onReplayRecorded(self) -> None:
if not self._recordedAudio: if not self._recordedAudio:
tooltip(_("You haven't recorded your voice yet.")) tooltip(tr(TR.STUDYING_YOU_HAVENT_RECORDED_YOUR_VOICE_YET))
return return
av_player.play_file(self._recordedAudio) av_player.play_file(self._recordedAudio)

View file

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import Enum
@ -10,7 +9,7 @@ import aqt
from anki.errors import DeckRenameError from anki.errors import DeckRenameError
from anki.lang import _ from anki.lang import _
from aqt.qt import * from aqt.qt import *
from aqt.utils import getOnlyText, showWarning from aqt.utils import TR, getOnlyText, showWarning, tr
class SidebarItemType(Enum): class SidebarItemType(Enum):
@ -71,7 +70,7 @@ class NewSidebarTreeView(SidebarTreeViewBase):
self.setContextMenuPolicy(Qt.CustomContextMenu) self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.onContextMenu) # type: ignore self.customContextMenuRequested.connect(self.onContextMenu) # type: ignore
self.context_menus = { self.context_menus = {
SidebarItemType.DECK: ((_("Rename"), self.rename_deck),), SidebarItemType.DECK: ((tr(TR.ACTIONS_RENAME), self.rename_deck),),
} }
def onContextMenu(self, point: QPoint) -> None: def onContextMenu(self, point: QPoint) -> None:
@ -92,10 +91,10 @@ class NewSidebarTreeView(SidebarTreeViewBase):
m.exec_(QCursor.pos()) m.exec_(QCursor.pos())
def rename_deck(self, item: "aqt.browser.SidebarItem") -> None: def rename_deck(self, item: "aqt.browser.SidebarItem") -> None:
self.mw.checkpoint(_("Rename Deck")) self.mw.checkpoint(tr(TR.ACTIONS_RENAME_DECK))
deck = self.mw.col.decks.get(item.id) deck = self.mw.col.decks.get(item.id)
old_name = deck["name"] old_name = deck["name"]
new_name = getOnlyText(_("New deck name:"), default=old_name) new_name = getOnlyText(tr(TR.DECKS_NEW_DECK_NAME), default=old_name)
new_name = new_name.replace('"', "") new_name = new_name.replace('"', "")
if not new_name or new_name == old_name: if not new_name or new_name == old_name:
return return

View file

@ -23,7 +23,7 @@ from aqt import gui_hooks
from aqt.mpv import MPV, MPVBase, MPVCommandError from aqt.mpv import MPV, MPVBase, MPVCommandError
from aqt.qt import * from aqt.qt import *
from aqt.taskman import TaskManager from aqt.taskman import TaskManager
from aqt.utils import restoreGeom, saveGeom, showWarning, startup_info from aqt.utils import TR, restoreGeom, saveGeom, showWarning, startup_info, tr
try: try:
import pyaudio import pyaudio
@ -318,11 +318,7 @@ class SimpleProcessPlayer(Player): # pylint: disable=abstract-method
try: try:
ret.result() ret.result()
except FileNotFoundError: except FileNotFoundError:
showWarning( showWarning(tr(TR.MEDIA_SOUND_AND_VIDEO_ON_CARDS_WILL))
_(
"Sound and video on cards will not function until mpv or mplayer is installed."
)
)
# must call cb() here, as we don't currently have another way # must call cb() here, as we don't currently have another way
# to flag to av_player that we've stopped # to flag to av_player that we've stopped
cb() cb()
@ -499,7 +495,7 @@ class _Recorder:
finally: finally:
self.cleanup() self.cleanup()
if ret: if ret:
raise Exception(_("Error running %s") % " ".join(cmd)) raise Exception(tr(TR.MEDIA_ERROR_RUNNING, val="%s") % " ".join(cmd))
def cleanup(self) -> None: def cleanup(self) -> None:
if os.path.exists(processingSrc): if os.path.exists(processingSrc):
@ -596,10 +592,10 @@ def getAudio(parent: QWidget, encode: bool = True) -> Optional[str]:
restoreGeom(mb, "audioRecorder") restoreGeom(mb, "audioRecorder")
mb.setWindowTitle("Anki") mb.setWindowTitle("Anki")
mb.setIconPixmap(QPixmap(":/icons/media-record.png")) mb.setIconPixmap(QPixmap(":/icons/media-record.png"))
but = QPushButton(_("Save")) but = QPushButton(tr(TR.ACTIONS_SAVE))
mb.addButton(but, QMessageBox.AcceptRole) mb.addButton(but, QMessageBox.AcceptRole)
but.setDefault(True) but.setDefault(True)
but = QPushButton(_("Cancel")) but = QPushButton(tr(TR.ACTIONS_CANCEL))
mb.addButton(but, QMessageBox.RejectRole) mb.addButton(but, QMessageBox.RejectRole)
mb.setEscapeButton(but) mb.setEscapeButton(but)
t = time.time() t = time.time()
@ -607,7 +603,7 @@ def getAudio(parent: QWidget, encode: bool = True) -> Optional[str]:
time.sleep(r.startupDelay) time.sleep(r.startupDelay)
QApplication.instance().processEvents() # type: ignore QApplication.instance().processEvents() # type: ignore
while not mb.clickedButton(): while not mb.clickedButton():
txt = _("Recording...<br>Time: %0.1f") txt = tr(TR.MEDIA_RECORDINGTIME)
mb.setText(txt % (time.time() - t)) mb.setText(txt % (time.time() - t))
mb.show() mb.show()
QApplication.instance().processEvents() # type: ignore QApplication.instance().processEvents() # type: ignore

View file

@ -1,12 +1,20 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import aqt import aqt
from anki.lang import _ from anki.lang import _
from aqt import gui_hooks from aqt import gui_hooks
from aqt.qt import * from aqt.qt import *
from aqt.utils import getOnlyText, openHelp, restoreGeom, saveGeom, shortcut, showInfo from aqt.utils import (
TR,
getOnlyText,
openHelp,
restoreGeom,
saveGeom,
shortcut,
showInfo,
tr,
)
class StudyDeck(QDialog): class StudyDeck(QDialog):
@ -43,9 +51,9 @@ class StudyDeck(QDialog):
for b in buttons: for b in buttons:
self.form.buttonBox.addButton(b, QDialogButtonBox.ActionRole) self.form.buttonBox.addButton(b, QDialogButtonBox.ActionRole)
else: else:
b = QPushButton(_("Add")) b = QPushButton(tr(TR.ACTIONS_ADD))
b.setShortcut(QKeySequence("Ctrl+N")) b.setShortcut(QKeySequence("Ctrl+N"))
b.setToolTip(shortcut(_("Add New Deck (Ctrl+N)"))) b.setToolTip(shortcut(tr(TR.DECKS_ADD_NEW_DECK_CTRLANDN)))
self.form.buttonBox.addButton(b, QDialogButtonBox.ActionRole) self.form.buttonBox.addButton(b, QDialogButtonBox.ActionRole)
qconnect(b.clicked, self.onAddDeck) qconnect(b.clicked, self.onAddDeck)
if title: if title:
@ -64,7 +72,7 @@ class StudyDeck(QDialog):
self.origNames = names() self.origNames = names()
self.name = None self.name = None
self.ok = self.form.buttonBox.addButton( self.ok = self.form.buttonBox.addButton(
accept or _("Study"), QDialogButtonBox.AcceptRole accept or tr(TR.DECKS_STUDY), QDialogButtonBox.AcceptRole
) )
self.setWindowModality(Qt.WindowModal) self.setWindowModality(Qt.WindowModal)
qconnect(self.form.buttonBox.helpRequested, lambda: openHelp(help)) qconnect(self.form.buttonBox.helpRequested, lambda: openHelp(help))
@ -132,7 +140,7 @@ class StudyDeck(QDialog):
gui_hooks.state_did_reset.remove(self.onReset) gui_hooks.state_did_reset.remove(self.onReset)
row = self.form.list.currentRow() row = self.form.list.currentRow()
if row < 0: if row < 0:
showInfo(_("Please select something.")) showInfo(tr(TR.DECKS_PLEASE_SELECT_SOMETHING))
return return
self.name = self.names[self.form.list.currentRow()] self.name = self.names[self.form.list.currentRow()]
QDialog.accept(self) QDialog.accept(self)
@ -148,7 +156,7 @@ class StudyDeck(QDialog):
default = self.form.filter.text() default = self.form.filter.text()
else: else:
default = self.names[self.form.list.currentRow()] default = self.names[self.form.list.currentRow()]
n = getOnlyText(_("New deck name:"), default=default) n = getOnlyText(tr(TR.DECKS_NEW_DECK_NAME), default=default)
n = n.strip() n = n.strip()
if n: if n:
did = self.mw.col.decks.id(n) did = self.mw.col.decks.id(n)

View file

@ -1,7 +1,6 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations from __future__ import annotations
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
@ -12,6 +11,7 @@ from anki.rsbackend import SyncStatus
from aqt import gui_hooks from aqt import gui_hooks
from aqt.qt import * from aqt.qt import *
from aqt.sync import get_sync_status from aqt.sync import get_sync_status
from aqt.utils import TR, tr
from aqt.webview import AnkiWebView from aqt.webview import AnkiWebView
@ -102,30 +102,30 @@ class Toolbar:
links = [ links = [
self.create_link( self.create_link(
"decks", "decks",
_("Decks"), tr(TR.ACTIONS_DECKS),
self._deckLinkHandler, self._deckLinkHandler,
tip=_("Shortcut key: %s") % "D", tip=tr(TR.ACTIONS_SHORTCUT_KEY, val="%s") % "D",
id="decks", id="decks",
), ),
self.create_link( self.create_link(
"add", "add",
_("Add"), tr(TR.ACTIONS_ADD),
self._addLinkHandler, self._addLinkHandler,
tip=_("Shortcut key: %s") % "A", tip=tr(TR.ACTIONS_SHORTCUT_KEY, val="%s") % "A",
id="add", id="add",
), ),
self.create_link( self.create_link(
"browse", "browse",
_("Browse"), tr(TR.QT_MISC_BROWSE),
self._browseLinkHandler, self._browseLinkHandler,
tip=_("Shortcut key: %s") % "B", tip=tr(TR.ACTIONS_SHORTCUT_KEY, val="%s") % "B",
id="browse", id="browse",
), ),
self.create_link( self.create_link(
"stats", "stats",
_("Stats"), tr(TR.QT_MISC_STATS),
self._statsLinkHandler, self._statsLinkHandler,
tip=_("Shortcut key: %s") % "T", tip=tr(TR.ACTIONS_SHORTCUT_KEY, val="%s") % "T",
id="stats", id="stats",
), ),
] ]
@ -140,8 +140,8 @@ class Toolbar:
###################################################################### ######################################################################
def _create_sync_link(self) -> str: def _create_sync_link(self) -> str:
name = _("Sync") name = tr(TR.QT_MISC_SYNC)
title = _("Shortcut key: %s") % "Y" title = tr(TR.ACTIONS_SHORTCUT_KEY, val="%s") % "Y"
label = "sync" label = "sync"
self.link_handlers[label] = self._syncLinkHandler self.link_handlers[label] = self._syncLinkHandler

View file

@ -9,7 +9,7 @@ import aqt
from anki.lang import _ from anki.lang import _
from anki.utils import platDesc, versionWithBuild from anki.utils import platDesc, versionWithBuild
from aqt.qt import * from aqt.qt import *
from aqt.utils import openLink, showText from aqt.utils import TR, openLink, showText, tr
class LatestVersionFinder(QThread): class LatestVersionFinder(QThread):
@ -56,12 +56,12 @@ class LatestVersionFinder(QThread):
def askAndUpdate(mw, ver): def askAndUpdate(mw, ver):
baseStr = _("""<h1>Anki Updated</h1>Anki %s has been released.<br><br>""") % ver baseStr = tr(TR.QT_MISC_ANKI_UPDATEDANKI_HAS_BEEN_RELEASED, val="%s") % ver
msg = QMessageBox(mw) msg = QMessageBox(mw)
msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) # type: ignore msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) # type: ignore
msg.setIcon(QMessageBox.Information) msg.setIcon(QMessageBox.Information)
msg.setText(baseStr + _("Would you like to download it now?")) msg.setText(baseStr + tr(TR.QT_MISC_WOULD_YOU_LIKE_TO_DOWNLOAD_IT))
button = QPushButton(_("Ignore this update")) button = QPushButton(tr(TR.QT_MISC_IGNORE_THIS_UPDATE))
msg.addButton(button, QMessageBox.RejectRole) msg.addButton(button, QMessageBox.RejectRole)
msg.setDefaultButton(QMessageBox.Yes) msg.setDefaultButton(QMessageBox.Yes)
ret = msg.exec_() ret = msg.exec_()

View file

@ -1,7 +1,6 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations from __future__ import annotations
import os import os
@ -54,7 +53,7 @@ def openHelp(section):
def openLink(link): def openLink(link):
tooltip(_("Loading..."), period=1000) tooltip(tr(TR.QT_MISC_LOADING), period=1000)
with noBundledLibs(): with noBundledLibs():
QDesktopServices.openUrl(QUrl(link)) QDesktopServices.openUrl(QUrl(link))
@ -145,7 +144,7 @@ def showText(
def onCopy(): def onCopy():
QApplication.clipboard().setText(text.toPlainText()) QApplication.clipboard().setText(text.toPlainText())
btn = QPushButton(_("Copy to Clipboard")) btn = QPushButton(tr(TR.QT_MISC_COPY_TO_CLIPBOARD))
qconnect(btn.clicked, onCopy) qconnect(btn.clicked, onCopy)
box.addButton(btn, QDialogButtonBox.ActionRole) box.addButton(btn, QDialogButtonBox.ActionRole)
@ -209,8 +208,8 @@ class ButtonedDialog(QMessageBox):
for b in buttons: for b in buttons:
self._buttons.append(self.addButton(b, QMessageBox.AcceptRole)) self._buttons.append(self.addButton(b, QMessageBox.AcceptRole))
if help: if help:
self.addButton(_("Help"), QMessageBox.HelpRole) self.addButton(tr(TR.ACTIONS_HELP), QMessageBox.HelpRole)
buttons.append(_("Help")) buttons.append(tr(TR.ACTIONS_HELP))
# self.setLayout(v) # self.setLayout(v)
def run(self): def run(self):
@ -408,9 +407,7 @@ def getSaveFile(parent, title, dir_description, key, ext, fname=None):
aqt.mw.pm.profile[config_key] = dir aqt.mw.pm.profile[config_key] = dir
# check if it exists # check if it exists
if os.path.exists(file): if os.path.exists(file):
if not askUser( if not askUser(tr(TR.QT_MISC_THIS_FILE_EXISTS_ARE_YOU_SURE), parent):
_("This file exists. Are you sure you want to overwrite it?"), parent
):
return None return None
return file return file
@ -656,7 +653,7 @@ def closeTooltip():
def checkInvalidFilename(str, dirsep=True): def checkInvalidFilename(str, dirsep=True):
bad = invalidFilename(str, dirsep) bad = invalidFilename(str, dirsep)
if bad: if bad:
showWarning(_("The following character can not be used: %s") % bad) showWarning(tr(TR.QT_MISC_THE_FOLLOWING_CHARACTER_CAN_NOT_BE, val="%s") % bad)
return True return True
return False return False

View file

@ -1,7 +1,6 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import dataclasses import dataclasses
import json import json
import re import re
@ -14,7 +13,7 @@ from anki.utils import isLin, isMac, isWin
from aqt import gui_hooks from aqt import gui_hooks
from aqt.qt import * from aqt.qt import *
from aqt.theme import theme_manager from aqt.theme import theme_manager
from aqt.utils import openLink, showInfo from aqt.utils import TR, openLink, showInfo, tr
serverbaseurl = re.compile(r"^.+:\/\/[^\/]+") serverbaseurl = re.compile(r"^.+:\/\/[^\/]+")
@ -286,7 +285,7 @@ class AnkiWebView(QWebEngineView):
def contextMenuEvent(self, evt: QContextMenuEvent) -> None: def contextMenuEvent(self, evt: QContextMenuEvent) -> None:
m = QMenu(self) m = QMenu(self)
a = m.addAction(_("Copy")) a = m.addAction(tr(TR.ACTIONS_COPY))
qconnect(a.triggered, self.onCopy) qconnect(a.triggered, self.onCopy)
gui_hooks.webview_will_show_context_menu(self, m) gui_hooks.webview_will_show_context_menu(self, m)
m.popup(QCursor.pos()) m.popup(QCursor.pos())

View file

@ -129,38 +129,43 @@ def register_repos():
qtpo_i18n_commit = "872d7f0f6bde52577e8fc795dd85699b0eeb97d5" qtpo_i18n_commit = "872d7f0f6bde52577e8fc795dd85699b0eeb97d5"
qtpo_i18n_shallow_since = "1605564627 +0000" qtpo_i18n_shallow_since = "1605564627 +0000"
new_git_repository( i18n_build_content = """
name = "rslib_ftl",
build_file_content = """
filegroup( filegroup(
name = "files", name = "files",
srcs = glob(["**/*.ftl"]), srcs = glob(["**/*.ftl"]),
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
) )
exports_files(["l10n.toml"]) exports_files(["l10n.toml"])
""", """
commit = core_i18n_commit,
shallow_since = core_i18n_shallow_since, native.new_local_repository(
remote = "https://github.com/ankitects/anki-core-i18n", name = "rslib_ftl",
path = "../anki-i18n/core",
build_file_content = i18n_build_content,
) )
# new_git_repository(
# name = "rslib_ftl",
# build_file_content = i18n_build_content,
# commit = core_i18n_commit,
# shallow_since = core_i18n_shallow_since,
# remote = "https://github.com/ankitects/anki-core-i18n",
# )
if not native.existing_rule("extra_ftl"): if not native.existing_rule("extra_ftl"):
new_git_repository( native.new_local_repository(
name = "extra_ftl", name = "extra_ftl",
build_file_content = """ path = "../anki-i18n/qtftl",
filegroup( build_file_content = i18n_build_content,
name = "files",
srcs = glob(["**/*.ftl"]),
visibility = ["//visibility:public"],
) )
exports_files(["l10n.toml"]) # new_git_repository(
""", # name = "extra_ftl",
commit = qtftl_i18n_commit, # build_file_content = i18n_build_content,
shallow_since = qtftl_i18n_shallow_since, # commit = qtftl_i18n_commit,
remote = "https://github.com/ankitects/anki-desktop-ftl", # shallow_since = qtftl_i18n_shallow_since,
) # remote = "https://github.com/ankitects/anki-desktop-ftl",
# )
new_git_repository( new_git_repository(
name = "aqt_po", name = "aqt_po",

32
rslib/ftl/actions.ftl Normal file
View file

@ -0,0 +1,32 @@
actions-add = Add
actions-blue-flag = Blue Flag
actions-cancel = Cancel
actions-choose = Choose
actions-close = Close
actions-copy = Copy
actions-custom-study = Custom Study
actions-decks = Decks
actions-delete = Delete
actions-export = Export
actions-filter = Filter
actions-green-flag = Green Flag
actions-help = Help
actions-import = Import
actions-manage = Manage...
actions-name = Name:
actions-new = New
actions-new-name = New name:
actions-options = Options
actions-options-for = Options for { $val }
actions-orange-flag = Orange Flag
actions-preview = Preview
actions-rebuild = Rebuild
actions-red-flag = Red Flag
actions-rename = Rename
actions-rename-deck = Rename Deck
actions-replay-audio = Replay Audio
actions-reposition = Reposition
actions-save = Save
actions-search = Search
actions-shortcut-key = Shortcut key: { $val }
actions-suspend-card = Suspend Card

9
rslib/ftl/adding.ftl Normal file
View file

@ -0,0 +1,9 @@
adding-add-shortcut-ctrlandenter = Add (shortcut: ctrl+enter)
adding-added = Added
adding-close-and-lose-current-input = Close and lose current input?
adding-edit = Edit "{ $val }"
adding-history = History
adding-note-deleted = (Note deleted)
adding-shortcut = Shortcut: { $val }
adding-the-first-field-is-empty = The first field is empty.
adding-you-have-a-cloze-deletion-note = You have a cloze deletion note type but have not made any cloze deletions. Proceed?

104
rslib/ftl/browsing.ftl Normal file
View file

@ -0,0 +1,104 @@
browsing-add-notes = Add Notes...
browsing-add-tags = Add Tags
browsing-add-tags2 = Add Tags...
browsing-added-today = Added Today
browsing-addon = Add-on
browsing-again-today = Again Today
browsing-all-card-types = All Card Types
browsing-all-fields = All Fields
browsing-answer = Answer
browsing-any-cards-mapped-to-nothing-will = Any cards mapped to nothing will be deleted. If a note has no remaining cards, it will be lost. Are you sure you want to continue?
browsing-any-flag = Any Flag
browsing-browser-appearance = Browser Appearance
browsing-browser-options = Browser Options
browsing-buried = Buried
browsing-card = Card
browsing-card-list = Card List
browsing-card-state = Card State
browsing-cards-cant-be-manually-moved-into = Cards can't be manually moved into a filtered deck.
browsing-change-deck = Change Deck
browsing-change-deck2 = Change Deck...
browsing-change-note-type = Change Note Type
browsing-change-note-type2 = Change Note Type...
browsing-change-to = Change { $val } to:
browsing-clear-unused = Clear Unused
browsing-clear-unused-tags = Clear Unused Tags
browsing-created = Created
browsing-ctrlandshiftande = Ctrl+Shift+E
browsing-current-deck = Current Deck
browsing-current-note-type = Current note type:
browsing-delete-notes = Delete Notes
browsing-delete-tags = Delete Tags
browsing-duplicate = duplicate
browsing-ease = Ease
browsing-end = End
browsing-enter-tags-to-add = Enter tags to add:
browsing-enter-tags-to-delete = Enter tags to delete:
browsing-filter = Filter...
browsing-filtered = (filtered)
browsing-find = <b>Find</b>:
browsing-find-and-replace = Find and Replace
browsing-find-duplicates = Find Duplicates
browsing-first-card = First Card
browsing-flag = Flag
browsing-font = <b>Font</b>:
browsing-font-size = <b>Font Size</b>:
browsing-found-as-across-bs = Found %(a)s across %(b)s.
browsing-home = Home
browsing-ignore-case = Ignore case
browsing-in = <b>In</b>:
browsing-interval = Interval
browsing-last-card = Last Card
browsing-learning = (learning)
browsing-line-size = <b>Line Size</b>:
browsing-manage-note-types = Manage Note Types...
browsing-move-cards = Move Cards
browsing-move-cards-to-deck = Move cards to deck:
browsing-nd-names = %(n)d: %(name)s
browsing-new = (new)
browsing-new-note-type = New note type:
browsing-no-flag = No Flag
browsing-note = Note
browsing-notes-tagged = Notes tagged.
browsing-nothing = Nothing
browsing-only-new-cards-can-be-repositioned = Only new cards can be repositioned.
browsing-optional-filter = Optional filter:
browsing-override-back-template = Override back template:
browsing-override-font = Override font:
browsing-override-front-template = Override front template:
browsing-place-at-end-of-new-card = Place at end of new card queue
browsing-place-in-review-queue-with-interval = Place in review queue with interval between:
browsing-please-give-your-filter-a-name = Please give your filter a name:
browsing-please-select-cards-from-only-one = Please select cards from only one note type.
browsing-preview-selected-card = Preview Selected Card ({ $val })
browsing-question = Question
browsing-queue-bottom = Queue bottom: { $val }
browsing-queue-top = Queue top: { $val }
browsing-randomize-order = Randomize order
browsing-remove-current-filter = Remove Current Filter...
browsing-remove-from-your-saved-searches = Remove { $val } from your saved searches?
browsing-remove-tags = Remove Tags...
browsing-replace-with = <b>Replace With</b>:
browsing-reposition = Reposition...
browsing-reposition-new-cards = Reposition New Cards
browsing-reschedule = Reschedule
browsing-save-current-filter = Save Current Filter...
browsing-search-in = Search in:
browsing-search-within-formatting-slow = Search within formatting (slow)
browsing-shift-position-of-existing-cards = Shift position of existing cards
browsing-sidebar = Sidebar
browsing-sort-field = Sort Field
browsing-sorting-on-this-column-is-not = Sorting on this column is not supported. Please choose another.
browsing-start-position = Start position:
browsing-step = Step:
browsing-studied-today = Studied Today
browsing-suspended = Suspended
browsing-tag-duplicates = Tag Duplicates
browsing-target-field = Target field:
browsing-today = Today
browsing-toggle-mark = Toggle Mark
browsing-toggle-suspend = Toggle Suspend
browsing-treat-input-as-regular-expression = Treat input as regular expression
browsing-type-here-to-search = <type here to search; hit enter to show current deck>
browsing-whole-collection = Whole Collection
browsing-you-must-have-at-least-one = You must have at least one column.

View file

@ -21,3 +21,22 @@ card-templates-preview-settings = Options
card-templates-invalid-template-number = Card template { $number } has a problem. card-templates-invalid-template-number = Card template { $number } has a problem.
card-templates-changes-saved = Changes saved. card-templates-changes-saved = Changes saved.
card-templates-discard-changes = Discard changes? card-templates-discard-changes = Discard changes?
card-templates-add-card-type = Add Card Type...
card-templates-anki-couldnt-find-the-line-between = Anki couldn't find the line between the question and answer. Please adjust the template manually to switch the question and answer.
card-templates-at-least-one-card-type-is = At least one card type is required.
card-templates-browser-appearance = Browser Appearance...
card-templates-card = Card { $val }
card-templates-card-types = Card Types
card-templates-card-types-for = Card Types for { $val }
card-templates-cloze = Cloze { $val }
card-templates-deck-override = Deck Override...
card-templates-delete-the-as-card-type-and = Delete the '%(a)s' card type, and its %(b)s?
card-templates-enter-deck-to-place-new = Enter deck to place new { $val } cards in, or leave blank:
card-templates-enter-new-card-position-1 = Enter new card position (1...{ $val }):
card-templates-flip = Flip
card-templates-form = Form
card-templates-off = (off)
card-templates-on = (on)
card-templates-remove-card-type = Remove Card Type...
card-templates-rename-card-type = Rename Card Type...
card-templates-reposition-card-type = Reposition Card Type...

View file

@ -1 +1,28 @@
custom-study-must-rename-deck = Please rename the existing Custom Study deck first. custom-study-must-rename-deck = Please rename the existing Custom Study deck first.
custom-study-all-cards-in-random-order-dont = All cards in random order (don't reschedule)
custom-study-all-review-cards-in-random-order = All review cards in random order
custom-study-cards = cards
custom-study-cards-from-the-deck = cards from the deck
custom-study-choose-tags = Choose Tags
custom-study-custom-study-session = Custom Study Session
custom-study-due-cards-only = Due cards only
custom-study-increase-todays-new-card-limit = Increase today's new card limit
custom-study-increase-todays-new-card-limit-by = Increase today's new card limit by
custom-study-increase-todays-review-card-limit = Increase today's review card limit
custom-study-increase-todays-review-limit-by = Increase today's review limit by
custom-study-new-cards-in-deck-over-today = New cards in deck over today limit: { $val }
custom-study-new-cards-only = New cards only
custom-study-no-cards-matched-the-criteria-you = No cards matched the criteria you provided.
custom-study-ok = OK
custom-study-preview-new-cards = Preview new cards
custom-study-preview-new-cards-added-in-the = Preview new cards added in the last
custom-study-require-one-or-more-of-these = Require one or more of these tags:
custom-study-review-ahead = Review ahead
custom-study-review-ahead-by = Review ahead by
custom-study-review-cards-forgotten-in-last = Review cards forgotten in last
custom-study-review-forgotten-cards = Review forgotten cards
custom-study-reviews-due-in-deck-over-today = Reviews due in deck over today limit: { $val }
custom-study-select = Select
custom-study-select-tags-to-exclude = Select tags to exclude:
custom-study-selective-study = Selective Study
custom-study-study-by-card-state-or-tag = Study by card state or tag

33
rslib/ftl/decks.ftl Normal file
View file

@ -0,0 +1,33 @@
decks-add-new-deck-ctrlandn = Add New Deck (Ctrl+N)
decks-are-you-sure-you-wish-to = Are you sure you wish to delete { $val }?
decks-build = Build
decks-cards-selected-by = cards selected by
decks-create-deck = Create Deck
decks-custom-steps-in-minutes = Custom steps (in minutes)
decks-deck = Deck
decks-decreasing-intervals = Decreasing intervals
decks-delete-deck = Delete Deck
decks-enable-second-filter = Enable second filter
decks-filter = Filter:
decks-filter-2 = Filter 2
decks-get-shared = Get Shared
decks-import-file = Import File
decks-increasing-intervals = Increasing intervals
decks-latest-added-first = Latest added first
decks-limit-to = Limit to
decks-minutes = minutes
decks-most-lapses = Most lapses
decks-name-for-deck = Name for deck:
decks-new-deck-name = New deck name:
decks-no-deck = [no deck]
decks-oldest-seen-first = Oldest seen first
decks-order-added = Order added
decks-order-due = Order due
decks-please-select-something = Please select something.
decks-random = Random
decks-relative-overdueness = Relative overdueness
decks-repeat-failed-cards-after = Repeat failed cards after
decks-reschedule-cards-based-on-my-answers = Reschedule cards based on my answers in this deck
decks-study = Study
decks-study-deck = Study Deck
decks-the-provided-search-did-not-match = The provided search did not match any cards. Would you like to revise it?

35
rslib/ftl/editing.ftl Normal file
View file

@ -0,0 +1,35 @@
editing-add-media = Add Media
editing-an-error-occurred-while-opening = An error occurred while opening { $val }
editing-attach-picturesaudiovideo-f3 = Attach pictures/audio/video (F3)
editing-bold-text-ctrlandb = Bold text (Ctrl+B)
editing-cards = Cards
editing-change-colour-f8 = Change colour (F8)
editing-cloze-deletion-ctrlandshiftandc = Cloze deletion (Ctrl+Shift+C)
editing-couldnt-record-audio-have-you-installed = Couldn't record audio. Have you installed 'lame'?
editing-customize-card-templates-ctrlandl = Customize Card Templates (Ctrl+L)
editing-customize-fields = Customize Fields
editing-cut = Cut
editing-edit-current = Edit Current
editing-edit-html = Edit HTML
editing-fields = Fields
editing-html-editor = HTML Editor
editing-italic-text-ctrlandi = Italic text (Ctrl+I)
editing-jump-to-tags-with-ctrlandshiftandt = Jump to tags with Ctrl+Shift+T
editing-latex = LaTeX
editing-latex-equation = LaTeX equation
editing-latex-math-env = LaTeX math env.
editing-mathjax-block = MathJax block
editing-mathjax-chemistry = MathJax chemistry
editing-mathjax-inline = MathJax inline
editing-media = Media
editing-paste = Paste
editing-record-audio-f5 = Record audio (F5)
editing-remove-formatting-ctrlandr = Remove formatting (Ctrl+R)
editing-set-foreground-colour-f7 = Set foreground colour (F7)
editing-show-duplicates = Show Duplicates
editing-subscript-ctrland = Subscript (Ctrl+=)
editing-superscript-ctrlandand = Superscript (Ctrl++)
editing-tags = Tags
editing-to-make-a-cloze-deletion-on = To make a cloze deletion on an existing note, you need to change it to a cloze type first, via 'Notes>Change Note Type'
editing-underline-text-ctrlandu = Underline text (Ctrl+U)
editing-warning-cloze-deletions-will-not-work = Warning, cloze deletions will not work until you switch the type at the top to Cloze.

17
rslib/ftl/exporting.ftl Normal file
View file

@ -0,0 +1,17 @@
exporting-all-decks = All Decks
exporting-anki-20-deck = Anki 2.0 Deck
exporting-anki-collection-package = Anki Collection Package
exporting-anki-deck-package = Anki Deck Package
exporting-cards-in-plain-text = Cards in Plain Text
exporting-collection = collection
exporting-collection-exported = Collection exported.
exporting-couldnt-save-file = Couldn't save file: { $val }
exporting-export = Export...
exporting-export-format = <b>Export format</b>:
exporting-include = <b>Include</b>:
exporting-include-html-and-media-references = Include HTML and media references
exporting-include-media = Include media
exporting-include-scheduling-information = Include scheduling information
exporting-include-tags = Include tags
exporting-notes-in-plain-text = Notes in Plain Text
exporting-selected-notes = Selected Notes

14
rslib/ftl/fields.ftl Normal file
View file

@ -0,0 +1,14 @@
fields-add-field = Add Field
fields-delete-field-from = Delete field from { $val }?
fields-editing-font = Editing Font
fields-field = Field:
fields-field-name = Field name:
fields-fields-for = Fields for { $val }
fields-font = Font:
fields-new-position-1 = New position (1...{ $val }):
fields-notes-require-at-least-one-field = Notes require at least one field.
fields-remember-last-input-when-adding = Remember last input when adding
fields-reverse-text-direction-rtl = Reverse text direction (RTL)
fields-size = Size:
fields-sort-by-this-field-in-the = Sort by this field in the browser
fields-that-field-name-is-already-used = That field name is already used.

View file

@ -1 +1,49 @@
importing-failed-debug-info = Import failed. Debugging info: importing-failed-debug-info = Import failed. Debugging info:
importing-aborted = Aborted: { $val }
importing-added-duplicate-with-first-field = Added duplicate with first field: { $val }
importing-allow-html-in-fields = Allow HTML in fields
importing-anki-files-are-from-a-very = .anki files are from a very old version of Anki. You can import them with Anki 2.0, available on the Anki website.
importing-anki2-files-are-not-directly-importable = .anki2 files are not directly importable - please import the .apkg or .zip file you have received instead.
importing-appeared-twice-in-file = Appeared twice in file: { $val }
importing-by-default-anki-will-detect-the = By default, Anki will detect the character between fields, such as a tab, comma, and so on. If Anki is detecting the character incorrectly, you can enter it here. Use \t to represent tab.
importing-change = Change
importing-colon = Colon
importing-comma = Comma
importing-empty-first-field = Empty first field: { $val }
importing-field-mapping = Field mapping
importing-field-of-file-is = Field <b>{ $val }</b> of file is:
importing-fields-separated-by = Fields separated by: { $val }
importing-file-version-unknown-trying-import-anyway = File version unknown, trying import anyway.
importing-first-field-matched = First field matched: { $val }
importing-identical = Identical
importing-ignore-field = Ignore field
importing-ignore-lines-where-first-field-matches = Ignore lines where first field matches existing note
importing-ignored = <ignored>
importing-import-even-if-existing-note-has = Import even if existing note has same first field
importing-import-options = Import options
importing-importing-complete = Importing complete.
importing-invalid-file-please-restore-from-backup = Invalid file. Please restore from backup.
importing-map-to = Map to { $val }
importing-map-to-tags = Map to Tags
importing-mapped-to = mapped to <b>{ $val }</b>
importing-mapped-to-tags = mapped to <b>Tags</b>
importing-multicharacter-separators-are-not-supported-please = Multi-character separators are not supported. Please enter one character only.
importing-notes-added-from-file = Notes added from file: { $val }
importing-notes-found-in-file = Notes found in file: { $val }
importing-notes-skipped-as-theyre-already-in = Notes skipped, as they're already in your collection: { $val }
importing-notes-that-could-not-be-imported = Notes that could not be imported as note type has changed: { $val }
importing-notes-updated-as-file-had-newer = Notes updated, as file had newer version: { $val }
importing-rows-had-num1d-fields-expected-num2d = '%(row)s' had %(num1)d fields, expected %(num2)d
importing-selected-file-was-not-in-utf8 = Selected file was not in UTF-8 format. Please see the importing section of the manual.
importing-semicolon = Semicolon
importing-skipped = Skipped
importing-tab = Tab
importing-tag-modified-notes = Tag modified notes:
importing-the-first-field-of-the-note = The first field of the note type must be mapped.
importing-the-provided-file-is-not-a = The provided file is not a valid .apkg file.
importing-this-file-does-not-appear-to = This file does not appear to be a valid .apkg file. If you're getting this error from a file downloaded from AnkiWeb, chances are that your download failed. Please try again, and if the problem persists, please try again with a different browser.
importing-this-will-delete-your-existing-collection = This will delete your existing collection and replace it with the data in the file you're importing. Are you sure?
importing-unable-to-import-from-a-readonly = Unable to import from a read-only file.
importing-unknown-file-format = Unknown file format.
importing-update-existing-notes-when-first-field = Update existing notes when first field matches
importing-updated = Updated

7
rslib/ftl/media.ftl Normal file
View file

@ -0,0 +1,7 @@
media-error-executing = Error executing { $val }.
media-error-running = Error running { $val }
media-for-security-reasons-is-not = For security reasons, '{ $val }' is not allowed on cards. You can still use it by placing the command in a different package, and importing that package in the LaTeX header instead.
media-generated-file = Generated file: { $val }
media-have-you-installed-latex-and-dvipngdvisvgm = Have you installed latex and dvipng/dvisvgm?
media-recordingtime = Recording...<br>Time: %0.1f
media-sound-and-video-on-cards-will = Sound and video on cards will not function until mpv or mplayer is installed.

View file

@ -19,3 +19,18 @@ notetypes-cloze-name = Cloze
notetypes-card-1-name = Card 1 notetypes-card-1-name = Card 1
notetypes-card-2-name = Card 2 notetypes-card-2-name = Card 2
notetypes-add = Add: { $val }
notetypes-add-note-type = Add Note Type
notetypes-cards = Cards...
notetypes-clone = Clone: { $val }
notetypes-copy = { $val } copy
notetypes-create-scalable-images-with-dvisvgm = Create scalable images with dvisvgm
notetypes-delete-this-note-type-and-all = Delete this note type and all its cards?
notetypes-delete-this-unused-note-type = Delete this unused note type?
notetypes-fields = Fields...
notetypes-footer = Footer
notetypes-header = Header
notetypes-note-types = Note Types
notetypes-options = Options...
notetypes-please-add-another-note-type-first = Please add another note type first.
notetypes-type = Type

39
rslib/ftl/preferences.ftl Normal file
View file

@ -0,0 +1,39 @@
preferences-anki-21-scheduler-beta = Anki 2.1 scheduler (beta)
preferences-automatically-sync-on-profile-openclose = Automatically sync on profile open/close
preferences-backups = Backups
preferences-backups2 = backups
preferences-backupsanki-will-create-a-backup-of = <html><head/><body><p><span style=" font-weight:600;">Backups</span><br/>Anki will create a backup of your collection each time it is closed.</p></body></html>
preferences-basic = Basic
preferences-change-deck-depending-on-note-type = Change deck depending on note type
preferences-changes-will-take-effect-when-you = Changes will take effect when you restart Anki.
preferences-hardware-acceleration-faster-may-cause-display = Hardware acceleration (faster, may cause display issues)
preferences-hours-past-midnight = hours past midnight
preferences-interface-language = Interface language:
preferences-interrupt-current-audio-when-answering = Interrupt current audio when answering
preferences-keep = Keep
preferences-learn-ahead-limit = Learn ahead limit
preferences-mins = mins
preferences-network = Network
preferences-next-day-starts-at = Next day starts at
preferences-night-mode = Night mode
preferences-note-media-is-not-backed-up = Note: Media is not backed up. Please create a periodic backup of your Anki folder to be safe.
preferences-on-next-sync-force-changes-in = On next sync, force changes in one direction
preferences-paste-clipboard-images-as-png = Paste clipboard images as PNG
preferences-paste-without-shift-key-strips-formatting = Paste without shift key strips formatting
preferences-periodically-sync-media = Periodically sync media
preferences-please-restart-anki-to-complete-language = Please restart Anki to complete language change.
preferences-preferences = Preferences
preferences-scheduling = Scheduling
preferences-show-learning-cards-with-larger-steps = Show learning cards with larger steps before reviews
preferences-show-next-review-time-above-answer = Show next review time above answer buttons
preferences-show-play-buttons-on-cards-with = Show play buttons on cards with audio
preferences-show-remaining-card-count-during-review = Show remaining card count during review
preferences-some-settings-will-take-effect-after = Some settings will take effect after you restart Anki.
preferences-synchronisation = <b>Synchronisation</b>
preferences-synchronizationnot-currently-enabled-click-the-sync = <b>Synchronization</b><br> Not currently enabled; click the sync button in the main window to enable.
preferences-synchronize-audio-and-images-too = Synchronize audio and images too
preferences-this-will-reset-any-cards-in = This will reset any cards in learning, clear filtered decks, and change the scheduler version. Proceed?
preferences-timebox-time-limit = Timebox time limit
preferences-user-interface-size = User interface size
preferences-when-adding-default-to-current-deck = When adding, default to current deck
preferences-you-can-restore-backups-via-fileswitch = You can restore backups via File>Switch Profile.

10
rslib/ftl/profiles.ftl Normal file
View file

@ -0,0 +1,10 @@
profiles-anki-could-not-read-your-profile = Anki could not read your profile data. Window sizes and your sync login details have been forgotten.
profiles-anki-could-not-rename-your-profile = Anki could not rename your profile because it could not rename the profile folder on disk. Please ensure you have permission to write to Documents/Anki and no other programs are accessing your profile folders, then try again.
profiles-folder-already-exists = Folder already exists.
profiles-open = Open
profiles-open-backup = Open Backup...
profiles-please-remove-the-folder-and = Please remove the folder { $val } and try again.
profiles-profile-corrupt = Profile Corrupt
profiles-profiles = Profiles
profiles-quit = Quit
profiles-user-1 = User 1

View file

@ -90,3 +90,51 @@ scheduling-how-to-custom-study = If you wish to study outside of the regular sch
# used in scheduling-how-to-custom-study # used in scheduling-how-to-custom-study
# "... you can use the custom study feature." # "... you can use the custom study feature."
scheduling-custom-study = custom study scheduling-custom-study = custom study
scheduling-always-include-question-side-when-replaying = Always include question side when replaying audio
scheduling-at-least-one-step-is-required = At least one step is required.
scheduling-automatically-play-audio = Automatically play audio
scheduling-bury-related-new-cards-until-the = Bury related new cards until the next day
scheduling-bury-related-reviews-until-the-next = Bury related reviews until the next day
scheduling-days = days
scheduling-description = Description
scheduling-description-to-show-on-overview-screen = Description to show on overview screen, for current deck:
scheduling-easy-bonus = Easy bonus
scheduling-easy-interval = Easy interval
scheduling-end = (end)
scheduling-general = General
scheduling-graduating-interval = Graduating interval
scheduling-hard-interval = Hard interval
scheduling-ignore-answer-times-longer-than = Ignore answer times longer than
scheduling-interval-modifier = Interval modifier
scheduling-lapses = Lapses
scheduling-lapses2 = lapses
scheduling-learning = Learning
scheduling-leech-action = Leech action
scheduling-leech-threshold = Leech threshold
scheduling-maximum-interval = Maximum interval
scheduling-maximum-reviewsday = Maximum reviews/day
scheduling-minimum-interval = Minimum interval
scheduling-mix-new-cards-and-reviews = Mix new cards and reviews
scheduling-new-cards = New Cards
scheduling-new-cardsday = New cards/day
scheduling-new-interval = New interval
scheduling-new-options-group-name = New options group name:
scheduling-options-group = Options group:
scheduling-order = Order
scheduling-parent-limit = (parent limit: { $val })
scheduling-review = Review
scheduling-reviews = Reviews
scheduling-seconds = seconds
scheduling-set-all-decks-below-to = Set all decks below { $val } to this option group?
scheduling-set-for-all-subdecks = Set for all subdecks
scheduling-show-answer-timer = Show answer timer
scheduling-show-new-cards-after-reviews = Show new cards after reviews
scheduling-show-new-cards-before-reviews = Show new cards before reviews
scheduling-show-new-cards-in-order-added = Show new cards in order added
scheduling-show-new-cards-in-random-order = Show new cards in random order
scheduling-starting-ease = Starting ease
scheduling-steps-in-minutes = Steps (in minutes)
scheduling-steps-must-be-numbers = Steps must be numbers.
scheduling-tag-only = Tag Only
scheduling-the-default-configuration-cant-be-removed = The default configuration can't be removed.
scheduling-your-changes-will-affect-multiple-decks = Your changes will affect multiple decks. If you wish to change only the current deck, please add a new options group first.

44
rslib/ftl/studying.ftl Normal file
View file

@ -0,0 +1,44 @@
studying-again = Again
studying-all-buried-cards = All Buried Cards
studying-audio-5s = Audio -5s
studying-audio-and5s = Audio +5s
studying-buried-siblings = Buried Siblings
studying-bury = Bury
studying-bury-card = Bury Card
studying-bury-note = Bury Note
studying-card-buried = Card buried.
studying-card-suspended = Card suspended.
studying-card-was-a-leech = Card was a leech.
studying-cards-will-be-automatically-returned-to = Cards will be automatically returned to their original decks after you review them.
studying-continue = Continue
studying-delete-note = Delete Note
studying-deleting-this-deck-from-the-deck = Deleting this deck from the deck list will return all remaining cards to their original deck.
studying-easy = Easy
studying-edit = Edit
studying-empty = Empty
studying-finish = Finish
studying-flag-card = Flag Card
studying-good = Good
studying-hard = Hard
studying-it-has-been-suspended = It has been suspended.
studying-manually-buried-cards = Manually Buried Cards
studying-mark-note = Mark Note
studying-more = More
studying-no-cards-are-due-yet = No cards are due yet.
studying-note-buried = Note buried.
studying-note-suspended = Note suspended.
studying-pause-audio = Pause Audio
studying-please-run-toolsempty-cards = Please run Tools>Empty Cards
studying-record-own-voice = Record Own Voice
studying-replay-own-voice = Replay Own Voice
studying-show-answer = Show Answer
studying-space = Space
studying-study-now = Study Now
studying-suspend = Suspend
studying-suspend-note = Suspend Note
studying-this-is-a-special-deck-for = This is a special deck for studying outside of the normal schedule.
studying-to-review = To Review
studying-type-answer-unknown-field = Type answer: unknown field { $val }
studying-unbury = Unbury
studying-what-would-you-like-to-unbury = What would you like to unbury?
studying-you-havent-recorded-your-voice-yet = You haven't recorded your voice yet.