mirror of
https://github.com/ankitects/anki.git
synced 2025-12-15 07:40:58 -05:00
make template errors translatable
This commit is contained in:
parent
5c8e3df612
commit
33367c8edf
8 changed files with 93 additions and 51 deletions
|
|
@ -81,7 +81,6 @@ message StringError {
|
||||||
|
|
||||||
message TemplateParseError {
|
message TemplateParseError {
|
||||||
string info = 1;
|
string info = 1;
|
||||||
bool q_side = 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message NetworkError {
|
message NetworkError {
|
||||||
|
|
|
||||||
|
|
@ -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":
|
||||||
|
|
|
||||||
|
|
@ -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=[],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
|
|
|
||||||
15
rslib/src/i18n/card-templates.ftl
Normal file
15
rslib/src/i18n/card-templates.ftl
Normal 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
|
||||||
|
|
@ -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| {
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue