search error tweaks

- use markdown instead of HTML, to make editing and translating easier
- use a shared prefix
- a few very minor wording tweaks
- we don't need to translate undocumented command errors
- share a string for positive number of days
- share a string for invalid property and state arguments, and avoid
listing them out

Related discussion: https://github.com/ankitects/anki/pull/922
This commit is contained in:
Damien Elmes 2021-01-16 15:37:40 +10:00
parent 65d3a1393c
commit 9686cd99ec
4 changed files with 140 additions and 103 deletions

View File

@ -1,34 +1,30 @@
## Errors shown when invalid search input is encountered.
## Text wrapped in code tags is literal search input and should generally not to be altered.
## Text wrapped in `backticks` is literal search input, and should generally not to be altered.
search-invalid = Invalid search - please check for typing mistakes.
search-misplaced-and = Invalid search:<br>An <code>and</code> was found but it is not connecting two search terms.<br>If you want to search for the word itself, wrap it in double quotes: <code>"and"</code>.
search-misplaced-or = Invalid search:<br>An <code>or</code> was found but it is not connecting two search terms.<br>If you want to search for the word itself, wrap it in double quotes: <code>"or"</code>.
search-invalid-search = Invalid search: { $reason }
search-misplaced-and = an `and` was found but it is not connecting two search terms. If you want to search for the word itself, wrap it in double quotes: `"and"`.
search-misplaced-or = an `or` was found but it is not connecting two search terms. If you want to search for the word itself, wrap it in double quotes: `"or"`.
# Here, the ellipsis "..." may be localised.
search-empty-group = Invalid search:<br>A group <code>(...)</code> was found but there was nothing between the brackets to search for.<br>If you want to search for literal brackets, wrap them in double quotes: <code>"( )"</code>.
search-unopened-group = Invalid search:<br>A closing bracket <code>)</code> was found, but there was no opening bracket <code>(</code> preceding it.<br>If you want to search for the literal <code>)</code>, wrap it in double quotes or prepend a backslash: <code>")"</code> or <code>\)</code>.
search-unclosed-group = Invalid search:<br>An opening bracket <code>(</code> was found, but there was no closing bracket <code>)</code> following it.<br>If you want to search for the literal <code>(</code>, wrap it in double quotes or prepend a backslash: <code>"("</code> or <code>\(</code> .
search-empty-quote = Invalid search:<br>A pair of double quotes <code>""</code> was found but there was nothing between them to search for.<br>If you want to search for literal double quotes, prepend backslashes: <code>\"\"</code>.
search-unclosed-quote = Invalid search:<br>An opening double quote <code>"</code> was found but there was no second one to close it.<br>If you want to search for the literal <code>"</code>, prepend a backslash: <code>\"</code>.
search-missing-key = Invalid search:<br>A colon <code>:</code> was found but there was no key word preceding it.<br>If you want to search for the literal <code>:</code>, prepend a backslash: <code>\:</code>.
search-unknown-escape = Invalid search:<br>The escape sequence <code>{ $val }</code> is not defined.<br>If you want to search for the literal backslash <code>\</code>, prepend another one: <code>\\</code>.
search-invalid-id-list = Invalid search:<br>Note or card id lists must be comma-separated number series.
search-invalid-state = Invalid search:<br><code>is:</code> must be followed by one of the predefined card states: <code>new</code>, <code>review</code>, <code>learn</code>, <code>due</code>, <code>buried</code>, <code>buried-manually</code>, <code>buried-sibling</code> or <code>suspended</code>.
search-invalid-flag = Invalid search:<br><code>flag:</code> must be followed by a valid flag number: <code>1</code> (red), <code>2</code> (orange), <code>3</code> (green), <code>4</code> (blue) or <code>0</code> (no flag).
search-invalid-added = Invalid search:<br><code>added:</code> must be followed by a positive number of days.
search-invalid-edited = Invalid search:<br><code>edited:</code> must be followed by a positive number of days.
search-invalid-rated-days = Invalid search:<br><code>rated:</code> must be followed by a positive number of days.
search-invalid-rated-ease = Invalid search:<br><code>rated:{ $val }:</code> must be followed by <code>1</code> (again), <code>2</code> (hard), <code>3</code> (good) or <code>4</code> (easy).
search-invalid-resched = Invalid search:<br><code>resched:</code> must be followed by a positive number of days.
search-invalid-dupe-mid = Invalid search:<br><code>dupe:</code> must be followed by a note type id, a comma and then arbitrary text.
search-invalid-dupe-text = Invalid search:<br><code>dupe:</code> must be followed by a note type id, a comma and then arbitrary text.
search-invalid-prop-property = Invalid search:<br><code>prop:</code> must be followed by one of the predefined card properties: <code>ivl</code> (interval), <code>due</code>, <code>reps</code> (repetitions), <code>lapses</code>, <code>ease</code> or <code>pos</code> (position).
search-invalid-prop-operator = Invalid search:<br><code>prop:{ $val }</code> must be followed by one of the comparison operators: <code>=</code>, <code>!=</code>, <code>&lt;</code>, <code>&gt;</code>, <code>&lt;=</code> or <code>&gt;=</code>.
search-invalid-prop-float = Invalid search:<br><code>prop:{ $val }</code> must be followed by a decimal number.
search-invalid-prop-integer = Invalid search:<br><code>prop:{ $val }</code> must be followed by a whole number.
search-invalid-prop-unsigned = Invalid search:<br><code>prop:{ $val }</code> must be followed by a non-negative whole number.
search-invalid-did = Invalid search:<br><code>did:</code> must be followed by a valid deck id.
search-invalid-mid = Invalid search:<br><code>mid:</code> must be followed by a note type deck id.
search-empty-group = a group `(...)` was found, but there was nothing between the brackets to search for. If you want to search for literal brackets, wrap them in double quotes: `"( )"`.
search-unopened-group = a closing bracket `)` was found, but there was no opening bracket `(` preceding it. If you want to search for the literal `)`, wrap it in double quotes or prepend a backslash: `")"` or `\)`.
search-unclosed-group = an opening bracket `(` was found, but there was no closing bracket `)` following it. If you want to search for the literal `(`, wrap it in double quotes or prepend a backslash: `"("` or `\(` .
search-empty-quote = a pair of double quotes `""` was found but there was nothing between them to search for. If you want to search for literal double quotes, prepend backslashes: `\"\"`.
search-unclosed-quote = an opening double quote `"` was found but there was no second one to close it. If you want to search for the literal `"`, prepend a backslash: `\"`.
search-missing-key = a colon `:` was found but there was no keyword preceding it. If you want to search for the literal `:`, prepend a backslash: `\:`.
search-unknown-escape = the escape sequence `{ $val }` is not defined. If you want to search for the literal backslash `\`, prepend another one: `\\`.
search-invalid-id-list = note or card id lists must be comma-separated numbers.
search-invalid-argument = `{ $term }` was given an invalid argument '`{ $argument }`'.
search-invalid-flag = `flag:` must be followed by a valid flag number: `1` (red), `2` (orange), `3` (green), `4` (blue) or `0` (no flag).
search-invalid-followed-by-positive-days = `{ $term }` must be followed by a positive number of days.
search-invalid-rated-days = `rated:` must be followed by a positive number of days.
search-invalid-rated-ease = `rated:{ $val }:` must be followed by `1` (again), `2` (hard), `3` (good) or `4` (easy).
search-invalid-prop-operator = `prop:{ $val }` must be followed by one of the comparison operators: `=`, `!=`, `<`, `>`, `<=` or `>=`.
search-invalid-prop-float = `prop:{ $val }` must be followed by a decimal number.
search-invalid-prop-integer = `prop:{ $val }` must be followed by a whole number.
search-invalid-prop-unsigned = `prop:{ $val }` must be followed by a non-negative whole number.
search-invalid-did = `did:` must be followed by a valid deck id.
search-invalid-mid = `mid:` must be followed by a note type id.
search-invalid-other = please check for typing mistakes.
## Column labels in browse screen

View File

@ -11,6 +11,8 @@ from enum import Enum
from operator import itemgetter
from typing import Callable, List, Optional, Sequence, Tuple, Union, cast
from markdown import markdown
import anki
import aqt
import aqt.forms
@ -88,6 +90,14 @@ class SearchContext:
card_ids: Optional[Sequence[int]] = None
def show_invalid_search_error(err: Exception):
"Render search errors in markdown, then display a warning."
text = str(err)
if isinstance(err, InvalidInput):
text = markdown(text)
showWarning(text)
# Data model
##########################################################################
@ -191,7 +201,7 @@ class DataModel(QAbstractTableModel):
def search(self, txt: str) -> None:
self.beginReset()
self.cards = []
error_message: Optional[str] = None
exception: Optional[Exception] = None
try:
ctx = SearchContext(search=txt, browser=self.browser)
gui_hooks.browser_will_search(ctx)
@ -201,12 +211,12 @@ class DataModel(QAbstractTableModel):
gui_hooks.browser_did_search(ctx)
self.cards = ctx.card_ids
except Exception as e:
error_message = str(e)
exception = e
finally:
self.endReset()
if error_message:
showWarning(error_message)
if exception:
show_invalid_search_error(exception)
def reset(self):
self.beginReset()
@ -1252,7 +1262,7 @@ QTableView {{ gridline-color: {grid} }}
searches=[cur, search],
)
except InvalidInput as e:
showWarning(str(e))
show_invalid_search_error(e)
else:
self.form.searchEdit.lineEdit().setText(search)
self.onSearchActivated()
@ -1446,7 +1456,7 @@ QTableView {{ gridline-color: {grid} }}
self.form.searchEdit.lineEdit().text()
)
except InvalidInput as e:
showWarning(str(e))
show_invalid_search_error(e)
else:
name = getOnlyText(tr(TR.BROWSING_PLEASE_GIVE_YOUR_FILTER_A_NAME))
if not name:
@ -2001,8 +2011,7 @@ where id in %s"""
try:
changed = fut.result()
except InvalidInput as e:
# failed regex
showWarning(str(e))
show_invalid_search_error(e)
return
showInfo(

View File

@ -123,62 +123,94 @@ impl AnkiError {
DBErrorKind::Locked => "Anki already open, or media currently syncing.".into(),
_ => format!("{:?}", self),
},
AnkiError::SearchError(kind) => match kind {
SearchErrorKind::MisplacedAnd => i18n.tr(TR::SearchMisplacedAnd),
SearchErrorKind::MisplacedOr => i18n.tr(TR::SearchMisplacedOr),
SearchErrorKind::EmptyGroup => i18n.tr(TR::SearchEmptyGroup),
SearchErrorKind::UnopenedGroup => i18n.tr(TR::SearchUnopenedGroup),
SearchErrorKind::UnclosedGroup => i18n.tr(TR::SearchUnclosedGroup),
SearchErrorKind::EmptyQuote => i18n.tr(TR::SearchEmptyQuote),
SearchErrorKind::UnclosedQuote => i18n.tr(TR::SearchUnclosedQuote),
SearchErrorKind::MissingKey => i18n.tr(TR::SearchMissingKey),
SearchErrorKind::UnknownEscape(ctx) => i18n
.trn(
TR::SearchUnknownEscape,
tr_strs!["val"=>(htmlescape::encode_minimal(ctx))],
)
.into(),
SearchErrorKind::InvalidIdList => i18n.tr(TR::SearchInvalidIdList),
SearchErrorKind::InvalidState => i18n.tr(TR::SearchInvalidState),
SearchErrorKind::InvalidFlag => i18n.tr(TR::SearchInvalidFlag),
SearchErrorKind::InvalidAdded => i18n.tr(TR::SearchInvalidAdded),
SearchErrorKind::InvalidEdited => i18n.tr(TR::SearchInvalidEdited),
SearchErrorKind::InvalidRatedDays => i18n.tr(TR::SearchInvalidRatedDays),
SearchErrorKind::InvalidRatedEase(ctx) => i18n
.trn(TR::SearchInvalidRatedEase, tr_strs!["val"=>(ctx)])
.into(),
SearchErrorKind::InvalidResched => i18n.tr(TR::SearchInvalidResched),
SearchErrorKind::InvalidDupeMid => i18n.tr(TR::SearchInvalidDupeMid),
SearchErrorKind::InvalidDupeText => i18n.tr(TR::SearchInvalidDupeText),
SearchErrorKind::InvalidPropProperty => i18n.tr(TR::SearchInvalidPropProperty),
SearchErrorKind::InvalidPropOperator(ctx) => i18n
.trn(TR::SearchInvalidPropOperator, tr_strs!["val"=>(ctx)])
.into(),
SearchErrorKind::InvalidPropFloat(ctx) => i18n
.trn(
TR::SearchInvalidPropFloat,
tr_strs!["val"=>(htmlescape::encode_minimal(ctx))],
)
.into(),
SearchErrorKind::InvalidPropInteger(ctx) => i18n
.trn(
TR::SearchInvalidPropInteger,
tr_strs!["val"=>(htmlescape::encode_minimal(ctx))],
)
.into(),
SearchErrorKind::InvalidPropUnsigned(ctx) => i18n
.trn(
TR::SearchInvalidPropUnsigned,
tr_strs!["val"=>(htmlescape::encode_minimal(ctx))],
)
.into(),
SearchErrorKind::InvalidDid => i18n.tr(TR::SearchInvalidDid),
SearchErrorKind::InvalidMid => i18n.tr(TR::SearchInvalidMid),
SearchErrorKind::Regex(text) => text.into(),
SearchErrorKind::Other(Some(info)) => info.into(),
SearchErrorKind::Other(None) => i18n.tr(TR::SearchInvalid),
AnkiError::SearchError(kind) => {
let reason = match kind {
SearchErrorKind::MisplacedAnd => i18n.tr(TR::SearchMisplacedAnd),
SearchErrorKind::MisplacedOr => i18n.tr(TR::SearchMisplacedOr),
SearchErrorKind::EmptyGroup => i18n.tr(TR::SearchEmptyGroup),
SearchErrorKind::UnopenedGroup => i18n.tr(TR::SearchUnopenedGroup),
SearchErrorKind::UnclosedGroup => i18n.tr(TR::SearchUnclosedGroup),
SearchErrorKind::EmptyQuote => i18n.tr(TR::SearchEmptyQuote),
SearchErrorKind::UnclosedQuote => i18n.tr(TR::SearchUnclosedQuote),
SearchErrorKind::MissingKey => i18n.tr(TR::SearchMissingKey),
SearchErrorKind::UnknownEscape(ctx) => i18n
.trn(
TR::SearchUnknownEscape,
tr_strs!["val"=>(htmlescape::encode_minimal(ctx))],
)
.into(),
SearchErrorKind::InvalidIdList => i18n.tr(TR::SearchInvalidIdList),
SearchErrorKind::InvalidState(state) => i18n
.trn(
TR::SearchInvalidArgument,
tr_strs!("term" => "is:", "argument" => state),
)
.into(),
SearchErrorKind::InvalidFlag => i18n.tr(TR::SearchInvalidFlag),
SearchErrorKind::InvalidAdded => i18n
.trn(
TR::SearchInvalidFollowedByPositiveDays,
tr_strs!("term" => "added:"),
)
.into(),
SearchErrorKind::InvalidEdited => i18n
.trn(
TR::SearchInvalidFollowedByPositiveDays,
tr_strs!("term" => "edited:"),
)
.into(),
SearchErrorKind::InvalidRatedDays => i18n.tr(TR::SearchInvalidRatedDays),
SearchErrorKind::InvalidRatedEase(ctx) => i18n
.trn(TR::SearchInvalidRatedEase, tr_strs!["val"=>(ctx)])
.into(),
SearchErrorKind::InvalidResched => i18n
.trn(
TR::SearchInvalidFollowedByPositiveDays,
tr_strs!("term" => "resched:"),
)
.into(),
SearchErrorKind::InvalidDupeMid | SearchErrorKind::InvalidDupeText => {
// this is an undocumented search keyword, so no translation
"`dupe:` arguments were invalid".into()
}
SearchErrorKind::InvalidPropProperty(prop) => i18n
.trn(
TR::SearchInvalidArgument,
tr_strs!("term" => "prop:", "argument" => prop),
)
.into(),
SearchErrorKind::InvalidPropOperator(ctx) => i18n
.trn(TR::SearchInvalidPropOperator, tr_strs!["val"=>(ctx)])
.into(),
SearchErrorKind::InvalidPropFloat(ctx) => i18n
.trn(
TR::SearchInvalidPropFloat,
tr_strs!["val"=>(htmlescape::encode_minimal(ctx))],
)
.into(),
SearchErrorKind::InvalidPropInteger(ctx) => i18n
.trn(
TR::SearchInvalidPropInteger,
tr_strs!["val"=>(htmlescape::encode_minimal(ctx))],
)
.into(),
SearchErrorKind::InvalidPropUnsigned(ctx) => i18n
.trn(
TR::SearchInvalidPropUnsigned,
tr_strs!["val"=>(htmlescape::encode_minimal(ctx))],
)
.into(),
SearchErrorKind::InvalidDid => i18n.tr(TR::SearchInvalidDid),
SearchErrorKind::InvalidMid => i18n.tr(TR::SearchInvalidMid),
SearchErrorKind::Regex(text) => text.into(),
SearchErrorKind::Other(Some(info)) => info.into(),
SearchErrorKind::Other(None) => i18n.tr(TR::SearchInvalidOther),
};
i18n.trn(
TR::SearchInvalidSearch,
tr_args!("reason" => reason.into_owned()),
)
}
.into(),
_ => format!("{:?}", self),
}
}
@ -408,7 +440,7 @@ pub enum SearchErrorKind {
MissingKey,
UnknownEscape(String),
InvalidIdList,
InvalidState,
InvalidState(String),
InvalidFlag,
InvalidAdded,
InvalidEdited,
@ -417,7 +449,7 @@ pub enum SearchErrorKind {
InvalidDupeMid,
InvalidDupeText,
InvalidResched,
InvalidPropProperty,
InvalidPropProperty(String),
InvalidPropOperator(String),
InvalidPropFloat(String),
InvalidPropInteger(String),

View File

@ -370,7 +370,7 @@ fn parse_prop(s: &str) -> ParseResult<SearchNode> {
tag("ease"),
tag("pos"),
))(s)
.map_err(|_| parse_failure(s, FailKind::InvalidPropProperty))?;
.map_err(|_| parse_failure(s, FailKind::InvalidPropProperty(s.into())))?;
let (num, operator) = alt::<_, _, ParseError, _>((
tag("<="),
@ -482,7 +482,7 @@ fn parse_state(s: &str) -> ParseResult<SearchNode> {
"buried-manually" => UserBuried,
"buried-sibling" => SchedBuried,
"suspended" => Suspended,
_ => return Err(parse_failure(s, FailKind::InvalidState)),
_ => return Err(parse_failure(s, FailKind::InvalidState(s.into()))),
}))
}
@ -845,11 +845,11 @@ mod test {
assert_err_kind("cid:,2,3", InvalidIdList);
assert_err_kind("cid:1,2,", InvalidIdList);
assert_err_kind("is:foo", InvalidState);
assert_err_kind("is:DUE", InvalidState);
assert_err_kind("is:New", InvalidState);
assert_err_kind("is:", InvalidState);
assert_err_kind(r#""is:learn ""#, InvalidState);
assert_err_kind("is:foo", InvalidState("foo".into()));
assert_err_kind("is:DUE", InvalidState("DUE".into()));
assert_err_kind("is:New", InvalidState("New".into()));
assert_err_kind("is:", InvalidState("".into()));
assert_err_kind(r#""is:learn ""#, InvalidState("learn ".into()));
assert_err_kind(r#""flag: ""#, InvalidFlag);
assert_err_kind("flag:-0", InvalidFlag);
@ -888,9 +888,9 @@ mod test {
assert_err_kind("dupe:123", InvalidDupeText);
assert_err_kind("prop:", InvalidPropProperty);
assert_err_kind("prop:=1", InvalidPropProperty);
assert_err_kind("prop:DUE<5", InvalidPropProperty);
assert_err_kind("prop:", InvalidPropProperty("".into()));
assert_err_kind("prop:=1", InvalidPropProperty("=1".into()));
assert_err_kind("prop:DUE<5", InvalidPropProperty("DUE<5".into()));
assert_err_kind("prop:lapses", InvalidPropOperator("lapses".to_string()));
assert_err_kind("prop:pos~1", InvalidPropOperator("pos".to_string()));