make template errors translatable

This commit is contained in:
Damien Elmes 2020-02-14 18:10:57 +10:00
parent 5c8e3df612
commit 33367c8edf
8 changed files with 93 additions and 51 deletions

View file

@ -81,7 +81,6 @@ message StringError {
message TemplateParseError {
string info = 1;
bool q_side = 2;
}
message NetworkError {

View file

@ -46,8 +46,7 @@ class DBError(StringError):
class TemplateError(StringError):
def q_side(self) -> bool:
return self.args[1]
pass
SyncErrorKind = pb.SyncError.SyncErrorKind
@ -70,7 +69,7 @@ def proto_exception_to_native(err: pb.BackendError) -> Exception:
elif val == "db_error":
return DBError(err.db_error.info)
elif val == "template_parse":
return TemplateError(err.template_parse.info, err.template_parse.q_side)
return TemplateError(err.template_parse.info)
elif val == "invalid_input":
return StringError(err.invalid_input.info)
elif val == "sync_error":

View file

@ -121,17 +121,9 @@ def render_card(
try:
output = render_card_from_context(ctx)
except anki.rsbackend.TemplateError as e:
if e.q_side():
side = _("Front")
else:
side = _("Back")
errmsg = _("{} template has a problem:").format(side) + f"<br>{e}"
errmsg += "<br><a href=https://anki.tenderapp.com/kb/problems/card-template-has-a-problem>{}</a>".format(
_("More info")
)
output = TemplateRenderOutput(
question_text=errmsg,
answer_text=errmsg,
question_text=str(e),
answer_text=str(e),
question_av_tags=[],
answer_av_tags=[],
)

View file

@ -43,9 +43,7 @@ impl std::convert::From<AnkiError> for pb::BackendError {
use pb::backend_error::Value as V;
let value = match err {
AnkiError::InvalidInput { info } => V::InvalidInput(pb::StringError { info }),
AnkiError::TemplateError { info, q_side } => {
V::TemplateParse(pb::TemplateParseError { info, q_side })
}
AnkiError::TemplateError { info } => V::TemplateParse(pb::TemplateParseError { info }),
AnkiError::IOError { info } => V::IoError(pb::StringError { info }),
AnkiError::DBError { info } => V::DbError(pb::StringError { info }),
AnkiError::NetworkError { info, kind } => V::NetworkError(pb::NetworkError {
@ -280,6 +278,7 @@ impl Backend {
&input.answer_template,
&fields,
input.card_ordinal as u16,
&self.i18n,
)?;
// return

View file

@ -13,7 +13,7 @@ pub enum AnkiError {
InvalidInput { info: String },
#[fail(display = "invalid card template: {}", info)]
TemplateError { info: String, q_side: bool },
TemplateError { info: String },
#[fail(display = "I/O error: {}", info)]
IOError { info: String },

View file

@ -0,0 +1,15 @@
front-side-problem = Front template has a problem:
back-side-problem = Back template has a problem:
no-closing-brackets =
Missing '{$missing}' in '{$tag}'
conditional-not-closed =
Missing '{$missing}'
wrong-conditional-closed =
Found '{$found}', but expected '{$expected}'
conditional-not-open =
Found '{$found}', but missing '{$missing1}' or '{$missing2}'
no-such-field =
Found '{$found}', but there is no field called '{$field}'
more-info = More information

View file

@ -34,6 +34,13 @@ pub enum LanguageDialect {
ChineseTaiwan,
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum TranslationFile {
Test,
MediaCheck,
CardTemplates,
}
fn lang_dialect(lang: LanguageIdentifier) -> Option<LanguageDialect> {
use LanguageDialect as L;
Some(match lang.get_language() {
@ -54,16 +61,11 @@ fn dialect_file_locale(dialect: LanguageDialect) -> &'static str {
}
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum TranslationFile {
Test,
MediaCheck,
}
fn data_for_fallback(file: TranslationFile) -> String {
match file {
TranslationFile::MediaCheck => include_str!("media-check.ftl"),
TranslationFile::Test => include_str!("../../tests/support/test.ftl"),
TranslationFile::MediaCheck => include_str!("media-check.ftl"),
TranslationFile::CardTemplates => include_str!("card-templates.ftl"),
}
.to_string()
}
@ -74,8 +76,9 @@ fn data_for_lang_and_file(
locales: &Path,
) -> Option<String> {
let path = locales.join(dialect_file_locale(dialect)).join(match file {
TranslationFile::MediaCheck => "media-check.ftl",
TranslationFile::Test => "test.ftl",
TranslationFile::MediaCheck => "media-check.ftl",
TranslationFile::CardTemplates => "card-templates.ftl",
});
fs::read_to_string(&path)
.map_err(|e| {

View file

@ -2,6 +2,7 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::err::{AnkiError, Result, TemplateError};
use crate::i18n::{tr_strs, I18n, I18nCategory, TranslationFile};
use crate::template_filters::apply_filters;
use lazy_static::lazy_static;
use nom;
@ -17,6 +18,9 @@ use std::iter;
pub type FieldMap<'a> = HashMap<&'a str, u16>;
type TemplateResult<T> = std::result::Result<T, TemplateError>;
static TEMPLATE_ERROR_LINK: &str =
"https://anki.tenderapp.com/kb/problems/card-template-has-a-problem";
// Lexing
//----------------------------------------
@ -189,30 +193,60 @@ fn parse_inner<'a, I: Iterator<Item = TemplateResult<Token<'a>>>>(
}
}
fn template_error_to_anki_error(err: TemplateError, q_side: bool) -> AnkiError {
AnkiError::TemplateError {
info: match err {
TemplateError::NoClosingBrackets(context) => format!("Missing '}}}}' in '{}'", context),
TemplateError::ConditionalNotClosed(tag) => format!("Missing '{{{{/{}}}}}'", tag),
fn template_error_to_anki_error(err: TemplateError, q_side: bool, i18n: &I18n) -> AnkiError {
let cat = i18n.get(TranslationFile::CardTemplates);
let header = cat.tr(if q_side {
"front-side-problem"
} else {
"back-side-problem"
});
let details = localized_template_error(&cat, err);
let more_info = cat.tr("more-info");
let info = format!(
"{}<br>{}<br><a href='{}'>{}</a>",
header, details, TEMPLATE_ERROR_LINK, more_info
);
AnkiError::TemplateError { info }
}
fn localized_template_error(cat: &I18nCategory, err: TemplateError) -> String {
match err {
TemplateError::NoClosingBrackets(tag) => {
cat.trn("no-closing-brackets", tr_strs!("tag"=>tag, "missing"=>"}}"))
}
TemplateError::ConditionalNotClosed(tag) => cat.trn(
"conditional-not-closed",
tr_strs!("missing"=>format!("{{{{/{}}}}}", tag)),
),
TemplateError::ConditionalNotOpen {
closed,
currently_open,
} => {
if let Some(open) = currently_open {
format!("Found {{{{/{}}}}}, but expected {{{{/{}}}}}", closed, open)
cat.trn(
"wrong-conditional-closed",
tr_strs!(
"found"=>format!("{{{{/{}}}}}", closed),
"expected"=>format!("{{{{/{}}}}}", open)),
)
} else {
format!(
"Found {{{{/{}}}}}, but missing '{{{{#{}}}}}' or '{{{{^{}}}}}'",
closed, closed, closed
cat.trn(
"conditional-not-open",
tr_strs!(
"found"=>format!("{{{{/{}}}}}", closed),
"missing1"=>format!("{{{{#{}}}}}", closed),
"missing2"=>format!("{{{{^{}}}}}", closed)
),
)
}
}
TemplateError::FieldNotFound { field, filters } => format!(
"Found '{{{{{}{}}}}}', but there is no field called '{}'",
filters, field, field
TemplateError::FieldNotFound { field, filters } => cat.trn(
"no-such-field",
tr_strs!(
"found"=>format!("{{{{{}{}}}}}", filters, field),
"field"=>field),
),
},
q_side,
}
}
@ -454,6 +488,7 @@ pub fn render_card(
afmt: &str,
field_map: &HashMap<&str, &str>,
card_ord: u16,
i18n: &I18n,
) -> Result<(Vec<RenderedNode>, Vec<RenderedNode>)> {
// prepare context
let mut context = RenderContext {
@ -467,14 +502,14 @@ pub fn render_card(
let qnorm = without_legacy_template_directives(qfmt);
let qnodes = ParsedTemplate::from_text(qnorm.as_ref())
.and_then(|tmpl| tmpl.render(&context))
.map_err(|e| template_error_to_anki_error(e, true))?;
.map_err(|e| template_error_to_anki_error(e, true, i18n))?;
// answer side
context.question_side = false;
let anorm = without_legacy_template_directives(afmt);
let anodes = ParsedTemplate::from_text(anorm.as_ref())
.and_then(|tmpl| tmpl.render(&context))
.map_err(|e| template_error_to_anki_error(e, false))?;
.map_err(|e| template_error_to_anki_error(e, false, i18n))?;
Ok((qnodes, anodes))
}