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. ## 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-invalid-search = Invalid search: { $reason }
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-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 = 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-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. # 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-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 = 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-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 = 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-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 = 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-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 = 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-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 = 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-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 = 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-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 = Invalid search:<br>Note or card id lists must be comma-separated number series. search-invalid-id-list = note or card id lists must be comma-separated numbers.
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-argument = `{ $term }` was given an invalid argument '`{ $argument }`'.
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-flag = `flag:` must be followed by a valid flag number: `1` (red), `2` (orange), `3` (green), `4` (blue) or `0` (no flag).
search-invalid-added = Invalid search:<br><code>added:</code> must be followed by a positive number of days. search-invalid-followed-by-positive-days = `{ $term }` 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 = `rated:` 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 = `rated:{ $val }:` must be followed by `1` (again), `2` (hard), `3` (good) or `4` (easy).
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-prop-operator = `prop:{ $val }` must be followed by one of the comparison operators: `=`, `!=`, `<`, `>`, `<=` or `>=`.
search-invalid-resched = Invalid search:<br><code>resched:</code> must be followed by a positive number of days. search-invalid-prop-float = `prop:{ $val }` must be followed by a decimal number.
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-prop-integer = `prop:{ $val }` must be followed by a whole number.
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-unsigned = `prop:{ $val }` must be followed by a non-negative whole number.
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-did = `did:` must be followed by a valid deck id.
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-mid = `mid:` must be followed by a note type id.
search-invalid-prop-float = Invalid search:<br><code>prop:{ $val }</code> must be followed by a decimal number. search-invalid-other = please check for typing mistakes.
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.
## Column labels in browse screen ## Column labels in browse screen

View file

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

View file

@ -123,7 +123,8 @@ impl AnkiError {
DBErrorKind::Locked => "Anki already open, or media currently syncing.".into(), DBErrorKind::Locked => "Anki already open, or media currently syncing.".into(),
_ => format!("{:?}", self), _ => format!("{:?}", self),
}, },
AnkiError::SearchError(kind) => match kind { AnkiError::SearchError(kind) => {
let reason = match kind {
SearchErrorKind::MisplacedAnd => i18n.tr(TR::SearchMisplacedAnd), SearchErrorKind::MisplacedAnd => i18n.tr(TR::SearchMisplacedAnd),
SearchErrorKind::MisplacedOr => i18n.tr(TR::SearchMisplacedOr), SearchErrorKind::MisplacedOr => i18n.tr(TR::SearchMisplacedOr),
SearchErrorKind::EmptyGroup => i18n.tr(TR::SearchEmptyGroup), SearchErrorKind::EmptyGroup => i18n.tr(TR::SearchEmptyGroup),
@ -139,18 +140,45 @@ impl AnkiError {
) )
.into(), .into(),
SearchErrorKind::InvalidIdList => i18n.tr(TR::SearchInvalidIdList), SearchErrorKind::InvalidIdList => i18n.tr(TR::SearchInvalidIdList),
SearchErrorKind::InvalidState => i18n.tr(TR::SearchInvalidState), SearchErrorKind::InvalidState(state) => i18n
.trn(
TR::SearchInvalidArgument,
tr_strs!("term" => "is:", "argument" => state),
)
.into(),
SearchErrorKind::InvalidFlag => i18n.tr(TR::SearchInvalidFlag), SearchErrorKind::InvalidFlag => i18n.tr(TR::SearchInvalidFlag),
SearchErrorKind::InvalidAdded => i18n.tr(TR::SearchInvalidAdded), SearchErrorKind::InvalidAdded => i18n
SearchErrorKind::InvalidEdited => i18n.tr(TR::SearchInvalidEdited), .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::InvalidRatedDays => i18n.tr(TR::SearchInvalidRatedDays),
SearchErrorKind::InvalidRatedEase(ctx) => i18n SearchErrorKind::InvalidRatedEase(ctx) => i18n
.trn(TR::SearchInvalidRatedEase, tr_strs!["val"=>(ctx)]) .trn(TR::SearchInvalidRatedEase, tr_strs!["val"=>(ctx)])
.into(), .into(),
SearchErrorKind::InvalidResched => i18n.tr(TR::SearchInvalidResched), SearchErrorKind::InvalidResched => i18n
SearchErrorKind::InvalidDupeMid => i18n.tr(TR::SearchInvalidDupeMid), .trn(
SearchErrorKind::InvalidDupeText => i18n.tr(TR::SearchInvalidDupeText), TR::SearchInvalidFollowedByPositiveDays,
SearchErrorKind::InvalidPropProperty => i18n.tr(TR::SearchInvalidPropProperty), 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 SearchErrorKind::InvalidPropOperator(ctx) => i18n
.trn(TR::SearchInvalidPropOperator, tr_strs!["val"=>(ctx)]) .trn(TR::SearchInvalidPropOperator, tr_strs!["val"=>(ctx)])
.into(), .into(),
@ -176,9 +204,13 @@ impl AnkiError {
SearchErrorKind::InvalidMid => i18n.tr(TR::SearchInvalidMid), SearchErrorKind::InvalidMid => i18n.tr(TR::SearchInvalidMid),
SearchErrorKind::Regex(text) => text.into(), SearchErrorKind::Regex(text) => text.into(),
SearchErrorKind::Other(Some(info)) => info.into(), SearchErrorKind::Other(Some(info)) => info.into(),
SearchErrorKind::Other(None) => i18n.tr(TR::SearchInvalid), SearchErrorKind::Other(None) => i18n.tr(TR::SearchInvalidOther),
};
i18n.trn(
TR::SearchInvalidSearch,
tr_args!("reason" => reason.into_owned()),
)
} }
.into(),
_ => format!("{:?}", self), _ => format!("{:?}", self),
} }
} }
@ -408,7 +440,7 @@ pub enum SearchErrorKind {
MissingKey, MissingKey,
UnknownEscape(String), UnknownEscape(String),
InvalidIdList, InvalidIdList,
InvalidState, InvalidState(String),
InvalidFlag, InvalidFlag,
InvalidAdded, InvalidAdded,
InvalidEdited, InvalidEdited,
@ -417,7 +449,7 @@ pub enum SearchErrorKind {
InvalidDupeMid, InvalidDupeMid,
InvalidDupeText, InvalidDupeText,
InvalidResched, InvalidResched,
InvalidPropProperty, InvalidPropProperty(String),
InvalidPropOperator(String), InvalidPropOperator(String),
InvalidPropFloat(String), InvalidPropFloat(String),
InvalidPropInteger(String), InvalidPropInteger(String),

View file

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