From 33367c8edf3737249654ac356d541d1a0db29971 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 14 Feb 2020 18:10:57 +1000 Subject: [PATCH] make template errors translatable --- proto/backend.proto | 1 - pylib/anki/rsbackend.py | 5 +- pylib/anki/template.py | 12 +---- rslib/src/backend.rs | 5 +- rslib/src/err.rs | 2 +- rslib/src/i18n/card-templates.ftl | 15 ++++++ rslib/src/i18n/mod.rs | 19 ++++--- rslib/src/template.rs | 85 ++++++++++++++++++++++--------- 8 files changed, 93 insertions(+), 51 deletions(-) create mode 100644 rslib/src/i18n/card-templates.ftl diff --git a/proto/backend.proto b/proto/backend.proto index 5e6e151c8..2d958d6cc 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -81,7 +81,6 @@ message StringError { message TemplateParseError { string info = 1; - bool q_side = 2; } message NetworkError { diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 0c9a55200..72360806d 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -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": diff --git a/pylib/anki/template.py b/pylib/anki/template.py index 3ba3afe80..238b0764f 100644 --- a/pylib/anki/template.py +++ b/pylib/anki/template.py @@ -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"
{e}" - errmsg += "
{}".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=[], ) diff --git a/rslib/src/backend.rs b/rslib/src/backend.rs index 4e1c66717..60a7f2b5d 100644 --- a/rslib/src/backend.rs +++ b/rslib/src/backend.rs @@ -43,9 +43,7 @@ impl std::convert::From 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 diff --git a/rslib/src/err.rs b/rslib/src/err.rs index 303e24b98..f63aea920 100644 --- a/rslib/src/err.rs +++ b/rslib/src/err.rs @@ -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 }, diff --git a/rslib/src/i18n/card-templates.ftl b/rslib/src/i18n/card-templates.ftl new file mode 100644 index 000000000..b3695c7db --- /dev/null +++ b/rslib/src/i18n/card-templates.ftl @@ -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 diff --git a/rslib/src/i18n/mod.rs b/rslib/src/i18n/mod.rs index 37bbb9fb1..9e3f9953d 100644 --- a/rslib/src/i18n/mod.rs +++ b/rslib/src/i18n/mod.rs @@ -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 { 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 { 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| { diff --git a/rslib/src/template.rs b/rslib/src/template.rs index 318d26376..9e06f3e33 100644 --- a/rslib/src/template.rs +++ b/rslib/src/template.rs @@ -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 = std::result::Result; +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>>>( } } -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), - TemplateError::ConditionalNotOpen { - closed, - currently_open, - } => { - if let Some(open) = currently_open { - format!("Found {{{{/{}}}}}, but expected {{{{/{}}}}}", closed, open) - } else { - format!( - "Found {{{{/{}}}}}, but missing '{{{{#{}}}}}' or '{{{{^{}}}}}'", - closed, closed, closed - ) - } +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!( + "{}
{}
{}", + 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 { + 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 '{}'", - filters, field, field - ), - }, - q_side, + } + TemplateError::FieldNotFound { field, filters } => cat.trn( + "no-such-field", + tr_strs!( + "found"=>format!("{{{{{}{}}}}}", filters, field), + "field"=>field), + ), } } @@ -454,6 +488,7 @@ pub fn render_card( afmt: &str, field_map: &HashMap<&str, &str>, card_ord: u16, + i18n: &I18n, ) -> Result<(Vec, Vec)> { // 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)) }