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 { message TemplateParseError {
string info = 1; string info = 1;
bool q_side = 2;
} }
message NetworkError { message NetworkError {

View file

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

View file

@ -121,17 +121,9 @@ def render_card(
try: try:
output = render_card_from_context(ctx) output = render_card_from_context(ctx)
except anki.rsbackend.TemplateError as e: 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( output = TemplateRenderOutput(
question_text=errmsg, question_text=str(e),
answer_text=errmsg, answer_text=str(e),
question_av_tags=[], question_av_tags=[],
answer_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; use pb::backend_error::Value as V;
let value = match err { let value = match err {
AnkiError::InvalidInput { info } => V::InvalidInput(pb::StringError { info }), AnkiError::InvalidInput { info } => V::InvalidInput(pb::StringError { info }),
AnkiError::TemplateError { info, q_side } => { AnkiError::TemplateError { info } => V::TemplateParse(pb::TemplateParseError { info }),
V::TemplateParse(pb::TemplateParseError { info, q_side })
}
AnkiError::IOError { info } => V::IoError(pb::StringError { info }), AnkiError::IOError { info } => V::IoError(pb::StringError { info }),
AnkiError::DBError { info } => V::DbError(pb::StringError { info }), AnkiError::DBError { info } => V::DbError(pb::StringError { info }),
AnkiError::NetworkError { info, kind } => V::NetworkError(pb::NetworkError { AnkiError::NetworkError { info, kind } => V::NetworkError(pb::NetworkError {
@ -280,6 +278,7 @@ impl Backend {
&input.answer_template, &input.answer_template,
&fields, &fields,
input.card_ordinal as u16, input.card_ordinal as u16,
&self.i18n,
)?; )?;
// return // return

View file

@ -13,7 +13,7 @@ pub enum AnkiError {
InvalidInput { info: String }, InvalidInput { info: String },
#[fail(display = "invalid card template: {}", info)] #[fail(display = "invalid card template: {}", info)]
TemplateError { info: String, q_side: bool }, TemplateError { info: String },
#[fail(display = "I/O error: {}", info)] #[fail(display = "I/O error: {}", info)]
IOError { info: String }, 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, ChineseTaiwan,
} }
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum TranslationFile {
Test,
MediaCheck,
CardTemplates,
}
fn lang_dialect(lang: LanguageIdentifier) -> Option<LanguageDialect> { fn lang_dialect(lang: LanguageIdentifier) -> Option<LanguageDialect> {
use LanguageDialect as L; use LanguageDialect as L;
Some(match lang.get_language() { 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 { fn data_for_fallback(file: TranslationFile) -> String {
match file { match file {
TranslationFile::MediaCheck => include_str!("media-check.ftl"),
TranslationFile::Test => include_str!("../../tests/support/test.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() .to_string()
} }
@ -74,8 +76,9 @@ fn data_for_lang_and_file(
locales: &Path, locales: &Path,
) -> Option<String> { ) -> Option<String> {
let path = locales.join(dialect_file_locale(dialect)).join(match file { let path = locales.join(dialect_file_locale(dialect)).join(match file {
TranslationFile::MediaCheck => "media-check.ftl",
TranslationFile::Test => "test.ftl", TranslationFile::Test => "test.ftl",
TranslationFile::MediaCheck => "media-check.ftl",
TranslationFile::CardTemplates => "card-templates.ftl",
}); });
fs::read_to_string(&path) fs::read_to_string(&path)
.map_err(|e| { .map_err(|e| {

View file

@ -2,6 +2,7 @@
// 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
use crate::err::{AnkiError, Result, TemplateError}; use crate::err::{AnkiError, Result, TemplateError};
use crate::i18n::{tr_strs, I18n, I18nCategory, TranslationFile};
use crate::template_filters::apply_filters; use crate::template_filters::apply_filters;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use nom; use nom;
@ -17,6 +18,9 @@ use std::iter;
pub type FieldMap<'a> = HashMap<&'a str, u16>; pub type FieldMap<'a> = HashMap<&'a str, u16>;
type TemplateResult<T> = std::result::Result<T, TemplateError>; 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 // 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 { fn template_error_to_anki_error(err: TemplateError, q_side: bool, i18n: &I18n) -> AnkiError {
AnkiError::TemplateError { let cat = i18n.get(TranslationFile::CardTemplates);
info: match err { let header = cat.tr(if q_side {
TemplateError::NoClosingBrackets(context) => format!("Missing '}}}}' in '{}'", context), "front-side-problem"
TemplateError::ConditionalNotClosed(tag) => format!("Missing '{{{{/{}}}}}'", tag), } else {
TemplateError::ConditionalNotOpen { "back-side-problem"
closed, });
currently_open, let details = localized_template_error(&cat, err);
} => { let more_info = cat.tr("more-info");
if let Some(open) = currently_open { let info = format!(
format!("Found {{{{/{}}}}}, but expected {{{{/{}}}}}", closed, open) "{}<br>{}<br><a href='{}'>{}</a>",
} else { header, details, TEMPLATE_ERROR_LINK, more_info
format!( );
"Found {{{{/{}}}}}, but missing '{{{{#{}}}}}' or '{{{{^{}}}}}'",
closed, closed, closed 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 {
cat.trn(
"wrong-conditional-closed",
tr_strs!(
"found"=>format!("{{{{/{}}}}}", closed),
"expected"=>format!("{{{{/{}}}}}", open)),
)
} else {
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 '{}'", TemplateError::FieldNotFound { field, filters } => cat.trn(
filters, field, field "no-such-field",
), tr_strs!(
}, "found"=>format!("{{{{{}{}}}}}", filters, field),
q_side, "field"=>field),
),
} }
} }
@ -454,6 +488,7 @@ pub fn render_card(
afmt: &str, afmt: &str,
field_map: &HashMap<&str, &str>, field_map: &HashMap<&str, &str>,
card_ord: u16, card_ord: u16,
i18n: &I18n,
) -> Result<(Vec<RenderedNode>, Vec<RenderedNode>)> { ) -> Result<(Vec<RenderedNode>, Vec<RenderedNode>)> {
// prepare context // prepare context
let mut context = RenderContext { let mut context = RenderContext {
@ -467,14 +502,14 @@ pub fn render_card(
let qnorm = without_legacy_template_directives(qfmt); let qnorm = without_legacy_template_directives(qfmt);
let qnodes = ParsedTemplate::from_text(qnorm.as_ref()) let qnodes = ParsedTemplate::from_text(qnorm.as_ref())
.and_then(|tmpl| tmpl.render(&context)) .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 // answer side
context.question_side = false; context.question_side = false;
let anorm = without_legacy_template_directives(afmt); let anorm = without_legacy_template_directives(afmt);
let anodes = ParsedTemplate::from_text(anorm.as_ref()) let anodes = ParsedTemplate::from_text(anorm.as_ref())
.and_then(|tmpl| tmpl.render(&context)) .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)) Ok((qnodes, anodes))
} }