diff --git a/proto/backend.proto b/proto/backend.proto index 41224421b..77dc50a0d 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -18,6 +18,7 @@ enum StringsGroup { MEDIA_CHECK = 2; CARD_TEMPLATES = 3; SYNC = 4; + NETWORK = 5; } // 1-15 reserved for future use; 2047 for errors @@ -102,6 +103,7 @@ message NetworkError { PROXY_AUTH = 3; } NetworkErrorKind kind = 2; + string localized = 3; } message SyncError { @@ -117,6 +119,7 @@ message SyncError { RESYNC_REQUIRED = 7; } SyncErrorKind kind = 2; + string localized = 3; } message MediaSyncProgress { diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 3c1a074ea..1c089303a 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -37,6 +37,9 @@ class NetworkError(StringError): def kind(self) -> NetworkErrorKind: return self.args[1] + def localized(self) -> str: + return self.args[2] + class IOError(StringError): pass @@ -57,6 +60,9 @@ class SyncError(StringError): def kind(self) -> SyncErrorKind: return self.args[1] + def localized(self) -> str: + return self.args[2] + def proto_exception_to_native(err: pb.BackendError) -> Exception: val = err.WhichOneof("value") @@ -64,7 +70,7 @@ def proto_exception_to_native(err: pb.BackendError) -> Exception: return Interrupted() elif val == "network_error": e = err.network_error - return NetworkError(e.info, e.kind) + return NetworkError(e.info, e.kind, e.localized) elif val == "io_error": return IOError(err.io_error.info) elif val == "db_error": @@ -75,7 +81,7 @@ def proto_exception_to_native(err: pb.BackendError) -> Exception: return StringError(err.invalid_input.info) elif val == "sync_error": e2 = err.sync_error - return SyncError(e2.info, e2.kind) + return SyncError(e2.info, e2.kind, e2.localized) else: assert_impossible_literal(val) diff --git a/qt/aqt/mediasync.py b/qt/aqt/mediasync.py index 47821f698..f0a4b7f09 100644 --- a/qt/aqt/mediasync.py +++ b/qt/aqt/mediasync.py @@ -10,18 +10,14 @@ from typing import List, Union import aqt from anki import hooks -from anki.lang import _ from anki.rsbackend import ( - DBError, Interrupted, MediaSyncProgress, NetworkError, - NetworkErrorKind, Progress, ProgressKind, StringsGroup, SyncError, - SyncErrorKind, ) from anki.types import assert_impossible from anki.utils import intTime @@ -114,40 +110,10 @@ class MediaSyncer: self._log_and_notify(tr(StringsGroup.SYNC, "media-failed")) if isinstance(exc, SyncError): - kind = exc.kind() - if kind == SyncErrorKind.AUTH_FAILED: - self.mw.pm.set_sync_key(None) - showWarning( - _("AnkiWeb ID or password was incorrect; please try again.") - ) - elif kind == SyncErrorKind.SERVER_ERROR: - showWarning( - _( - "AnkiWeb encountered a problem. Please try again in a few minutes." - ) - ) - elif kind == SyncErrorKind.MEDIA_CHECK_REQUIRED: - showWarning(_("Please use the Tools>Check Media menu option.")) - elif kind == SyncErrorKind.RESYNC_REQUIRED: - showWarning( - _( - "Please sync again, and post on the support forum if this message keeps appearing." - ) - ) - else: - showWarning(_("Unexpected error: {}").format(str(exc))) + showWarning(exc.localized()) elif isinstance(exc, NetworkError): - nkind = exc.kind() - if nkind in (NetworkErrorKind.OFFLINE, NetworkErrorKind.TIMEOUT): - showWarning( - _("Syncing failed; please check your internet connection.") - + "\n\n" - + _("Detailed error: {}").format(str(exc)) - ) - else: - showWarning(_("Unexpected error: {}").format(str(exc))) - elif isinstance(exc, DBError): - showWarning(_("Problem accessing the media database: {}").format(str(exc))) + msg = exc.localized() + msg += "\n\n" + tr(StringsGroup.NETWORK, "details", details=str(exc)) else: raise exc diff --git a/rslib/src/backend.rs b/rslib/src/backend.rs index 2d327739e..292f78834 100644 --- a/rslib/src/backend.rs +++ b/rslib/src/backend.rs @@ -39,33 +39,34 @@ enum Progress<'a> { } /// Convert an Anki error to a protobuf error. -impl std::convert::From for pb::BackendError { - fn from(err: AnkiError) -> Self { - use pb::backend_error::Value as V; - let value = match err { - AnkiError::InvalidInput { info } => V::InvalidInput(pb::StringError { info }), - 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 { - info, - kind: kind.into(), - }), - AnkiError::SyncError { info, kind } => V::SyncError(pb::SyncError { - info, - kind: kind.into(), - }), - AnkiError::Interrupted => V::Interrupted(Empty {}), - }; +fn anki_error_to_proto_error(err: AnkiError, i18n: &I18n) -> pb::BackendError { + use pb::backend_error::Value as V; + let localized = err.localized_description(i18n); + let value = match err { + AnkiError::InvalidInput { info } => V::InvalidInput(pb::StringError { info }), + 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 { + info, + kind: kind.into(), + localized, + }), + AnkiError::SyncError { info, kind } => V::SyncError(pb::SyncError { + info, + kind: kind.into(), + localized, + }), + AnkiError::Interrupted => V::Interrupted(Empty {}), + }; - pb::BackendError { value: Some(value) } - } + pb::BackendError { value: Some(value) } } // Convert an Anki error to a protobuf output. -impl std::convert::From for pb::backend_output::Value { - fn from(err: AnkiError) -> Self { - pb::backend_output::Value::Error(err.into()) +impl std::convert::From for pb::backend_output::Value { + fn from(err: pb::BackendError) -> Self { + pb::backend_output::Value::Error(err) } } @@ -136,8 +137,9 @@ impl Backend { Err(_e) => { // unable to decode let err = AnkiError::invalid_input("couldn't decode backend request"); + let oerr = anki_error_to_proto_error(err, &self.i18n); let output = pb::BackendOutput { - value: Some(err.into()), + value: Some(oerr.into()), }; output.encode(&mut buf).expect("encode failed"); return buf; @@ -153,10 +155,14 @@ impl Backend { let oval = if let Some(ival) = input.value { match self.run_command_inner(ival) { Ok(output) => output, - Err(err) => err.into(), + Err(err) => anki_error_to_proto_error(err, &self.i18n).into(), } } else { - AnkiError::invalid_input("unrecognized backend input value").into() + anki_error_to_proto_error( + AnkiError::invalid_input("unrecognized backend input value"), + &self.i18n, + ) + .into() }; pb::BackendOutput { value: Some(oval) } diff --git a/rslib/src/err.rs b/rslib/src/err.rs index f63aea920..9f1b60f2f 100644 --- a/rslib/src/err.rs +++ b/rslib/src/err.rs @@ -1,6 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use crate::i18n::{I18n, StringsGroup}; pub use failure::{Error, Fail}; use reqwest::StatusCode; use std::io; @@ -53,6 +54,36 @@ impl AnkiError { kind: SyncErrorKind::Other, } } + + pub fn localized_description(&self, i18n: &I18n) -> String { + match self { + AnkiError::SyncError { info, kind } => { + let cat = i18n.get(StringsGroup::Sync); + match kind { + SyncErrorKind::ServerMessage => info.into(), + SyncErrorKind::Other => info.into(), + SyncErrorKind::Conflict => cat.tr("conflict"), + SyncErrorKind::ServerError => cat.tr("server-error"), + SyncErrorKind::ClientTooOld => cat.tr("client-too-old"), + SyncErrorKind::AuthFailed => cat.tr("wrong-pass"), + SyncErrorKind::MediaCheckRequired => cat.tr("media-check-required"), + SyncErrorKind::ResyncRequired => cat.tr("resync-required"), + } + .into() + } + AnkiError::NetworkError { kind, .. } => { + let cat = i18n.get(StringsGroup::Network); + match kind { + NetworkErrorKind::Offline => cat.tr("offline"), + NetworkErrorKind::Timeout => cat.tr("timeout"), + NetworkErrorKind::ProxyAuth => cat.tr("proxy-auth"), + NetworkErrorKind::Other => cat.tr("other"), + } + .into() + } + _ => "".into(), + } + } } #[derive(Debug, PartialEq)] diff --git a/rslib/src/i18n/mod.rs b/rslib/src/i18n/mod.rs index 2156822c6..b1a72c312 100644 --- a/rslib/src/i18n/mod.rs +++ b/rslib/src/i18n/mod.rs @@ -56,6 +56,7 @@ fn ftl_fallback_for_group(group: StringsGroup) -> String { StringsGroup::MediaCheck => include_str!("media-check.ftl"), StringsGroup::CardTemplates => include_str!("card-template-rendering.ftl"), StringsGroup::Sync => include_str!("sync.ftl"), + StringsGroup::Network => include_str!("network.ftl"), } .to_string() } @@ -69,6 +70,7 @@ fn localized_ftl_for_group(group: StringsGroup, lang_ftl_folder: &Path) -> Optio StringsGroup::MediaCheck => "media-check.ftl", StringsGroup::CardTemplates => "card-template-rendering.ftl", StringsGroup::Sync => "sync.ftl", + StringsGroup::Network => "network.ftl", }); fs::read_to_string(&path) .map_err(|e| { diff --git a/rslib/src/i18n/network.ftl b/rslib/src/i18n/network.ftl new file mode 100644 index 000000000..3ab7835a2 --- /dev/null +++ b/rslib/src/i18n/network.ftl @@ -0,0 +1,6 @@ +offline = Please check your internet connection. +timeout = Connection timed out. Please try again on a different network. +proxy-auth = Your proxy requires authentication. +other = A network error occurred. + +details = Error details: {$details} diff --git a/rslib/src/i18n/sync.ftl b/rslib/src/i18n/sync.ftl index 71d0f1b28..e8b738268 100644 --- a/rslib/src/i18n/sync.ftl +++ b/rslib/src/i18n/sync.ftl @@ -14,3 +14,14 @@ media-aborted = Media sync aborted. media-disabled = Media sync disabled. abort-button = Abort + +## Error messages + +conflict = Only one copy of Anki can sync to your account at once. Please wait a few minutes, then try again. +server-error = AnkiWeb encountered a problem. Please try again in a few minutes. +client-too-old = + Your Anki version is too old. Please update to the latest version to continue syncing. +wrong-pass = AnkiWeb ID or password was incorrect; please try again. +media-check-required = Please use the Check Media function, then sync again. +resync-required = + Please sync again. If this message keeps appearing, please post on the support site.