i18n->tr in rslib/ to match Python/TS code

This commit is contained in:
Damien Elmes 2021-03-27 12:09:51 +10:00
parent d6b9cc4d9b
commit 3433c02242
32 changed files with 310 additions and 310 deletions

View File

@ -426,50 +426,50 @@ mod test {
#[test] #[test]
fn i18n() { fn i18n() {
// English template // English template
let i18n = I18n::new(&["zz"]); let tr = I18n::new(&["zz"]);
assert_eq!(i18n.translate("valid-key", None), "a valid key"); assert_eq!(tr.translate("valid-key", None), "a valid key");
assert_eq!(i18n.translate("invalid-key", None), "invalid-key"); assert_eq!(tr.translate("invalid-key", None), "invalid-key");
assert_eq!( assert_eq!(
i18n.translate("two-args-key", Some(tr_args!["one"=>1.1, "two"=>"2"])), tr.translate("two-args-key", Some(tr_args!["one"=>1.1, "two"=>"2"])),
"two args: 1.1 and 2" "two args: 1.1 and 2"
); );
assert_eq!( assert_eq!(
i18n.translate("plural", Some(tr_args!["hats"=>1.0])), tr.translate("plural", Some(tr_args!["hats"=>1.0])),
"You have 1 hat." "You have 1 hat."
); );
assert_eq!( assert_eq!(
i18n.translate("plural", Some(tr_args!["hats"=>1.1])), tr.translate("plural", Some(tr_args!["hats"=>1.1])),
"You have 1.1 hats." "You have 1.1 hats."
); );
assert_eq!( assert_eq!(
i18n.translate("plural", Some(tr_args!["hats"=>3])), tr.translate("plural", Some(tr_args!["hats"=>3])),
"You have 3 hats." "You have 3 hats."
); );
// Another language // Another language
let i18n = I18n::new(&["ja_JP"]); let tr = I18n::new(&["ja_JP"]);
assert_eq!(i18n.translate("valid-key", None), "キー"); assert_eq!(tr.translate("valid-key", None), "キー");
assert_eq!(i18n.translate("only-in-english", None), "not translated"); assert_eq!(tr.translate("only-in-english", None), "not translated");
assert_eq!(i18n.translate("invalid-key", None), "invalid-key"); assert_eq!(tr.translate("invalid-key", None), "invalid-key");
assert_eq!( assert_eq!(
i18n.translate("two-args-key", Some(tr_args!["one"=>1, "two"=>"2"])), tr.translate("two-args-key", Some(tr_args!["one"=>1, "two"=>"2"])),
"1と2" "1と2"
); );
// Decimal separator // Decimal separator
let i18n = I18n::new(&["pl-PL"]); let tr = I18n::new(&["pl-PL"]);
// Polish will use a comma if the string is translated // Polish will use a comma if the string is translated
assert_eq!( assert_eq!(
i18n.translate("one-arg-key", Some(tr_args!["one"=>2.07])), tr.translate("one-arg-key", Some(tr_args!["one"=>2.07])),
"fake Polish 2,07" "fake Polish 2,07"
); );
// but if it falls back on English, it will use an English separator // but if it falls back on English, it will use an English separator
assert_eq!( assert_eq!(
i18n.translate("two-args-key", Some(tr_args!["one"=>1, "two"=>2.07])), tr.translate("two-args-key", Some(tr_args!["one"=>1, "two"=>2.07])),
"two args: 1 and 2.07" "two args: 1 and 2.07"
); );
} }

View File

@ -15,7 +15,7 @@ use slog::error;
impl CollectionService for Backend { impl CollectionService for Backend {
fn latest_progress(&self, _input: pb::Empty) -> Result<pb::Progress> { fn latest_progress(&self, _input: pb::Empty) -> Result<pb::Progress> {
let progress = self.progress_state.lock().unwrap().last_progress; let progress = self.progress_state.lock().unwrap().last_progress;
Ok(progress_to_proto(progress, &self.i18n)) Ok(progress_to_proto(progress, &self.tr))
} }
fn set_wants_abort(&self, _input: pb::Empty) -> Result<pb::Empty> { fn set_wants_abort(&self, _input: pb::Empty) -> Result<pb::Empty> {
@ -43,7 +43,7 @@ impl CollectionService for Backend {
input.media_folder_path, input.media_folder_path,
input.media_db_path, input.media_db_path,
self.server, self.server,
self.i18n.clone(), self.tr.clone(),
logger, logger,
)?; )?;
@ -79,26 +79,26 @@ impl CollectionService for Backend {
self.with_col(|col| { self.with_col(|col| {
col.check_database(progress_fn) col.check_database(progress_fn)
.map(|problems| pb::CheckDatabaseOut { .map(|problems| pb::CheckDatabaseOut {
problems: problems.to_i18n_strings(&col.i18n), problems: problems.to_i18n_strings(&col.tr),
}) })
}) })
} }
fn get_undo_status(&self, _input: pb::Empty) -> Result<pb::UndoStatus> { fn get_undo_status(&self, _input: pb::Empty) -> Result<pb::UndoStatus> {
self.with_col(|col| Ok(col.undo_status().into_protobuf(&col.i18n))) self.with_col(|col| Ok(col.undo_status().into_protobuf(&col.tr)))
} }
fn undo(&self, _input: pb::Empty) -> Result<pb::UndoStatus> { fn undo(&self, _input: pb::Empty) -> Result<pb::UndoStatus> {
self.with_col(|col| { self.with_col(|col| {
col.undo()?; col.undo()?;
Ok(col.undo_status().into_protobuf(&col.i18n)) Ok(col.undo_status().into_protobuf(&col.tr))
}) })
} }
fn redo(&self, _input: pb::Empty) -> Result<pb::UndoStatus> { fn redo(&self, _input: pb::Empty) -> Result<pb::UndoStatus> {
self.with_col(|col| { self.with_col(|col| {
col.redo()?; col.redo()?;
Ok(col.undo_status().into_protobuf(&col.i18n)) Ok(col.undo_status().into_protobuf(&col.tr))
}) })
} }
} }

View File

@ -8,9 +8,9 @@ use crate::{
}; };
/// Convert an Anki error to a protobuf error. /// Convert an Anki error to a protobuf error.
pub(super) fn anki_error_to_proto_error(err: AnkiError, i18n: &I18n) -> pb::BackendError { pub(super) fn anki_error_to_proto_error(err: AnkiError, tr: &I18n) -> pb::BackendError {
use pb::backend_error::Value as V; use pb::backend_error::Value as V;
let localized = err.localized_description(i18n); let localized = err.localized_description(tr);
let value = match err { let value = match err {
AnkiError::InvalidInput { .. } => V::InvalidInput(pb::Empty {}), AnkiError::InvalidInput { .. } => V::InvalidInput(pb::Empty {}),
AnkiError::TemplateError { .. } => V::TemplateParse(pb::Empty {}), AnkiError::TemplateError { .. } => V::TemplateParse(pb::Empty {}),

View File

@ -19,7 +19,7 @@ impl I18nService for Backend {
.collect(); .collect();
Ok(self Ok(self
.i18n .tr
.translate_via_index( .translate_via_index(
input.module_index as usize, input.module_index as usize,
input.message_index as usize, input.message_index as usize,
@ -31,15 +31,15 @@ impl I18nService for Backend {
fn format_timespan(&self, input: pb::FormatTimespanIn) -> Result<pb::String> { fn format_timespan(&self, input: pb::FormatTimespanIn) -> Result<pb::String> {
use pb::format_timespan_in::Context; use pb::format_timespan_in::Context;
Ok(match input.context() { Ok(match input.context() {
Context::Precise => time_span(input.seconds, &self.i18n, true), Context::Precise => time_span(input.seconds, &self.tr, true),
Context::Intervals => time_span(input.seconds, &self.i18n, false), Context::Intervals => time_span(input.seconds, &self.tr, false),
Context::AnswerButtons => answer_button_time(input.seconds, &self.i18n), Context::AnswerButtons => answer_button_time(input.seconds, &self.tr),
} }
.into()) .into())
} }
fn i18n_resources(&self, input: pb::I18nResourcesIn) -> Result<pb::Json> { fn i18n_resources(&self, input: pb::I18nResourcesIn) -> Result<pb::Json> {
serde_json::to_vec(&self.i18n.resources_for_js(&input.modules)) serde_json::to_vec(&self.tr.resources_for_js(&input.modules))
.map(Into::into) .map(Into::into)
.map_err(Into::into) .map_err(Into::into)
} }

View File

@ -62,7 +62,7 @@ use self::err::anki_error_to_proto_error;
pub struct Backend { pub struct Backend {
col: Arc<Mutex<Option<Collection>>>, col: Arc<Mutex<Option<Collection>>>,
i18n: I18n, tr: I18n,
server: bool, server: bool,
sync_abort: AbortHandleSlot, sync_abort: AbortHandleSlot,
progress_state: Arc<Mutex<ProgressState>>, progress_state: Arc<Mutex<ProgressState>>,
@ -81,16 +81,16 @@ pub fn init_backend(init_msg: &[u8]) -> std::result::Result<Backend, String> {
Err(_) => return Err("couldn't decode init request".into()), Err(_) => return Err("couldn't decode init request".into()),
}; };
let i18n = I18n::new(&input.preferred_langs); let tr = I18n::new(&input.preferred_langs);
Ok(Backend::new(i18n, input.server)) Ok(Backend::new(tr, input.server))
} }
impl Backend { impl Backend {
pub fn new(i18n: I18n, server: bool) -> Backend { pub fn new(tr: I18n, server: bool) -> Backend {
Backend { Backend {
col: Arc::new(Mutex::new(None)), col: Arc::new(Mutex::new(None)),
i18n, tr,
server, server,
sync_abort: Arc::new(Mutex::new(None)), sync_abort: Arc::new(Mutex::new(None)),
progress_state: Arc::new(Mutex::new(ProgressState { progress_state: Arc::new(Mutex::new(ProgressState {
@ -103,7 +103,7 @@ impl Backend {
} }
pub fn i18n(&self) -> &I18n { pub fn i18n(&self) -> &I18n {
&self.i18n &self.tr
} }
pub fn run_method( pub fn run_method(
@ -134,7 +134,7 @@ impl Backend {
pb::ServiceIndex::Cards => CardsService::run_method(self, method, input), pb::ServiceIndex::Cards => CardsService::run_method(self, method, input),
}) })
.map_err(|err| { .map_err(|err| {
let backend_err = anki_error_to_proto_error(err, &self.i18n); let backend_err = anki_error_to_proto_error(err, &self.tr);
let mut bytes = Vec::new(); let mut bytes = Vec::new();
backend_err.encode(&mut bytes).unwrap(); backend_err.encode(&mut bytes).unwrap();
bytes bytes
@ -143,7 +143,7 @@ impl Backend {
pub fn run_db_command_bytes(&self, input: &[u8]) -> std::result::Result<Vec<u8>, Vec<u8>> { pub fn run_db_command_bytes(&self, input: &[u8]) -> std::result::Result<Vec<u8>, Vec<u8>> {
self.db_command(input).map_err(|err| { self.db_command(input).map_err(|err| {
let backend_err = anki_error_to_proto_error(err, &self.i18n); let backend_err = anki_error_to_proto_error(err, &self.tr);
let mut bytes = Vec::new(); let mut bytes = Vec::new();
backend_err.encode(&mut bytes).unwrap(); backend_err.encode(&mut bytes).unwrap();
bytes bytes

View File

@ -25,7 +25,7 @@ impl NoteTypesService for Backend {
fn get_stock_notetype_legacy(&self, input: pb::StockNoteType) -> Result<pb::Json> { fn get_stock_notetype_legacy(&self, input: pb::StockNoteType) -> Result<pb::Json> {
// fixme: use individual functions instead of full vec // fixme: use individual functions instead of full vec
let mut all = all_stock_notetypes(&self.i18n); let mut all = all_stock_notetypes(&self.tr);
let idx = (input.kind as usize).min(all.len() - 1); let idx = (input.kind as usize).min(all.len() - 1);
let nt = all.swap_remove(idx); let nt = all.swap_remove(idx);
let schema11: NoteTypeSchema11 = nt.into(); let schema11: NoteTypeSchema11 = nt.into();

View File

@ -31,10 +31,10 @@ impl From<OpChanges> for pb::OpChanges {
} }
impl UndoStatus { impl UndoStatus {
pub(crate) fn into_protobuf(self, i18n: &I18n) -> pb::UndoStatus { pub(crate) fn into_protobuf(self, tr: &I18n) -> pb::UndoStatus {
pb::UndoStatus { pb::UndoStatus {
undo: self.undo.map(|op| op.describe(i18n)).unwrap_or_default(), undo: self.undo.map(|op| op.describe(tr)).unwrap_or_default(),
redo: self.redo.map(|op| op.describe(i18n)).unwrap_or_default(), redo: self.redo.map(|op| op.describe(tr)).unwrap_or_default(),
} }
} }
} }

View File

@ -52,12 +52,12 @@ pub(super) enum Progress {
DatabaseCheck(DatabaseCheckProgress), DatabaseCheck(DatabaseCheckProgress),
} }
pub(super) fn progress_to_proto(progress: Option<Progress>, i18n: &I18n) -> pb::Progress { pub(super) fn progress_to_proto(progress: Option<Progress>, tr: &I18n) -> pb::Progress {
let progress = if let Some(progress) = progress { let progress = if let Some(progress) = progress {
match progress { match progress {
Progress::MediaSync(p) => pb::progress::Value::MediaSync(media_sync_progress(p, i18n)), Progress::MediaSync(p) => pb::progress::Value::MediaSync(media_sync_progress(p, tr)),
Progress::MediaCheck(n) => { Progress::MediaCheck(n) => {
pb::progress::Value::MediaCheck(i18n.media_check_checked(n).into()) pb::progress::Value::MediaCheck(tr.media_check_checked(n).into())
} }
Progress::FullSync(p) => pb::progress::Value::FullSync(pb::progress::FullSync { Progress::FullSync(p) => pb::progress::Value::FullSync(pb::progress::FullSync {
transferred: p.transferred_bytes as u32, transferred: p.transferred_bytes as u32,
@ -65,15 +65,15 @@ pub(super) fn progress_to_proto(progress: Option<Progress>, i18n: &I18n) -> pb::
}), }),
Progress::NormalSync(p) => { Progress::NormalSync(p) => {
let stage = match p.stage { let stage = match p.stage {
SyncStage::Connecting => i18n.sync_syncing(), SyncStage::Connecting => tr.sync_syncing(),
SyncStage::Syncing => i18n.sync_syncing(), SyncStage::Syncing => tr.sync_syncing(),
SyncStage::Finalizing => i18n.sync_checking(), SyncStage::Finalizing => tr.sync_checking(),
} }
.to_string(); .to_string();
let added = i18n let added = tr
.sync_added_updated_count(p.local_update, p.remote_update) .sync_added_updated_count(p.local_update, p.remote_update)
.into(); .into();
let removed = i18n let removed = tr
.sync_media_removed_count(p.local_remove, p.remote_remove) .sync_media_removed_count(p.local_remove, p.remote_remove)
.into(); .into();
pb::progress::Value::NormalSync(pb::progress::NormalSync { pb::progress::Value::NormalSync(pb::progress::NormalSync {
@ -86,15 +86,15 @@ pub(super) fn progress_to_proto(progress: Option<Progress>, i18n: &I18n) -> pb::
let mut stage_total = 0; let mut stage_total = 0;
let mut stage_current = 0; let mut stage_current = 0;
let stage = match p { let stage = match p {
DatabaseCheckProgress::Integrity => i18n.database_check_checking_integrity(), DatabaseCheckProgress::Integrity => tr.database_check_checking_integrity(),
DatabaseCheckProgress::Optimize => i18n.database_check_rebuilding(), DatabaseCheckProgress::Optimize => tr.database_check_rebuilding(),
DatabaseCheckProgress::Cards => i18n.database_check_checking_cards(), DatabaseCheckProgress::Cards => tr.database_check_checking_cards(),
DatabaseCheckProgress::Notes { current, total } => { DatabaseCheckProgress::Notes { current, total } => {
stage_total = total; stage_total = total;
stage_current = current; stage_current = current;
i18n.database_check_checking_notes() tr.database_check_checking_notes()
} }
DatabaseCheckProgress::History => i18n.database_check_checking_history(), DatabaseCheckProgress::History => tr.database_check_checking_history(),
} }
.to_string(); .to_string();
pb::progress::Value::DatabaseCheck(pb::progress::DatabaseCheck { pb::progress::Value::DatabaseCheck(pb::progress::DatabaseCheck {
@ -112,13 +112,13 @@ pub(super) fn progress_to_proto(progress: Option<Progress>, i18n: &I18n) -> pb::
} }
} }
fn media_sync_progress(p: MediaSyncProgress, i18n: &I18n) -> pb::progress::MediaSync { fn media_sync_progress(p: MediaSyncProgress, tr: &I18n) -> pb::progress::MediaSync {
pb::progress::MediaSync { pb::progress::MediaSync {
checked: i18n.sync_media_checked_count(p.checked).into(), checked: tr.sync_media_checked_count(p.checked).into(),
added: i18n added: tr
.sync_media_added_count(p.uploaded_files, p.downloaded_files) .sync_media_added_count(p.uploaded_files, p.downloaded_files)
.into(), .into(),
removed: i18n removed: tr
.sync_media_removed_count(p.uploaded_deletions, p.downloaded_deletions) .sync_media_removed_count(p.uploaded_deletions, p.downloaded_deletions)
.into(), .into(),
} }

View File

@ -34,7 +34,7 @@ impl SchedulingService for Backend {
/// Message rendering only, for old graphs. /// Message rendering only, for old graphs.
fn studied_today_message(&self, input: pb::StudiedTodayMessageIn) -> Result<pb::String> { fn studied_today_message(&self, input: pb::StudiedTodayMessageIn) -> Result<pb::String> {
Ok(studied_today(input.cards, input.seconds as f32, &self.i18n).into()) Ok(studied_today(input.cards, input.seconds as f32, &self.tr).into())
} }
fn update_stats(&self, input: pb::UpdateStatsIn) -> Result<pb::Empty> { fn update_stats(&self, input: pb::UpdateStatsIn) -> Result<pb::Empty> {

View File

@ -356,7 +356,7 @@ impl Backend {
media_folder_path, media_folder_path,
media_db_path, media_db_path,
self.server, self.server,
self.i18n.clone(), self.tr.clone(),
logger, logger,
)?); )?);

View File

@ -58,7 +58,7 @@ struct RowContext<'a> {
notetype: Arc<NoteType>, notetype: Arc<NoteType>,
deck: Option<Deck>, deck: Option<Deck>,
original_deck: Option<Option<Deck>>, original_deck: Option<Option<Deck>>,
i18n: &'a I18n, tr: &'a I18n,
timing: SchedTimingToday, timing: SchedTimingToday,
render_context: Option<RenderContext>, render_context: Option<RenderContext>,
} }
@ -152,7 +152,7 @@ impl<'a> RowContext<'a> {
notetype, notetype,
deck: None, deck: None,
original_deck: None, original_deck: None,
i18n: &col.i18n, tr: &col.tr,
timing, timing,
render_context, render_context,
}) })
@ -236,9 +236,9 @@ impl<'a> RowContext<'a> {
fn card_due_str(&mut self) -> String { fn card_due_str(&mut self) -> String {
let due = if self.card.original_deck_id != DeckID(0) { let due = if self.card.original_deck_id != DeckID(0) {
self.i18n.browsing_filtered() self.tr.browsing_filtered()
} else if self.card.queue == CardQueue::New || self.card.ctype == CardType::New { } else if self.card.queue == CardQueue::New || self.card.ctype == CardType::New {
self.i18n.statistics_due_for_new_card(self.card.due) self.tr.statistics_due_for_new_card(self.card.due)
} else { } else {
let date = if self.card.queue == CardQueue::Learn { let date = if self.card.queue == CardQueue::Learn {
TimestampSecs(self.card.due as i64) TimestampSecs(self.card.due as i64)
@ -262,16 +262,16 @@ impl<'a> RowContext<'a> {
fn card_ease_str(&self) -> String { fn card_ease_str(&self) -> String {
match self.card.ctype { match self.card.ctype {
CardType::New => self.i18n.browsing_new().into(), CardType::New => self.tr.browsing_new().into(),
_ => format!("{}%", self.card.ease_factor / 10), _ => format!("{}%", self.card.ease_factor / 10),
} }
} }
fn card_interval_str(&self) -> String { fn card_interval_str(&self) -> String {
match self.card.ctype { match self.card.ctype {
CardType::New => self.i18n.browsing_new().into(), CardType::New => self.tr.browsing_new().into(),
CardType::Learn => self.i18n.browsing_learning().into(), CardType::Learn => self.tr.browsing_learning().into(),
_ => time_span((self.card.interval * 86400) as f32, self.i18n, false), _ => time_span((self.card.interval * 86400) as f32, self.tr, false),
} }
} }

View File

@ -19,18 +19,18 @@ pub fn open_collection<P: Into<PathBuf>>(
media_folder: P, media_folder: P,
media_db: P, media_db: P,
server: bool, server: bool,
i18n: I18n, tr: I18n,
log: Logger, log: Logger,
) -> Result<Collection> { ) -> Result<Collection> {
let col_path = path.into(); let col_path = path.into();
let storage = SqliteStorage::open_or_create(&col_path, &i18n, server)?; let storage = SqliteStorage::open_or_create(&col_path, &tr, server)?;
let col = Collection { let col = Collection {
storage, storage,
col_path, col_path,
media_folder: media_folder.into(), media_folder: media_folder.into(),
media_db: media_db.into(), media_db: media_db.into(),
i18n, tr,
log, log,
server, server,
state: CollectionState::default(), state: CollectionState::default(),
@ -55,8 +55,8 @@ pub fn open_test_collection() -> Collection {
#[cfg(test)] #[cfg(test)]
pub fn open_test_collection_with_server(server: bool) -> Collection { pub fn open_test_collection_with_server(server: bool) -> Collection {
use crate::log; use crate::log;
let i18n = I18n::template_only(); let tr = I18n::template_only();
open_collection(":memory:", "", "", server, i18n, log::terminal()).unwrap() open_collection(":memory:", "", "", server, tr, log::terminal()).unwrap()
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
@ -76,7 +76,7 @@ pub struct Collection {
pub(crate) col_path: PathBuf, pub(crate) col_path: PathBuf,
pub(crate) media_folder: PathBuf, pub(crate) media_folder: PathBuf,
pub(crate) media_db: PathBuf, pub(crate) media_db: PathBuf,
pub(crate) i18n: I18n, pub(crate) tr: I18n,
pub(crate) log: Logger, pub(crate) log: Logger,
pub(crate) server: bool, pub(crate) server: bool,
pub(crate) state: CollectionState, pub(crate) state: CollectionState,

View File

@ -41,39 +41,39 @@ pub(crate) enum DatabaseCheckProgress {
} }
impl CheckDatabaseOutput { impl CheckDatabaseOutput {
pub fn to_i18n_strings(&self, i18n: &I18n) -> Vec<String> { pub fn to_i18n_strings(&self, tr: &I18n) -> Vec<String> {
let mut probs = Vec::new(); let mut probs = Vec::new();
if self.notetypes_recovered > 0 { if self.notetypes_recovered > 0 {
probs.push(i18n.database_check_notetypes_recovered()); probs.push(tr.database_check_notetypes_recovered());
} }
if self.card_position_too_high > 0 { if self.card_position_too_high > 0 {
probs.push(i18n.database_check_new_card_high_due(self.card_position_too_high)); probs.push(tr.database_check_new_card_high_due(self.card_position_too_high));
} }
if self.card_properties_invalid > 0 { if self.card_properties_invalid > 0 {
probs.push(i18n.database_check_card_properties(self.card_properties_invalid)); probs.push(tr.database_check_card_properties(self.card_properties_invalid));
} }
if self.cards_missing_note > 0 { if self.cards_missing_note > 0 {
probs.push(i18n.database_check_card_missing_note(self.cards_missing_note)); probs.push(tr.database_check_card_missing_note(self.cards_missing_note));
} }
if self.decks_missing > 0 { if self.decks_missing > 0 {
probs.push(i18n.database_check_missing_decks(self.decks_missing)); probs.push(tr.database_check_missing_decks(self.decks_missing));
} }
if self.field_count_mismatch > 0 { if self.field_count_mismatch > 0 {
probs.push(i18n.database_check_field_count(self.field_count_mismatch)); probs.push(tr.database_check_field_count(self.field_count_mismatch));
} }
if self.card_ords_duplicated > 0 { if self.card_ords_duplicated > 0 {
probs.push(i18n.database_check_duplicate_card_ords(self.card_ords_duplicated)); probs.push(tr.database_check_duplicate_card_ords(self.card_ords_duplicated));
} }
if self.templates_missing > 0 { if self.templates_missing > 0 {
probs.push(i18n.database_check_missing_templates(self.templates_missing)); probs.push(tr.database_check_missing_templates(self.templates_missing));
} }
if self.revlog_properties_invalid > 0 { if self.revlog_properties_invalid > 0 {
probs.push(i18n.database_check_revlog_properties(self.revlog_properties_invalid)); probs.push(tr.database_check_revlog_properties(self.revlog_properties_invalid));
} }
if self.invalid_utf8 > 0 { if self.invalid_utf8 > 0 {
probs.push(i18n.database_check_notes_with_invalid_utf8(self.invalid_utf8)); probs.push(tr.database_check_notes_with_invalid_utf8(self.invalid_utf8));
} }
probs.into_iter().map(Into::into).collect() probs.into_iter().map(Into::into).collect()
@ -91,7 +91,7 @@ impl Collection {
if self.storage.quick_check_corrupt() { if self.storage.quick_check_corrupt() {
debug!(self.log, "quick check failed"); debug!(self.log, "quick check failed");
return Err(AnkiError::DBError { return Err(AnkiError::DBError {
info: self.i18n.database_check_corrupt().into(), info: self.tr.database_check_corrupt().into(),
kind: DBErrorKind::Corrupt, kind: DBErrorKind::Corrupt,
}); });
} }
@ -266,7 +266,7 @@ impl Collection {
// if the collection is empty and the user has deleted all note types, ensure at least // if the collection is empty and the user has deleted all note types, ensure at least
// one note type exists // one note type exists
if self.storage.get_all_notetype_names()?.is_empty() { if self.storage.get_all_notetype_names()?.is_empty() {
let mut nt = all_stock_notetypes(&self.i18n).remove(0); let mut nt = all_stock_notetypes(&self.tr).remove(0);
self.add_notetype_inner(&mut nt, usn)?; self.add_notetype_inner(&mut nt, usn)?;
} }
@ -349,7 +349,7 @@ impl Collection {
let extra_cards_required = self let extra_cards_required = self
.storage .storage
.highest_card_ordinal_for_notetype(previous_id)?; .highest_card_ordinal_for_notetype(previous_id)?;
let mut basic = all_stock_notetypes(&self.i18n).remove(0); let mut basic = all_stock_notetypes(&self.tr).remove(0);
let mut field = 3; let mut field = 3;
while basic.fields.len() < field_count { while basic.fields.len() < field_count {
basic.add_field(format!("{}", field)); basic.add_field(format!("{}", field));

View File

@ -512,7 +512,7 @@ impl Collection {
if deck.id.0 == 1 { if deck.id.0 == 1 {
// if deleting the default deck, ensure there's a new one, and avoid the grave // if deleting the default deck, ensure there's a new one, and avoid the grave
let mut deck = deck.to_owned(); let mut deck = deck.to_owned();
deck.name = self.i18n.deck_config_default_name().into(); deck.name = self.tr.deck_config_default_name().into();
deck.set_modified(usn); deck.set_modified(usn);
self.add_or_update_single_deck_with_existing_id(&mut deck, usn)?; self.add_or_update_single_deck_with_existing_id(&mut deck, usn)?;
} else { } else {

View File

@ -90,37 +90,37 @@ impl AnkiError {
} }
} }
pub fn localized_description(&self, i18n: &I18n) -> String { pub fn localized_description(&self, tr: &I18n) -> String {
match self { match self {
AnkiError::SyncError { info, kind } => match kind { AnkiError::SyncError { info, kind } => match kind {
SyncErrorKind::ServerMessage => info.into(), SyncErrorKind::ServerMessage => info.into(),
SyncErrorKind::Other => info.into(), SyncErrorKind::Other => info.into(),
SyncErrorKind::Conflict => i18n.sync_conflict(), SyncErrorKind::Conflict => tr.sync_conflict(),
SyncErrorKind::ServerError => i18n.sync_server_error(), SyncErrorKind::ServerError => tr.sync_server_error(),
SyncErrorKind::ClientTooOld => i18n.sync_client_too_old(), SyncErrorKind::ClientTooOld => tr.sync_client_too_old(),
SyncErrorKind::AuthFailed => i18n.sync_wrong_pass(), SyncErrorKind::AuthFailed => tr.sync_wrong_pass(),
SyncErrorKind::ResyncRequired => i18n.sync_resync_required(), SyncErrorKind::ResyncRequired => tr.sync_resync_required(),
SyncErrorKind::ClockIncorrect => i18n.sync_clock_off(), SyncErrorKind::ClockIncorrect => tr.sync_clock_off(),
SyncErrorKind::DatabaseCheckRequired => i18n.sync_sanity_check_failed(), SyncErrorKind::DatabaseCheckRequired => tr.sync_sanity_check_failed(),
// server message // server message
SyncErrorKind::SyncNotStarted => "sync not started".into(), SyncErrorKind::SyncNotStarted => "sync not started".into(),
} }
.into(), .into(),
AnkiError::NetworkError { kind, info } => { AnkiError::NetworkError { kind, info } => {
let summary = match kind { let summary = match kind {
NetworkErrorKind::Offline => i18n.network_offline(), NetworkErrorKind::Offline => tr.network_offline(),
NetworkErrorKind::Timeout => i18n.network_timeout(), NetworkErrorKind::Timeout => tr.network_timeout(),
NetworkErrorKind::ProxyAuth => i18n.network_proxy_auth(), NetworkErrorKind::ProxyAuth => tr.network_proxy_auth(),
NetworkErrorKind::Other => i18n.network_other(), NetworkErrorKind::Other => tr.network_other(),
}; };
let details = i18n.network_details(info.as_str()); let details = tr.network_details(info.as_str());
format!("{}\n\n{}", summary, details) format!("{}\n\n{}", summary, details)
} }
AnkiError::TemplateError { info } => { AnkiError::TemplateError { info } => {
// already localized // already localized
info.into() info.into()
} }
AnkiError::TemplateSaveError { ordinal } => i18n AnkiError::TemplateSaveError { ordinal } => tr
.card_templates_invalid_template_number(ordinal + 1) .card_templates_invalid_template_number(ordinal + 1)
.into(), .into(),
AnkiError::DBError { info, kind } => match kind { AnkiError::DBError { info, kind } => match kind {
@ -130,75 +130,75 @@ impl AnkiError {
}, },
AnkiError::SearchError(kind) => { AnkiError::SearchError(kind) => {
let reason = match kind { let reason = match kind {
SearchErrorKind::MisplacedAnd => i18n.search_misplaced_and(), SearchErrorKind::MisplacedAnd => tr.search_misplaced_and(),
SearchErrorKind::MisplacedOr => i18n.search_misplaced_or(), SearchErrorKind::MisplacedOr => tr.search_misplaced_or(),
SearchErrorKind::EmptyGroup => i18n.search_empty_group(), SearchErrorKind::EmptyGroup => tr.search_empty_group(),
SearchErrorKind::UnopenedGroup => i18n.search_unopened_group(), SearchErrorKind::UnopenedGroup => tr.search_unopened_group(),
SearchErrorKind::UnclosedGroup => i18n.search_unclosed_group(), SearchErrorKind::UnclosedGroup => tr.search_unclosed_group(),
SearchErrorKind::EmptyQuote => i18n.search_empty_quote(), SearchErrorKind::EmptyQuote => tr.search_empty_quote(),
SearchErrorKind::UnclosedQuote => i18n.search_unclosed_quote(), SearchErrorKind::UnclosedQuote => tr.search_unclosed_quote(),
SearchErrorKind::MissingKey => i18n.search_missing_key(), SearchErrorKind::MissingKey => tr.search_missing_key(),
SearchErrorKind::UnknownEscape(ctx) => { SearchErrorKind::UnknownEscape(ctx) => {
i18n.search_unknown_escape(ctx.replace('`', "'")) tr.search_unknown_escape(ctx.replace('`', "'"))
} }
SearchErrorKind::InvalidState(state) => { SearchErrorKind::InvalidState(state) => {
i18n.search_invalid_argument("is:", state.replace('`', "'")) tr.search_invalid_argument("is:", state.replace('`', "'"))
} }
SearchErrorKind::InvalidFlag => i18n.search_invalid_flag(), SearchErrorKind::InvalidFlag => tr.search_invalid_flag(),
SearchErrorKind::InvalidPropProperty(prop) => { SearchErrorKind::InvalidPropProperty(prop) => {
i18n.search_invalid_argument("prop:", prop.replace('`', "'")) tr.search_invalid_argument("prop:", prop.replace('`', "'"))
} }
SearchErrorKind::InvalidPropOperator(ctx) => { SearchErrorKind::InvalidPropOperator(ctx) => {
i18n.search_invalid_prop_operator(ctx.as_str()) tr.search_invalid_prop_operator(ctx.as_str())
} }
SearchErrorKind::Regex(text) => { SearchErrorKind::Regex(text) => {
format!("<pre>`{}`</pre>", text.replace('`', "'")).into() format!("<pre>`{}`</pre>", text.replace('`', "'")).into()
} }
SearchErrorKind::Other(Some(info)) => info.into(), SearchErrorKind::Other(Some(info)) => info.into(),
SearchErrorKind::Other(None) => i18n.search_invalid_other(), SearchErrorKind::Other(None) => tr.search_invalid_other(),
SearchErrorKind::InvalidNumber { provided, context } => i18n SearchErrorKind::InvalidNumber { provided, context } => tr
.search_invalid_number( .search_invalid_number(
context.replace('`', "'"), context.replace('`', "'"),
provided.replace('`', "'"), provided.replace('`', "'"),
), ),
SearchErrorKind::InvalidWholeNumber { provided, context } => i18n SearchErrorKind::InvalidWholeNumber { provided, context } => tr
.search_invalid_whole_number( .search_invalid_whole_number(
context.replace('`', "'"), context.replace('`', "'"),
provided.replace('`', "'"), provided.replace('`', "'"),
), ),
SearchErrorKind::InvalidPositiveWholeNumber { provided, context } => i18n SearchErrorKind::InvalidPositiveWholeNumber { provided, context } => tr
.search_invalid_positive_whole_number( .search_invalid_positive_whole_number(
context.replace('`', "'"), context.replace('`', "'"),
provided.replace('`', "'"), provided.replace('`', "'"),
), ),
SearchErrorKind::InvalidNegativeWholeNumber { provided, context } => i18n SearchErrorKind::InvalidNegativeWholeNumber { provided, context } => tr
.search_invalid_negative_whole_number( .search_invalid_negative_whole_number(
context.replace('`', "'"), context.replace('`', "'"),
provided.replace('`', "'"), provided.replace('`', "'"),
), ),
SearchErrorKind::InvalidAnswerButton { provided, context } => i18n SearchErrorKind::InvalidAnswerButton { provided, context } => tr
.search_invalid_answer_button( .search_invalid_answer_button(
context.replace('`', "'"), context.replace('`', "'"),
provided.replace('`', "'"), provided.replace('`', "'"),
), ),
}; };
i18n.search_invalid_search(reason).into() tr.search_invalid_search(reason).into()
} }
AnkiError::InvalidInput { info } => { AnkiError::InvalidInput { info } => {
if info.is_empty() { if info.is_empty() {
i18n.errors_invalid_input_empty().into() tr.errors_invalid_input_empty().into()
} else { } else {
i18n.errors_invalid_input_details(info.as_str()).into() tr.errors_invalid_input_details(info.as_str()).into()
} }
} }
AnkiError::ParseNumError => i18n.errors_parse_number_fail().into(), AnkiError::ParseNumError => tr.errors_parse_number_fail().into(),
AnkiError::DeckIsFiltered => i18n.errors_filtered_parent_deck().into(), AnkiError::DeckIsFiltered => tr.errors_filtered_parent_deck().into(),
AnkiError::FilteredDeckEmpty => i18n.decks_filtered_deck_search_empty().into(), AnkiError::FilteredDeckEmpty => tr.decks_filtered_deck_search_empty().into(),
_ => format!("{:?}", self), _ => format!("{:?}", self),
} }
} }

View File

@ -89,7 +89,7 @@ where
pub fn summarize_output(&self, output: &mut MediaCheckOutput) -> String { pub fn summarize_output(&self, output: &mut MediaCheckOutput) -> String {
let mut buf = String::new(); let mut buf = String::new();
let i = &self.ctx.i18n; let i = &self.ctx.tr;
// top summary area // top summary area
if output.trash_count > 0 { if output.trash_count > 0 {
@ -532,9 +532,9 @@ pub(crate) mod test {
let mgr = MediaManager::new(&media_dir, media_db.clone())?; let mgr = MediaManager::new(&media_dir, media_db.clone())?;
let log = log::terminal(); let log = log::terminal();
let i18n = I18n::template_only(); let tr = I18n::template_only();
let col = open_collection(col_path, media_dir, media_db, false, i18n, log)?; let col = open_collection(col_path, media_dir, media_db, false, tr, log)?;
Ok((dir, mgr, col)) Ok((dir, mgr, col))
} }

View File

@ -73,7 +73,7 @@ impl Collection {
write!( write!(
buf, buf,
"<div><b>{}</b></div><ol>", "<div><b>{}</b></div><ol>",
self.i18n.empty_cards_for_note_type(nt.name.clone()) self.tr.empty_cards_for_note_type(nt.name.clone())
) )
.unwrap(); .unwrap();
@ -95,7 +95,7 @@ impl Collection {
// "Cloze 1, 3" // "Cloze 1, 3"
NoteTypeKind::Cloze => format!( NoteTypeKind::Cloze => format!(
"{} {}", "{} {}",
self.i18n.notetypes_cloze_name(), self.tr.notetypes_cloze_name(),
note.empty note.empty
.iter() .iter()
.map(|(ord, _)| (ord + 1).to_string()) .map(|(ord, _)| (ord + 1).to_string())
@ -113,7 +113,7 @@ impl Collection {
"<li class={}>[anki:nid:{}] {}</li>", "<li class={}>[anki:nid:{}] {}</li>",
class, class,
note.nid, note.nid,
self.i18n.empty_cards_count_line( self.tr.empty_cards_count_line(
note.empty.len(), note.empty.len(),
note.current_count, note.current_count,
templates templates

View File

@ -491,7 +491,7 @@ impl Collection {
col.storage.remove_notetype(ntid)?; col.storage.remove_notetype(ntid)?;
let all = col.storage.get_all_notetype_names()?; let all = col.storage.get_all_notetype_names()?;
if all.is_empty() { if all.is_empty() {
let mut nt = all_stock_notetypes(&col.i18n).remove(0); let mut nt = all_stock_notetypes(&col.tr).remove(0);
col.add_notetype_inner(&mut nt, col.usn()?)?; col.add_notetype_inner(&mut nt, col.usn()?)?;
col.set_current_notetype_id(nt.id) col.set_current_notetype_id(nt.id)
} else { } else {

View File

@ -56,7 +56,7 @@ impl Collection {
.ok_or_else(|| AnkiError::invalid_input("no such notetype"))?; .ok_or_else(|| AnkiError::invalid_input("no such notetype"))?;
if fill_empty { if fill_empty {
fill_empty_fields(note, &template.config.q_format, &nt, &self.i18n); fill_empty_fields(note, &template.config.q_format, &nt, &self.tr);
} }
self.render_card(note, &card, &nt, template, false) self.render_card(note, &card, &nt, template, false)
@ -116,7 +116,7 @@ impl Collection {
&field_map, &field_map,
card.template_idx, card.template_idx,
nt.is_cloze(), nt.is_cloze(),
&self.i18n, &self.tr,
)?; )?;
Ok(RenderCardOutput { qnodes, anodes }) Ok(RenderCardOutput { qnodes, anodes })
} }
@ -165,14 +165,14 @@ fn flag_name(n: u8) -> &'static str {
} }
} }
fn fill_empty_fields(note: &mut Note, qfmt: &str, nt: &NoteType, i18n: &I18n) { fn fill_empty_fields(note: &mut Note, qfmt: &str, nt: &NoteType, tr: &I18n) {
if let Ok(tmpl) = ParsedTemplate::from_text(qfmt) { if let Ok(tmpl) = ParsedTemplate::from_text(qfmt) {
let cloze_fields = tmpl.cloze_fields(); let cloze_fields = tmpl.cloze_fields();
for (val, field) in note.fields_mut().iter_mut().zip(nt.fields.iter()) { for (val, field) in note.fields_mut().iter_mut().zip(nt.fields.iter()) {
if field_is_empty(val) { if field_is_empty(val) {
if cloze_fields.contains(&field.name.as_str()) { if cloze_fields.contains(&field.name.as_str()) {
*val = i18n.card_templates_sample_cloze().into(); *val = tr.card_templates_sample_cloze().into();
} else { } else {
*val = format!("({})", field.name); *val = format!("({})", field.name);
} }

View File

@ -14,8 +14,8 @@ use crate::{
use crate::backend_proto::stock_note_type::Kind; use crate::backend_proto::stock_note_type::Kind;
impl SqliteStorage { impl SqliteStorage {
pub(crate) fn add_stock_notetypes(&self, i18n: &I18n) -> Result<()> { pub(crate) fn add_stock_notetypes(&self, tr: &I18n) -> Result<()> {
for (idx, mut nt) in all_stock_notetypes(i18n).into_iter().enumerate() { for (idx, mut nt) in all_stock_notetypes(tr).into_iter().enumerate() {
self.add_new_notetype(&mut nt)?; self.add_new_notetype(&mut nt)?;
if idx == Kind::Basic as usize { if idx == Kind::Basic as usize {
self.set_config_entry(&ConfigEntry::boxed( self.set_config_entry(&ConfigEntry::boxed(
@ -31,13 +31,13 @@ impl SqliteStorage {
} }
// if changing this, make sure to update StockNoteType enum // if changing this, make sure to update StockNoteType enum
pub fn all_stock_notetypes(i18n: &I18n) -> Vec<NoteType> { pub fn all_stock_notetypes(tr: &I18n) -> Vec<NoteType> {
vec![ vec![
basic(i18n), basic(tr),
basic_forward_reverse(i18n), basic_forward_reverse(tr),
basic_optional_reverse(i18n), basic_optional_reverse(tr),
basic_typing(i18n), basic_typing(tr),
cloze(i18n), cloze(tr),
] ]
} }
@ -46,17 +46,17 @@ fn fieldref<S: AsRef<str>>(name: S) -> String {
format!("{{{{{}}}}}", name.as_ref()) format!("{{{{{}}}}}", name.as_ref())
} }
pub(crate) fn basic(i18n: &I18n) -> NoteType { pub(crate) fn basic(tr: &I18n) -> NoteType {
let mut nt = NoteType { let mut nt = NoteType {
name: i18n.notetypes_basic_name().into(), name: tr.notetypes_basic_name().into(),
..Default::default() ..Default::default()
}; };
let front = i18n.notetypes_front_field(); let front = tr.notetypes_front_field();
let back = i18n.notetypes_back_field(); let back = tr.notetypes_back_field();
nt.add_field(front.as_ref()); nt.add_field(front.as_ref());
nt.add_field(back.as_ref()); nt.add_field(back.as_ref());
nt.add_template( nt.add_template(
i18n.notetypes_card_1_name(), tr.notetypes_card_1_name(),
fieldref(front), fieldref(front),
format!( format!(
"{}\n\n<hr id=answer>\n\n{}", "{}\n\n<hr id=answer>\n\n{}",
@ -68,11 +68,11 @@ pub(crate) fn basic(i18n: &I18n) -> NoteType {
nt nt
} }
pub(crate) fn basic_typing(i18n: &I18n) -> NoteType { pub(crate) fn basic_typing(tr: &I18n) -> NoteType {
let mut nt = basic(i18n); let mut nt = basic(tr);
nt.name = i18n.notetypes_basic_type_answer_name().into(); nt.name = tr.notetypes_basic_type_answer_name().into();
let front = i18n.notetypes_front_field(); let front = tr.notetypes_front_field();
let back = i18n.notetypes_back_field(); let back = tr.notetypes_back_field();
let tmpl = &mut nt.templates[0].config; let tmpl = &mut nt.templates[0].config;
tmpl.q_format = format!("{}\n\n{{{{type:{}}}}}", fieldref(front.as_ref()), back); tmpl.q_format = format!("{}\n\n{{{{type:{}}}}}", fieldref(front.as_ref()), back);
tmpl.a_format = format!( tmpl.a_format = format!(
@ -84,13 +84,13 @@ pub(crate) fn basic_typing(i18n: &I18n) -> NoteType {
nt nt
} }
pub(crate) fn basic_forward_reverse(i18n: &I18n) -> NoteType { pub(crate) fn basic_forward_reverse(tr: &I18n) -> NoteType {
let mut nt = basic(i18n); let mut nt = basic(tr);
nt.name = i18n.notetypes_basic_reversed_name().into(); nt.name = tr.notetypes_basic_reversed_name().into();
let front = i18n.notetypes_front_field(); let front = tr.notetypes_front_field();
let back = i18n.notetypes_back_field(); let back = tr.notetypes_back_field();
nt.add_template( nt.add_template(
i18n.notetypes_card_2_name(), tr.notetypes_card_2_name(),
fieldref(back), fieldref(back),
format!( format!(
"{}\n\n<hr id=answer>\n\n{}", "{}\n\n<hr id=answer>\n\n{}",
@ -102,10 +102,10 @@ pub(crate) fn basic_forward_reverse(i18n: &I18n) -> NoteType {
nt nt
} }
pub(crate) fn basic_optional_reverse(i18n: &I18n) -> NoteType { pub(crate) fn basic_optional_reverse(tr: &I18n) -> NoteType {
let mut nt = basic_forward_reverse(i18n); let mut nt = basic_forward_reverse(tr);
nt.name = i18n.notetypes_basic_optional_reversed_name().into(); nt.name = tr.notetypes_basic_optional_reversed_name().into();
let addrev = i18n.notetypes_add_reverse_field(); let addrev = tr.notetypes_add_reverse_field();
nt.add_field(addrev.as_ref()); nt.add_field(addrev.as_ref());
let tmpl = &mut nt.templates[1].config; let tmpl = &mut nt.templates[1].config;
tmpl.q_format = format!("{{{{#{}}}}}{}{{{{/{}}}}}", addrev, tmpl.q_format, addrev); tmpl.q_format = format!("{{{{#{}}}}}{}{{{{/{}}}}}", addrev, tmpl.q_format, addrev);
@ -113,14 +113,14 @@ pub(crate) fn basic_optional_reverse(i18n: &I18n) -> NoteType {
nt nt
} }
pub(crate) fn cloze(i18n: &I18n) -> NoteType { pub(crate) fn cloze(tr: &I18n) -> NoteType {
let mut nt = NoteType { let mut nt = NoteType {
name: i18n.notetypes_cloze_name().into(), name: tr.notetypes_cloze_name().into(),
..Default::default() ..Default::default()
}; };
let text = i18n.notetypes_text_field(); let text = tr.notetypes_text_field();
nt.add_field(text.as_ref()); nt.add_field(text.as_ref());
let back_extra = i18n.notetypes_back_extra_field(); let back_extra = tr.notetypes_back_extra_field();
nt.add_field(back_extra.as_ref()); nt.add_field(back_extra.as_ref());
let qfmt = format!("{{{{cloze:{}}}}}", text); let qfmt = format!("{{{{cloze:{}}}}}", text);
let afmt = format!("{}<br>\n{{{{{}}}}}", qfmt, back_extra); let afmt = format!("{}<br>\n{{{{{}}}}}", qfmt, back_extra);

View File

@ -36,36 +36,36 @@ pub enum Op {
} }
impl Op { impl Op {
pub fn describe(self, i18n: &I18n) -> String { pub fn describe(self, tr: &I18n) -> String {
match self { match self {
Op::AddDeck => i18n.undo_add_deck(), Op::AddDeck => tr.undo_add_deck(),
Op::AddNote => i18n.undo_add_note(), Op::AddNote => tr.undo_add_note(),
Op::AnswerCard => i18n.undo_answer_card(), Op::AnswerCard => tr.undo_answer_card(),
Op::Bury => i18n.studying_bury(), Op::Bury => tr.studying_bury(),
Op::RemoveDeck => i18n.decks_delete_deck(), Op::RemoveDeck => tr.decks_delete_deck(),
Op::RemoveNote => i18n.studying_delete_note(), Op::RemoveNote => tr.studying_delete_note(),
Op::RenameDeck => i18n.actions_rename_deck(), Op::RenameDeck => tr.actions_rename_deck(),
Op::ScheduleAsNew => i18n.undo_forget_card(), Op::ScheduleAsNew => tr.undo_forget_card(),
Op::SetDueDate => i18n.actions_set_due_date(), Op::SetDueDate => tr.actions_set_due_date(),
Op::Suspend => i18n.studying_suspend(), Op::Suspend => tr.studying_suspend(),
Op::UnburyUnsuspend => i18n.undo_unbury_unsuspend(), Op::UnburyUnsuspend => tr.undo_unbury_unsuspend(),
Op::UpdateCard => i18n.undo_update_card(), Op::UpdateCard => tr.undo_update_card(),
Op::UpdateDeck => i18n.undo_update_deck(), Op::UpdateDeck => tr.undo_update_deck(),
Op::UpdateNote => i18n.undo_update_note(), Op::UpdateNote => tr.undo_update_note(),
Op::UpdatePreferences => i18n.preferences_preferences(), Op::UpdatePreferences => tr.preferences_preferences(),
Op::UpdateTag => i18n.undo_update_tag(), Op::UpdateTag => tr.undo_update_tag(),
Op::SetDeck => i18n.browsing_change_deck(), Op::SetDeck => tr.browsing_change_deck(),
Op::SetFlag => i18n.undo_set_flag(), Op::SetFlag => tr.undo_set_flag(),
Op::FindAndReplace => i18n.browsing_find_and_replace(), Op::FindAndReplace => tr.browsing_find_and_replace(),
Op::ClearUnusedTags => i18n.browsing_clear_unused_tags(), Op::ClearUnusedTags => tr.browsing_clear_unused_tags(),
Op::SortCards => i18n.browsing_reschedule(), Op::SortCards => tr.browsing_reschedule(),
Op::RenameTag => i18n.actions_rename_tag(), Op::RenameTag => tr.actions_rename_tag(),
Op::RemoveTag => i18n.actions_remove_tag(), Op::RemoveTag => tr.actions_remove_tag(),
Op::ReparentTag => i18n.actions_rename_tag(), Op::ReparentTag => tr.actions_rename_tag(),
Op::ReparentDeck => i18n.actions_rename_deck(), Op::ReparentDeck => tr.actions_rename_deck(),
Op::BuildFilteredDeck => i18n.undo_build_filtered_deck(), Op::BuildFilteredDeck => tr.undo_build_filtered_deck(),
Op::RebuildFilteredDeck => i18n.undo_build_filtered_deck(), Op::RebuildFilteredDeck => tr.undo_build_filtered_deck(),
Op::EmptyFilteredDeck => i18n.studying_empty(), Op::EmptyFilteredDeck => tr.studying_empty(),
} }
.into() .into()
} }

View File

@ -207,7 +207,7 @@ impl Collection {
.maybe_as_days(secs_until_rollover) .maybe_as_days(secs_until_rollover)
.as_seconds(), .as_seconds(),
collapse_time, collapse_time,
&self.i18n, &self.tr,
), ),
answer_button_time_collapsible( answer_button_time_collapsible(
choices choices
@ -216,7 +216,7 @@ impl Collection {
.maybe_as_days(secs_until_rollover) .maybe_as_days(secs_until_rollover)
.as_seconds(), .as_seconds(),
collapse_time, collapse_time,
&self.i18n, &self.tr,
), ),
answer_button_time_collapsible( answer_button_time_collapsible(
choices choices
@ -225,7 +225,7 @@ impl Collection {
.maybe_as_days(secs_until_rollover) .maybe_as_days(secs_until_rollover)
.as_seconds(), .as_seconds(),
collapse_time, collapse_time,
&self.i18n, &self.tr,
), ),
answer_button_time_collapsible( answer_button_time_collapsible(
choices choices
@ -234,7 +234,7 @@ impl Collection {
.maybe_as_days(secs_until_rollover) .maybe_as_days(secs_until_rollover)
.as_seconds(), .as_seconds(),
collapse_time, collapse_time,
&self.i18n, &self.tr,
), ),
]) ])
} }

View File

@ -4,26 +4,26 @@
use crate::i18n::I18n; use crate::i18n::I18n;
/// Short string like '4d' to place above answer buttons. /// Short string like '4d' to place above answer buttons.
pub fn answer_button_time(seconds: f32, i18n: &I18n) -> String { pub fn answer_button_time(seconds: f32, tr: &I18n) -> String {
let span = Timespan::from_secs(seconds).natural_span(); let span = Timespan::from_secs(seconds).natural_span();
let amount = span.as_rounded_unit_for_answer_buttons(); let amount = span.as_rounded_unit_for_answer_buttons();
match span.unit() { match span.unit() {
TimespanUnit::Seconds => i18n.scheduling_answer_button_time_seconds(amount), TimespanUnit::Seconds => tr.scheduling_answer_button_time_seconds(amount),
TimespanUnit::Minutes => i18n.scheduling_answer_button_time_minutes(amount), TimespanUnit::Minutes => tr.scheduling_answer_button_time_minutes(amount),
TimespanUnit::Hours => i18n.scheduling_answer_button_time_hours(amount), TimespanUnit::Hours => tr.scheduling_answer_button_time_hours(amount),
TimespanUnit::Days => i18n.scheduling_answer_button_time_days(amount), TimespanUnit::Days => tr.scheduling_answer_button_time_days(amount),
TimespanUnit::Months => i18n.scheduling_answer_button_time_months(amount), TimespanUnit::Months => tr.scheduling_answer_button_time_months(amount),
TimespanUnit::Years => i18n.scheduling_answer_button_time_years(amount), TimespanUnit::Years => tr.scheduling_answer_button_time_years(amount),
} }
.into() .into()
} }
/// Short string like '4d' to place above answer buttons. /// Short string like '4d' to place above answer buttons.
/// Times within the collapse time are represented like '<10m' /// Times within the collapse time are represented like '<10m'
pub fn answer_button_time_collapsible(seconds: u32, collapse_secs: u32, i18n: &I18n) -> String { pub fn answer_button_time_collapsible(seconds: u32, collapse_secs: u32, tr: &I18n) -> String {
let string = answer_button_time(seconds as f32, i18n); let string = answer_button_time(seconds as f32, tr);
if seconds == 0 { if seconds == 0 {
i18n.scheduling_end().into() tr.scheduling_end().into()
} else if seconds < collapse_secs { } else if seconds < collapse_secs {
format!("<{}", string) format!("<{}", string)
} else { } else {
@ -35,7 +35,7 @@ pub fn answer_button_time_collapsible(seconds: u32, collapse_secs: u32, i18n: &I
/// If precise is true, show to two decimal places, eg /// If precise is true, show to two decimal places, eg
/// eg 70 seconds -> "1.17 minutes" /// eg 70 seconds -> "1.17 minutes"
/// If false, seconds and days are shown without decimals. /// If false, seconds and days are shown without decimals.
pub fn time_span(seconds: f32, i18n: &I18n, precise: bool) -> String { pub fn time_span(seconds: f32, tr: &I18n, precise: bool) -> String {
let span = Timespan::from_secs(seconds).natural_span(); let span = Timespan::from_secs(seconds).natural_span();
let amount = if precise { let amount = if precise {
span.as_unit() span.as_unit()
@ -43,12 +43,12 @@ pub fn time_span(seconds: f32, i18n: &I18n, precise: bool) -> String {
span.as_rounded_unit() span.as_rounded_unit()
}; };
match span.unit() { match span.unit() {
TimespanUnit::Seconds => i18n.scheduling_time_span_seconds(amount), TimespanUnit::Seconds => tr.scheduling_time_span_seconds(amount),
TimespanUnit::Minutes => i18n.scheduling_time_span_minutes(amount), TimespanUnit::Minutes => tr.scheduling_time_span_minutes(amount),
TimespanUnit::Hours => i18n.scheduling_time_span_hours(amount), TimespanUnit::Hours => tr.scheduling_time_span_hours(amount),
TimespanUnit::Days => i18n.scheduling_time_span_days(amount), TimespanUnit::Days => tr.scheduling_time_span_days(amount),
TimespanUnit::Months => i18n.scheduling_time_span_months(amount), TimespanUnit::Months => tr.scheduling_time_span_months(amount),
TimespanUnit::Years => i18n.scheduling_time_span_years(amount), TimespanUnit::Years => tr.scheduling_time_span_years(amount),
} }
.into() .into()
} }
@ -171,20 +171,20 @@ mod test {
#[test] #[test]
fn answer_buttons() { fn answer_buttons() {
let i18n = I18n::template_only(); let tr = I18n::template_only();
assert_eq!(answer_button_time(30.0, &i18n), "30s"); assert_eq!(answer_button_time(30.0, &tr), "30s");
assert_eq!(answer_button_time(70.0, &i18n), "1m"); assert_eq!(answer_button_time(70.0, &tr), "1m");
assert_eq!(answer_button_time(1.1 * MONTH, &i18n), "1.1mo"); assert_eq!(answer_button_time(1.1 * MONTH, &tr), "1.1mo");
} }
#[test] #[test]
fn time_spans() { fn time_spans() {
let i18n = I18n::template_only(); let tr = I18n::template_only();
assert_eq!(time_span(1.0, &i18n, false), "1 second"); assert_eq!(time_span(1.0, &tr, false), "1 second");
assert_eq!(time_span(30.3, &i18n, false), "30 seconds"); assert_eq!(time_span(30.3, &tr, false), "30 seconds");
assert_eq!(time_span(30.3, &i18n, true), "30.3 seconds"); assert_eq!(time_span(30.3, &tr, true), "30.3 seconds");
assert_eq!(time_span(90.0, &i18n, false), "1.5 minutes"); assert_eq!(time_span(90.0, &tr, false), "1.5 minutes");
assert_eq!(time_span(45.0 * 86_400.0, &i18n, false), "1.5 months"); assert_eq!(time_span(45.0 * 86_400.0, &tr, false), "1.5 months");
assert_eq!(time_span(365.0 * 86_400.0 * 1.5, &i18n, false), "1.5 years"); assert_eq!(time_span(365.0 * 86_400.0 * 1.5, &tr, false), "1.5 years");
} }
} }

View File

@ -610,13 +610,13 @@ mod test {
let col_path = dir.path().join("col.anki2"); let col_path = dir.path().join("col.anki2");
fs::write(&col_path, MEDIACHECK_ANKI2).unwrap(); fs::write(&col_path, MEDIACHECK_ANKI2).unwrap();
let i18n = I18n::template_only(); let tr = I18n::template_only();
let mut col = open_collection( let mut col = open_collection(
&col_path, &col_path,
&PathBuf::new(), &PathBuf::new(),
&PathBuf::new(), &PathBuf::new(),
false, false,
i18n, tr,
log::terminal(), log::terminal(),
) )
.unwrap(); .unwrap();

View File

@ -127,74 +127,71 @@ impl Collection {
} }
fn card_stats_to_string(&mut self, cs: CardStats) -> Result<String> { fn card_stats_to_string(&mut self, cs: CardStats) -> Result<String> {
let i18n = &self.i18n; let tr = &self.tr;
let mut stats = vec![(i18n.card_stats_added().into(), cs.added.date_string())]; let mut stats = vec![(tr.card_stats_added().into(), cs.added.date_string())];
if let Some(first) = cs.first_review { if let Some(first) = cs.first_review {
stats.push((i18n.card_stats_first_review().into(), first.date_string())) stats.push((tr.card_stats_first_review().into(), first.date_string()))
} }
if let Some(last) = cs.latest_review { if let Some(last) = cs.latest_review {
stats.push((i18n.card_stats_latest_review().into(), last.date_string())) stats.push((tr.card_stats_latest_review().into(), last.date_string()))
} }
match cs.due { match cs.due {
Due::Time(secs) => { Due::Time(secs) => {
stats.push((i18n.statistics_due_date().into(), secs.date_string())); stats.push((tr.statistics_due_date().into(), secs.date_string()));
} }
Due::Position(pos) => { Due::Position(pos) => {
stats.push((i18n.card_stats_new_card_position().into(), pos.to_string())); stats.push((tr.card_stats_new_card_position().into(), pos.to_string()));
} }
Due::Unknown => {} Due::Unknown => {}
}; };
if cs.interval_secs > 0 { if cs.interval_secs > 0 {
stats.push(( stats.push((
i18n.card_stats_interval().into(), tr.card_stats_interval().into(),
time_span(cs.interval_secs as f32, i18n, true), time_span(cs.interval_secs as f32, tr, true),
)); ));
} }
if cs.ease > 0 { if cs.ease > 0 {
stats.push((i18n.card_stats_ease().into(), format!("{}%", cs.ease))); stats.push((tr.card_stats_ease().into(), format!("{}%", cs.ease)));
} }
stats.push(( stats.push((tr.card_stats_review_count().into(), cs.reviews.to_string()));
i18n.card_stats_review_count().into(), stats.push((tr.card_stats_lapse_count().into(), cs.lapses.to_string()));
cs.reviews.to_string(),
));
stats.push((i18n.card_stats_lapse_count().into(), cs.lapses.to_string()));
if cs.total_secs > 0.0 { if cs.total_secs > 0.0 {
stats.push(( stats.push((
i18n.card_stats_average_time().into(), tr.card_stats_average_time().into(),
time_span(cs.average_secs, i18n, true), time_span(cs.average_secs, tr, true),
)); ));
stats.push(( stats.push((
i18n.card_stats_total_time().into(), tr.card_stats_total_time().into(),
time_span(cs.total_secs, i18n, true), time_span(cs.total_secs, tr, true),
)); ));
} }
stats.push((i18n.card_stats_card_template().into(), cs.card_type)); stats.push((tr.card_stats_card_template().into(), cs.card_type));
stats.push((i18n.card_stats_note_type().into(), cs.note_type)); stats.push((tr.card_stats_note_type().into(), cs.note_type));
stats.push((i18n.card_stats_deck_name().into(), cs.deck)); stats.push((tr.card_stats_deck_name().into(), cs.deck));
stats.push((i18n.card_stats_card_id().into(), cs.cid.0.to_string())); stats.push((tr.card_stats_card_id().into(), cs.cid.0.to_string()));
stats.push((i18n.card_stats_note_id().into(), cs.nid.0.to_string())); stats.push((tr.card_stats_note_id().into(), cs.nid.0.to_string()));
let revlog = cs let revlog = cs
.revlog .revlog
.into_iter() .into_iter()
.rev() .rev()
.map(|e| revlog_to_text(e, i18n)) .map(|e| revlog_to_text(e, tr))
.collect(); .collect();
let revlog_titles = RevlogText { let revlog_titles = RevlogText {
time: i18n.card_stats_review_log_date().into(), time: tr.card_stats_review_log_date().into(),
kind: i18n.card_stats_review_log_type().into(), kind: tr.card_stats_review_log_type().into(),
kind_class: "".to_string(), kind_class: "".to_string(),
rating: i18n.card_stats_review_log_rating().into(), rating: tr.card_stats_review_log_rating().into(),
interval: i18n.card_stats_interval().into(), interval: tr.card_stats_interval().into(),
ease: i18n.card_stats_ease().into(), ease: tr.card_stats_ease().into(),
rating_class: "".to_string(), rating_class: "".to_string(),
taken_secs: i18n.card_stats_review_log_time_taken().into(), taken_secs: tr.card_stats_review_log_time_taken().into(),
}; };
Ok(CardStatsTemplate { Ok(CardStatsTemplate {
@ -207,15 +204,15 @@ impl Collection {
} }
} }
fn revlog_to_text(e: RevlogEntry, i18n: &I18n) -> RevlogText { fn revlog_to_text(e: RevlogEntry, tr: &I18n) -> RevlogText {
let dt = Local.timestamp(e.id.as_secs().0, 0); let dt = Local.timestamp(e.id.as_secs().0, 0);
let time = dt.format("<b>%Y-%m-%d</b> @ %H:%M").to_string(); let time = dt.format("<b>%Y-%m-%d</b> @ %H:%M").to_string();
let kind = match e.review_kind { let kind = match e.review_kind {
RevlogReviewKind::Learning => i18n.card_stats_review_log_type_learn().into(), RevlogReviewKind::Learning => tr.card_stats_review_log_type_learn().into(),
RevlogReviewKind::Review => i18n.card_stats_review_log_type_review().into(), RevlogReviewKind::Review => tr.card_stats_review_log_type_review().into(),
RevlogReviewKind::Relearning => i18n.card_stats_review_log_type_relearn().into(), RevlogReviewKind::Relearning => tr.card_stats_review_log_type_relearn().into(),
RevlogReviewKind::EarlyReview => i18n.card_stats_review_log_type_filtered().into(), RevlogReviewKind::EarlyReview => tr.card_stats_review_log_type_filtered().into(),
RevlogReviewKind::Manual => i18n.card_stats_review_log_type_manual().into(), RevlogReviewKind::Manual => tr.card_stats_review_log_type_manual().into(),
}; };
let kind_class = match e.review_kind { let kind_class = match e.review_kind {
RevlogReviewKind::Learning => String::from("revlog-learn"), RevlogReviewKind::Learning => String::from("revlog-learn"),
@ -229,7 +226,7 @@ fn revlog_to_text(e: RevlogEntry, i18n: &I18n) -> RevlogText {
String::from("") String::from("")
} else { } else {
let interval_secs = e.interval_secs(); let interval_secs = e.interval_secs();
time_span(interval_secs as f32, i18n, true) time_span(interval_secs as f32, tr, true)
}; };
let ease = if e.ease_factor > 0 { let ease = if e.ease_factor > 0 {
format!("{}%", e.ease_factor / 10) format!("{}%", e.ease_factor / 10)
@ -241,7 +238,7 @@ fn revlog_to_text(e: RevlogEntry, i18n: &I18n) -> RevlogText {
} else { } else {
"".to_string() "".to_string()
}; };
let taken_secs = i18n let taken_secs = tr
.statistics_seconds_taken((e.taken_millis / 1000) as i32) .statistics_seconds_taken((e.taken_millis / 1000) as i32)
.into(); .into();

View File

@ -3,7 +3,7 @@
use crate::{i18n::I18n, prelude::*, scheduler::timespan::Timespan}; use crate::{i18n::I18n, prelude::*, scheduler::timespan::Timespan};
pub fn studied_today(cards: u32, secs: f32, i18n: &I18n) -> String { pub fn studied_today(cards: u32, secs: f32, tr: &I18n) -> String {
let span = Timespan::from_secs(secs).natural_span(); let span = Timespan::from_secs(secs).natural_span();
let amount = span.as_unit(); let amount = span.as_unit();
let unit = span.unit().as_str(); let unit = span.unit().as_str();
@ -12,7 +12,7 @@ pub fn studied_today(cards: u32, secs: f32, i18n: &I18n) -> String {
} else { } else {
0.0 0.0
}; };
i18n.statistics_studied_today(unit, secs_per_card, amount, cards) tr.statistics_studied_today(unit, secs_per_card, amount, cards)
.into() .into()
} }
@ -20,7 +20,7 @@ impl Collection {
pub fn studied_today(&mut self) -> Result<String> { pub fn studied_today(&mut self) -> Result<String> {
let timing = self.timing_today()?; let timing = self.timing_today()?;
let today = self.storage.studied_today(timing.next_day_at)?; let today = self.storage.studied_today(timing.next_day_at)?;
Ok(studied_today(today.cards, today.seconds as f32, &self.i18n)) Ok(studied_today(today.cards, today.seconds as f32, &self.tr))
} }
} }
@ -32,9 +32,9 @@ mod test {
#[test] #[test]
fn today() { fn today() {
// temporary test of fluent term handling // temporary test of fluent term handling
let i18n = I18n::template_only(); let tr = I18n::template_only();
assert_eq!( assert_eq!(
&studied_today(3, 13.0, &i18n).replace("\n", " "), &studied_today(3, 13.0, &tr).replace("\n", " "),
"Studied 3 cards in 13 seconds today (4.33s/card)" "Studied 3 cards in 13 seconds today (4.33s/card)"
); );
} }

View File

@ -507,8 +507,8 @@ mod test {
#[test] #[test]
fn add_card() { fn add_card() {
let i18n = I18n::template_only(); let tr = I18n::template_only();
let storage = SqliteStorage::open_or_create(Path::new(":memory:"), &i18n, false).unwrap(); let storage = SqliteStorage::open_or_create(Path::new(":memory:"), &tr, false).unwrap();
let mut card = Card::default(); let mut card = Card::default();
storage.add_card(&mut card).unwrap(); storage.add_card(&mut card).unwrap();
let id1 = card.id; let id1 = card.id;

View File

@ -335,11 +335,11 @@ impl SqliteStorage {
// Upgrading/downgrading/legacy // Upgrading/downgrading/legacy
pub(super) fn add_default_deck(&self, i18n: &I18n) -> Result<()> { pub(super) fn add_default_deck(&self, tr: &I18n) -> Result<()> {
let mut deck = Deck::new_normal(); let mut deck = Deck::new_normal();
deck.id.0 = 1; deck.id.0 = 1;
// fixme: separate key // fixme: separate key
deck.name = i18n.deck_config_default_name().into(); deck.name = tr.deck_config_default_name().into();
self.add_or_update_deck_with_existing_id(&deck) self.add_or_update_deck_with_existing_id(&deck)
} }

View File

@ -113,10 +113,10 @@ impl SqliteStorage {
// Creating/upgrading/downgrading // Creating/upgrading/downgrading
pub(super) fn add_default_deck_config(&self, i18n: &I18n) -> Result<()> { pub(super) fn add_default_deck_config(&self, tr: &I18n) -> Result<()> {
let mut conf = DeckConf::default(); let mut conf = DeckConf::default();
conf.id.0 = 1; conf.id.0 = 1;
conf.name = i18n.deck_config_default_name().into(); conf.name = tr.deck_config_default_name().into();
self.add_deck_conf(&mut conf) self.add_deck_conf(&mut conf)
} }

View File

@ -136,7 +136,7 @@ fn trace(s: &str) {
} }
impl SqliteStorage { impl SqliteStorage {
pub(crate) fn open_or_create(path: &Path, i18n: &I18n, server: bool) -> Result<Self> { pub(crate) fn open_or_create(path: &Path, tr: &I18n, server: bool) -> Result<Self> {
let db = open_or_create_collection_db(path)?; let db = open_or_create_collection_db(path)?;
let (create, ver) = schema_version(&db)?; let (create, ver) = schema_version(&db)?;
@ -184,9 +184,9 @@ impl SqliteStorage {
} }
if create { if create {
storage.add_default_deck_config(i18n)?; storage.add_default_deck_config(tr)?;
storage.add_default_deck(i18n)?; storage.add_default_deck(tr)?;
storage.add_stock_notetypes(i18n)?; storage.add_stock_notetypes(tr)?;
} }
if create || upgrade { if create || upgrade {

View File

@ -1235,8 +1235,8 @@ mod test {
fn open_col(dir: &Path, server: bool, fname: &str) -> Result<Collection> { fn open_col(dir: &Path, server: bool, fname: &str) -> Result<Collection> {
let path = dir.join(fname); let path = dir.join(fname);
let i18n = I18n::template_only(); let tr = I18n::template_only();
open_collection(path, "".into(), "".into(), server, i18n, log::terminal()) open_collection(path, "".into(), "".into(), server, tr, log::terminal())
} }
#[async_trait(?Send)] #[async_trait(?Send)]
@ -1390,7 +1390,7 @@ mod test {
col1.add_or_update_deck(&mut deck)?; col1.add_or_update_deck(&mut deck)?;
// and a new notetype // and a new notetype
let mut nt = all_stock_notetypes(&col1.i18n).remove(0); let mut nt = all_stock_notetypes(&col1.tr).remove(0);
nt.name = "new".into(); nt.name = "new".into();
col1.add_notetype(&mut nt)?; col1.add_notetype(&mut nt)?;
@ -1550,7 +1550,10 @@ mod test {
// removing things like a notetype forces a full sync // removing things like a notetype forces a full sync
col2.remove_notetype(ntid)?; col2.remove_notetype(ntid)?;
let out = ctx.normal_sync(&mut col2).await; let out = ctx.normal_sync(&mut col2).await;
assert!(matches!(out.required, SyncActionRequired::FullSyncRequired { .. })); assert!(matches!(
out.required,
SyncActionRequired::FullSyncRequired { .. }
));
Ok(()) Ok(())
} }

View File

@ -246,14 +246,14 @@ fn parse_inner<'a, I: Iterator<Item = TemplateResult<Token<'a>>>>(
} }
} }
fn template_error_to_anki_error(err: TemplateError, q_side: bool, i18n: &I18n) -> AnkiError { fn template_error_to_anki_error(err: TemplateError, q_side: bool, tr: &I18n) -> AnkiError {
let header = if q_side { let header = if q_side {
i18n.card_template_rendering_front_side_problem() tr.card_template_rendering_front_side_problem()
} else { } else {
i18n.card_template_rendering_back_side_problem() tr.card_template_rendering_back_side_problem()
}; };
let details = localized_template_error(i18n, err); let details = localized_template_error(tr, err);
let more_info = i18n.card_template_rendering_more_info(); let more_info = tr.card_template_rendering_more_info();
let info = format!( let info = format!(
"{}<br>{}<br><a href='{}'>{}</a>", "{}<br>{}<br><a href='{}'>{}</a>",
header, details, TEMPLATE_ERROR_LINK, more_info header, details, TEMPLATE_ERROR_LINK, more_info
@ -262,31 +262,31 @@ fn template_error_to_anki_error(err: TemplateError, q_side: bool, i18n: &I18n) -
AnkiError::TemplateError { info } AnkiError::TemplateError { info }
} }
fn localized_template_error(i18n: &I18n, err: TemplateError) -> String { fn localized_template_error(tr: &I18n, err: TemplateError) -> String {
match err { match err {
TemplateError::NoClosingBrackets(tag) => i18n TemplateError::NoClosingBrackets(tag) => tr
.card_template_rendering_no_closing_brackets("}}", tag) .card_template_rendering_no_closing_brackets("}}", tag)
.into(), .into(),
TemplateError::ConditionalNotClosed(tag) => i18n TemplateError::ConditionalNotClosed(tag) => tr
.card_template_rendering_conditional_not_closed(format!("{{{{/{}}}}}", tag)) .card_template_rendering_conditional_not_closed(format!("{{{{/{}}}}}", tag))
.into(), .into(),
TemplateError::ConditionalNotOpen { TemplateError::ConditionalNotOpen {
closed, closed,
currently_open, currently_open,
} => if let Some(open) = currently_open { } => if let Some(open) = currently_open {
i18n.card_template_rendering_wrong_conditional_closed( tr.card_template_rendering_wrong_conditional_closed(
format!("{{{{/{}}}}}", closed), format!("{{{{/{}}}}}", closed),
format!("{{{{/{}}}}}", open), format!("{{{{/{}}}}}", open),
) )
} else { } else {
i18n.card_template_rendering_conditional_not_open( tr.card_template_rendering_conditional_not_open(
format!("{{{{/{}}}}}", closed), format!("{{{{/{}}}}}", closed),
format!("{{{{#{}}}}}", closed), format!("{{{{#{}}}}}", closed),
format!("{{{{^{}}}}}", closed), format!("{{{{^{}}}}}", closed),
) )
} }
.into(), .into(),
TemplateError::FieldNotFound { field, filters } => i18n TemplateError::FieldNotFound { field, filters } => tr
.card_template_rendering_no_such_field(format!("{{{{{}{}}}}}", filters, field), field) .card_template_rendering_no_such_field(format!("{{{{{}{}}}}}", filters, field), field)
.into(), .into(),
} }
@ -531,7 +531,7 @@ pub fn render_card(
field_map: &HashMap<&str, Cow<str>>, field_map: &HashMap<&str, Cow<str>>,
card_ord: u16, card_ord: u16,
is_cloze: bool, is_cloze: bool,
i18n: &I18n, tr: &I18n,
) -> Result<(Vec<RenderedNode>, Vec<RenderedNode>)> { ) -> Result<(Vec<RenderedNode>, Vec<RenderedNode>)> {
// prepare context // prepare context
let mut context = RenderContext { let mut context = RenderContext {
@ -544,22 +544,22 @@ pub fn render_card(
// question side // question side
let (mut qnodes, qtmpl) = ParsedTemplate::from_text(qfmt) let (mut qnodes, qtmpl) = ParsedTemplate::from_text(qfmt)
.and_then(|tmpl| Ok((tmpl.render(&context)?, tmpl))) .and_then(|tmpl| Ok((tmpl.render(&context)?, tmpl)))
.map_err(|e| template_error_to_anki_error(e, true, i18n))?; .map_err(|e| template_error_to_anki_error(e, true, tr))?;
// check if the front side was empty // check if the front side was empty
let empty_message = if is_cloze && cloze_is_empty(field_map, card_ord) { let empty_message = if is_cloze && cloze_is_empty(field_map, card_ord) {
Some(format!( Some(format!(
"<div>{}<br><a href='{}'>{}</a></div>", "<div>{}<br><a href='{}'>{}</a></div>",
i18n.card_template_rendering_missing_cloze(card_ord + 1), tr.card_template_rendering_missing_cloze(card_ord + 1),
TEMPLATE_BLANK_CLOZE_LINK, TEMPLATE_BLANK_CLOZE_LINK,
i18n.card_template_rendering_more_info() tr.card_template_rendering_more_info()
)) ))
} else if !is_cloze && !qtmpl.renders_with_fields(context.nonempty_fields) { } else if !is_cloze && !qtmpl.renders_with_fields(context.nonempty_fields) {
Some(format!( Some(format!(
"<div>{}<br><a href='{}'>{}</a></div>", "<div>{}<br><a href='{}'>{}</a></div>",
i18n.card_template_rendering_empty_front(), tr.card_template_rendering_empty_front(),
TEMPLATE_BLANK_LINK, TEMPLATE_BLANK_LINK,
i18n.card_template_rendering_more_info() tr.card_template_rendering_more_info()
)) ))
} else { } else {
None None
@ -573,7 +573,7 @@ pub fn render_card(
context.question_side = false; context.question_side = false;
let anodes = ParsedTemplate::from_text(afmt) let anodes = ParsedTemplate::from_text(afmt)
.and_then(|tmpl| tmpl.render(&context)) .and_then(|tmpl| tmpl.render(&context))
.map_err(|e| template_error_to_anki_error(e, false, i18n))?; .map_err(|e| template_error_to_anki_error(e, false, tr))?;
Ok((qnodes, anodes)) Ok((qnodes, anodes))
} }
@ -1113,10 +1113,10 @@ mod test {
.map(|r| (r.0, r.1.into())) .map(|r| (r.0, r.1.into()))
.collect(); .collect();
let i18n = I18n::template_only(); let tr = I18n::template_only();
use crate::template::RenderedNode as FN; use crate::template::RenderedNode as FN;
let qnodes = super::render_card("test{{E}}", "", &map, 1, false, &i18n) let qnodes = super::render_card("test{{E}}", "", &map, 1, false, &tr)
.unwrap() .unwrap()
.0; .0;
assert_eq!( assert_eq!(