update multi-line _() references

This commit is contained in:
Damien Elmes 2020-11-18 11:32:22 +10:00
parent b49805fef5
commit ba336d5de3
20 changed files with 57 additions and 315 deletions

View file

@ -93,14 +93,8 @@ def show(mw):
# WebView contents
######################################################################
abouttext = "<center><img src='/_anki/imgs/anki-logo-thin.png'></center>"
abouttext += "<p>" + _(
"Anki is a friendly, intelligent spaced learning \
system. It's free and open source."
)
abouttext += "<p>" + _(
"Anki is licensed under the AGPL3 license. Please see "
"the license file in the source distribution for more information."
)
abouttext += "<p>" + tr(TR.ABOUT_ANKI_IS_A_FRIENDLY_INTELLIGENT_SPACED)
abouttext += "<p>" + tr(TR.ABOUT_ANKI_IS_LICENSED_UNDER_THE_AGPL3)
abouttext += "<p>" + tr(TR.ABOUT_VERSION, val=versionWithBuild()) + "<br>"
abouttext += ("Python %s Qt %s PyQt %s<br>") % (
platform.python_version(),
@ -209,22 +203,11 @@ system. It's free and open source."
)
)
abouttext += (
"<p>"
+ _(
"Written by Damien Elmes, with patches, translation,\
testing and design from:<p>%(cont)s"
)
% {"cont": ", ".join(allusers)}
)
abouttext += "<p>" + _(
"If you have contributed and are not on this list, \
please get in touch."
)
abouttext += "<p>" + _(
"A big thanks to all the people who have provided \
suggestions, bug reports and donations."
)
abouttext += "<p>" + tr(TR.ABOUT_WRITTEN_BY_DAMIEN_ELMES_WITH_PATCHES) % {
"cont": ", ".join(allusers)
}
abouttext += "<p>" + tr(TR.ABOUT_IF_YOU_HAVE_CONTRIBUTED_AND_ARE)
abouttext += "<p>" + tr(TR.ABOUT_A_BIG_THANKS_TO_ALL_THE)
abt.label.setMinimumWidth(800)
abt.label.setMinimumHeight(600)
dialog.show()

View file

@ -175,12 +175,7 @@ class AddCards(QDialog):
return None
if note.model()["type"] == MODEL_CLOZE:
if not note.cloze_numbers_in_fields():
if not askUser(
_(
"You have a cloze deletion note type "
"but have not made any cloze deletions. Proceed?"
)
):
if not askUser(tr(TR.ADDING_YOU_HAVE_A_CLOZE_DELETION_NOTE)):
return None
self.mw.col.add_note(note, self.deckChooser.selectedId())
self.mw.col.clearUndo()

View file

@ -836,12 +836,7 @@ class AddonsDialog(QDialog):
if not selected:
return
if not askUser(
ngettext(
"Delete the %(num)d selected add-on?",
"Delete the %(num)d selected add-ons?",
len(selected),
)
% dict(num=len(selected))
tr(TR.ADDONS_DELETE_THE_NUMD_SELECTED_ADDON, count=len(selected))
):
return
for dir in selected:

View file

@ -17,7 +17,6 @@ import aqt.forms
from anki.cards import Card
from anki.collection import Collection
from anki.consts import *
from anki.lang import _, ngettext
from anki.models import NoteType
from anki.notes import Note
from anki.rsbackend import TR, DeckTreeNode, InvalidInput
@ -821,16 +820,10 @@ class Browser(QMainWindow):
def updateTitle(self):
selected = len(self.form.tableView.selectionModel().selectedRows())
cur = len(self.model.cards)
print("fixme: browser updateTitle()")
self.setWindowTitle(
ngettext(
"Browse (%(cur)d card shown; %(sel)s)",
"Browse (%(cur)d cards shown; %(sel)s)",
cur,
)
% {
"cur": cur,
"sel": ngettext("%d selected", "%d selected", selected) % selected,
}
"Browse (%(cur)d cards shown; %(sel)s)"
% {"cur": cur, "sel": self.col.tr(TR.BROWSING_SELECTED, count=selected)}
)
return selected
@ -2266,14 +2259,7 @@ class ChangeModel(QDialog):
fmap = self.getFieldMap()
cmap = self.getTemplateMap()
if any(True for c in list(cmap.values()) if c is None):
if not askUser(
_(
"""\
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?"""
)
):
if not askUser(tr(TR.BROWSING_ANY_CARDS_MAPPED_TO_NOTHING_WILL)):
return
self.browser.mw.checkpoint(tr(TR.BROWSING_CHANGE_NOTE_TYPE))
b = self.browser

View file

@ -9,7 +9,6 @@ from typing import Any, Dict, List, Optional
import aqt
from anki.cards import Card
from anki.consts import *
from anki.lang import _, ngettext
from anki.notes import Note
from anki.rsbackend import TemplateError
from anki.template import TemplateRenderContext
@ -656,13 +655,7 @@ class CardLayout(QDialog):
def _flipQA(self, src, dst):
m = re.match("(?s)(.+)<hr id=answer>(.+)", src["afmt"])
if not m:
showInfo(
_(
"""\
Anki couldn't find the line between the question and answer. Please \
adjust the template manually to switch the question and answer."""
)
)
showInfo(tr(TR.CARD_TEMPLATES_ANKI_COULDNT_FIND_THE_LINE_BETWEEN))
return
self.change_tracker.mark_basic()
dst["afmt"] = "{{FrontSide}}\n\n<hr id=answer>\n\n%s" % src["qfmt"]
@ -736,10 +729,7 @@ adjust the template manually to switch the question and answer."""
d.setMinimumWidth(400)
l = QVBoxLayout()
lab = QLabel(
_(
"""\
Enter deck to place new %s cards in, or leave blank:"""
)
tr(TR.CARD_TEMPLATES_ENTER_DECK_TO_PLACE_NEW, val="%s")
% self.current_template()["name"]
)
lab.setWordWrap(True)

View file

@ -8,7 +8,6 @@ from dataclasses import dataclass
import aqt
from anki.errors import DeckRenameError
from anki.lang import _, ngettext
from anki.rsbackend import TR, DeckTreeNode
from anki.utils import ids2str
from aqt import AnkiQt, gui_hooks

View file

@ -8,7 +8,6 @@ from PyQt5.QtWidgets import QLineEdit
import aqt
from anki.consts import NEW_CARDS_RANDOM
from anki.lang import _, ngettext
from aqt import gui_hooks
from aqt.qt import *
from aqt.utils import (
@ -110,10 +109,7 @@ class DeckConf(QDialog):
self.loadConf()
cnt = len(self.mw.col.decks.didsForConf(conf))
if cnt > 1:
txt = _(
"Your changes will affect multiple decks. If you wish to "
"change only the current deck, please add a new options group first."
)
txt = tr(TR.SCHEDULING_YOUR_CHANGES_WILL_AFFECT_MULTIPLE_DECKS)
else:
txt = ""
self.form.count.setText(txt)

View file

@ -123,13 +123,7 @@ class DeckConf(QDialog):
if not self.saveConf():
return
if not self.mw.col.sched.rebuild_filtered_deck(self.deck["id"]):
if askUser(
_(
"""\
The provided search did not match any cards. Would you like to revise \
it?"""
)
):
if askUser(tr(TR.DECKS_THE_PROVIDED_SEARCH_DID_NOT_MATCH)):
return
self.mw.reset()
QDialog.accept(self)

View file

@ -658,20 +658,9 @@ class Editor:
# check that the model is set up for cloze deletion
if not re.search("{{(.*:)*cloze:", self.note.model()["tmpls"][0]["qfmt"]):
if self.addMode:
tooltip(
_(
"Warning, cloze deletions will not work until "
"you switch the type at the top to Cloze."
)
)
tooltip(tr(TR.EDITING_WARNING_CLOZE_DELETIONS_WILL_NOT_WORK))
else:
showInfo(
_(
"""\
To make a cloze deletion on an existing note, you need to change it \
to a cloze type first, via 'Notes>Change Note Type'"""
)
)
showInfo(tr(TR.EDITING_TO_MAKE_A_CLOZE_DELETION_ON))
return
# find the highest existing cloze
highest = 0

View file

@ -65,10 +65,7 @@ class ErrorHandler(QObject):
self.timer.start()
def tempFolderMsg(self):
return _(
"""Unable to access Anki media folder. The permissions on \
your system's temporary folder may be incorrect."""
)
return tr(TR.QT_MISC_UNABLE_TO_ACCESS_ANKI_MEDIA_FOLDER)
def onTimeout(self):
error = html.escape(self.pool)
@ -79,12 +76,7 @@ your system's temporary folder may be incorrect."""
if "10013" in error:
return showWarning(tr(TR.QT_MISC_YOUR_FIREWALL_OR_ANTIVIRUS_PROGRAM_IS))
if "no default input" in error.lower():
return showWarning(
_(
"Please connect a microphone, and ensure "
"other programs are not using the audio device."
)
)
return showWarning(tr(TR.QT_MISC_PLEASE_CONNECT_A_MICROPHONE_AND_ENSURE))
if "invalidTempFolder" in error:
return showWarning(self.tempFolderMsg())
if "Beautiful Soup is not an HTTP client" in error:

