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))
}