View file

@ -12,7 +12,6 @@ from typing import List, Optional
import aqt
from anki import hooks
from anki.exporting import Exporter, exporters
from anki.lang import _, ngettext
from aqt.qt import *
from aqt.utils import TR, checkInvalidFilename, getSaveFile, showWarning, tooltip, tr

View file

@ -3,7 +3,6 @@
import aqt
from anki.consts import *
from anki.lang import _, ngettext
from anki.models import NoteType
from anki.rsbackend import TemplateError
from aqt import AnkiQt, gui_hooks

View file

@ -15,7 +15,6 @@ import anki.importing as importing
import aqt.deckchooser
import aqt.forms
import aqt.modelchooser
from anki.lang import _, ngettext
from aqt import AnkiQt, gui_hooks
from aqt.qt import *
from aqt.utils import (
@ -118,12 +117,7 @@ class ImportDialog(QDialog):
def onDelimiter(self):
str = (
getOnlyText(
_(
"""\
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."""
),
tr(TR.IMPORTING_BY_DEFAULT_ANKI_WILL_DETECT_THE),
self,
help="importing",
)
@ -132,10 +126,7 @@ you can enter it here. Use \\t to represent tab."""
str = str.replace("\\t", "\t")
if len(str) > 1:
showWarning(
_(
"Multi-character separators are not supported. "
"Please enter one character only."
)
tr(TR.IMPORTING_MULTICHARACTER_SEPARATORS_ARE_NOT_SUPPORTED_PLEASE)
)
return
self.hideMapping()
@ -297,12 +288,7 @@ you can enter it here. Use \\t to represent tab."""
def showUnicodeWarning():
"""Shorthand to show a standard warning."""
showWarning(
_(
"Selected file was not in UTF-8 format. Please see the "
"importing section of the manual."
)
)
showWarning(tr(TR.IMPORTING_SELECTED_FILE_WAS_NOT_IN_UTF8))
def onImport(mw):
@ -390,20 +376,12 @@ def importFile(mw, file):
except Exception as e:
err = repr(str(e))
if "invalidFile" in err:
msg = _(
"""\
Invalid file. Please restore from backup."""
)
msg = tr(TR.IMPORTING_INVALID_FILE_PLEASE_RESTORE_FROM_BACKUP)
showWarning(msg)
elif "invalidTempFolder" in err:
showWarning(mw.errorHandler.tempFolderMsg())
elif "readonly" in err:
showWarning(
_(
"""\
Unable to import from a read-only file."""
)
)
showWarning(tr(TR.IMPORTING_UNABLE_TO_IMPORT_FROM_A_READONLY))
else:
msg = tr(TR.IMPORTING_FAILED_DEBUG_INFO) + "\n"
msg += str(traceback.format_exc())
@ -421,13 +399,7 @@ Unable to import from a read-only file."""
def invalidZipMsg():
return _(
"""\
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."""
)
return tr(TR.IMPORTING_THIS_FILE_DOES_NOT_APPEAR_TO)
def setupApkgImport(mw, importer):
@ -441,11 +413,7 @@ def setupApkgImport(mw, importer):
# adding
return True
if not mw.restoringBackup and not askUser(
_(
"""\
This will delete your existing collection and replace it with the data in \
the file you're importing. Are you sure?"""
),
tr(TR.IMPORTING_THIS_WILL_DELETE_YOUR_EXISTING_COLLECTION),
msgfunc=QMessageBox.warning,
defaultno=True,
):

View file

@ -29,7 +29,6 @@ from anki import hooks
from anki.collection import Collection
from anki.decks import Deck
from anki.hooks import runHook
from anki.lang import _, ngettext
from anki.rsbackend import RustBackend
from anki.sound import AVTag, SoundOrVideoTag
from anki.utils import devMode, ids2str, intTime, isMac, isWin, splitFields
@ -123,12 +122,7 @@ class AnkiQt(QMainWindow):
sys.exit(1)
# must call this after ui set up
if self.safeMode:
tooltip(
_(
"Shift key was held down. Skipping automatic "
"syncing and add-on loading."
)
)
tooltip(tr(TR.QT_MISC_SHIFT_KEY_WAS_HELD_DOWN_SKIPPING))
# were we given a file to import?
if args and args[0] and not self._isAddon(args[0]):
self.onAppMsg(args[0])
@ -319,11 +313,7 @@ class AnkiQt(QMainWindow):
return showWarning(tr(TR.QT_MISC_THERE_MUST_BE_AT_LEAST_ONE))
# sure?
if not askUser(
_(
"""\
All cards, notes, and media for this profile will be deleted. \
Are you sure?"""
),
tr(TR.QT_MISC_ALL_CARDS_NOTES_AND_MEDIA_FOR),
msgfunc=QMessageBox.warning,
defaultno=True,
):
@ -333,10 +323,7 @@ Are you sure?"""
def onOpenBackup(self):
if not askUser(
_(
"""\
Replace your collection with an earlier backup?"""
),
tr(TR.QT_MISC_REPLACE_YOUR_COLLECTION_WITH_AN_EARLIER),
msgfunc=QMessageBox.warning,
defaultno=True,
):
@ -364,13 +351,7 @@ Replace your collection with an earlier backup?"""
self.pendingImport = path
self.restoringBackup = True
showInfo(
_(
"""\
Automatic syncing and backups have been disabled while restoring. To enable them again, \
close the profile or restart Anki."""
)
)
showInfo(tr(TR.QT_MISC_AUTOMATIC_SYNCING_AND_BACKUPS_HAVE_BEEN))
self.onOpenProfile()
@ -568,15 +549,7 @@ close the profile or restart Anki."""
self.col = None
self.progress.finish()
if corrupt:
showWarning(
_(
"Your collection file appears to be corrupt. \
This can happen when the file is copied or moved while Anki is open, or \
when the collection is stored on a network or cloud drive. If problems \
persist after restarting your computer, please open an automatic backup \
from the profile screen."
)
)
showWarning(tr(TR.QT_MISC_YOUR_COLLECTION_FILE_APPEARS_TO_BE))
if not corrupt and not self.restoringBackup:
self.backup()
@ -1218,26 +1191,7 @@ title="%s" %s>%s</button>""" % (
print("clock is off; ignoring")
return
diffText = tr(TR.QT_MISC_SECOND, count=diff)
warn = (
_(
"""\
In order to ensure your collection works correctly when moved between \
devices, Anki requires your computer's internal clock to be set correctly. \
The internal clock can be wrong even if your system is showing the correct \
local time.
Please go to the time settings on your computer and check the following:
- AM/PM
- Clock drift
- Day, month and year
- Timezone
- Daylight savings
Difference to correct time: %s."""
)
% diffText
)
warn = tr(TR.QT_MISC_IN_ORDER_TO_ENSURE_YOUR_COLLECTION, val="%s") % diffText
showWarning(warn)
self.app.closeAllWindows()
@ -1281,13 +1235,7 @@ Difference to correct time: %s."""
self._activeWindowOnPlay: Optional[QWidget] = None
def onOdueInvalid(self):
showWarning(
_(
"""\
Invalid property found on card. Please use Tools>Check Database, \
and if the problem comes up again, please ask on the support site."""
)
)
showWarning(tr(TR.QT_MISC_INVALID_PROPERTY_FOUND_ON_CARD_PLEASE))
def _isVideo(self, tag: AVTag) -> bool:
if isinstance(tag, SoundOrVideoTag):
@ -1336,15 +1284,7 @@ and if the problem comes up again, please ask on the support site."""
progress_shown = self.progress.busy()
if progress_shown:
self.progress.finish()
ret = askUser(
_(
"""\
The requested change will require a full upload of the database when \
you next synchronize your collection. If you have reviews or other changes \
waiting on another device that haven't been synchronized here yet, they \
will be lost. Continue?"""
)
)
ret = askUser(tr(TR.QT_MISC_THE_REQUESTED_CHANGE_WILL_REQUIRE_A))
if progress_shown:
self.progress.start()
return ret
@ -1355,15 +1295,7 @@ will be lost. Continue?"""
True if confirmed or already modified."""
if self.col.schemaChanged():
return True
return askUser(
_(
"""\
The requested change will require a full upload of the database when \
you next synchronize your collection. If you have reviews or other changes \
waiting on another device that haven't been synchronized here yet, they \
will be lost. Continue?"""
)
)
return askUser(tr(TR.QT_MISC_THE_REQUESTED_CHANGE_WILL_REQUIRE_A))
# Advanced features
##########################################################################
@ -1603,10 +1535,7 @@ will be lost. Continue?"""
# we can't raise the main window while in profile dialog, syncing, etc
if buf != "raise":
showInfo(
_(
"""\
Please ensure a profile is open and Anki is not busy, then try again."""
),
tr(TR.QT_MISC_PLEASE_ENSURE_A_PROFILE_IS_OPEN),
parent=None,
)
return None

View file

@ -7,7 +7,6 @@ from typing import Any, List, Optional, Sequence
import aqt.clayout
from anki import stdmodels
from anki.backend_pb2 import NoteTypeNameIDUseCount
from anki.lang import _, ngettext
from anki.models import NoteType
from anki.notes import Note
from anki.rsbackend import pb

View file

@ -183,20 +183,9 @@ class Overview:
def _desc(self, deck):
if deck["dyn"]:
desc = _(
"""\
This is a special deck for studying outside of the normal schedule."""
)
desc += " " + _(
"""\
Cards will be automatically returned to their original decks after you review \
them."""
)
desc += " " + _(
"""\
Deleting this deck from the deck list will return all remaining cards \
to their original deck."""
)
desc = tr(TR.STUDYING_THIS_IS_A_SPECIAL_DECK_FOR)
desc += " " + tr(TR.STUDYING_CARDS_WILL_BE_AUTOMATICALLY_RETURNED_TO)
desc += " " + tr(TR.STUDYING_DELETING_THIS_DECK_FROM_THE_DECK)
else:
desc = deck.get("desc", "")
if not desc:

View file

@ -181,11 +181,7 @@ class Preferences(QDialog):
self.form.syncDeauth.setVisible(False)
self.form.syncUser.setText("")
self.form.syncLabel.setText(
_(
"""\
<b>Synchronization</b><br>
Not currently enabled; click the sync button in the main window to enable."""
)
tr(TR.PREFERENCES_SYNCHRONIZATIONNOT_CURRENTLY_ENABLED_CLICK_THE_SYNC)
)
def onSyncDeauth(self) -> None:

View file

@ -253,11 +253,7 @@ class ProfileManager:
QMessageBox.warning(
None,
tr(TR.PROFILES_PROFILE_CORRUPT),
_(
"""\
Anki could not read your profile data. Window sizes and your sync login \
details have been forgotten."""
),
tr(TR.PROFILES_ANKI_COULD_NOT_READ_YOUR_PROFILE),
)
traceback.print_stack()
print("resetting corrupt profile")
@ -322,14 +318,7 @@ details have been forgotten."""
except Exception as e:
self.db.rollback()
if "WinError 5" in str(e):
showWarning(
_(
"""\
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."""
)
)
showWarning(tr(TR.PROFILES_ANKI_COULD_NOT_RENAME_YOUR_PROFILE))
else:
raise
except:

View file

@ -14,7 +14,6 @@ from PyQt5.QtCore import Qt
from anki import hooks
from anki.cards import Card
from anki.lang import _, ngettext
from anki.utils import stripHTML
from aqt import AnkiQt, gui_hooks
from aqt.qt import *
@ -388,10 +387,7 @@ class Reviewer:
if not self.typeCorrect:
if self.typeCorrect is None:
if clozeIdx:
warn = _(
"""\
Please run Tools>Empty Cards"""
)
warn = tr(TR.STUDYING_PLEASE_RUN_TOOLSEMPTY_CARDS)
else:
warn = tr(TR.STUDYING_TYPE_ANSWER_UNKNOWN_FIELD, val=fld)
return re.sub(self.typeAnsPat, warn, buf)

View file

@ -3,13 +3,10 @@
import glob, re, json, stringcase
files = (
# glob.glob("../../pylib/**/*.py", recursive=True)
# glob.glob("../../pylib/**/*.py", recursive=True) +
glob.glob("../../qt/**/*.py", recursive=True)
# glob.glob("../../qt/**/forms/*.ui", recursive=True)
)
string_re = re.compile(
r'ngettext\(\s*"(.+?)",\s+".+?",\s+(.+?)\s*,?\s*\)\s+%\s+\2', re.DOTALL
)
string_re = re.compile(r'_\(\s*(".*?")\s*\)', re.DOTALL)
map = json.load(open("keys_by_text.json"))
@ -18,67 +15,29 @@ blacklist = {
"Label1",
"After pressing OK, you can choose which tags to include.",
"Filter/Cram",
"Show %s",
"~",
"about:blank",
"%d card imported.",
# need to update manually
"Browse (%(cur)d card shown; %(sel)s)",
# previewer.py needs updating to fix these
"Shortcut key: R",
"Shortcut key: B",
}
from html.entities import name2codepoint
reEnts = re.compile(r"&#?\w+;")
def decode_ents(html):
def fixup(m):
text = m.group(0)
if text[:2] == "&#":
# character reference
try:
if text[:3] == "&#x":
return chr(int(text[3:-1], 16))
else:
return chr(int(text[2:-1]))
except ValueError:
pass
else:
# named entity
try:
text = chr(name2codepoint[text[1:-1]])
except KeyError:
pass
return text # leave as is
return reEnts.sub(fixup, html)
def munge_key(key):
if key == "browsing-note":
return "browsing-note-count"
if key == "card-templates-card":
return "card-templates-card-count"
return key
def repl(m):
print(m.group(0))
text = decode_ents(m.group(1))
# the argument may consistent of multiple strings that need merging together
text = eval("(" + m.group(1) + ")")
print(f"text is `{text}`")
if text in blacklist:
return m.group(0)
(module, key) = map[text]
key = munge_key(key)
screaming = stringcase.constcase(key)
print(screaming)
ret = f"tr(TR.{screaming}, count={m.group(2)})"
print(ret)
return ret
if "%d" in text or "%s" in text:
# replace { $val } with %s for compat with old code
return f'tr(TR.{screaming}, val="%s")'
return f"tr(TR.{screaming})"
for file in files: