use std::fmt;
use super::LocaleId;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum LocaleRejection {
Empty,
TooLong,
NotWellFormed,
}
impl fmt::Display for LocaleRejection {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let reason = match self {
Self::Empty => "it was empty",
Self::TooLong => "it was too long to be a language identifier",
Self::NotWellFormed => "it is not a well-formed BCP-47 language identifier",
};
formatter.write_str(reason)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum I18nError {
InvalidLocale(LocaleRejection),
Parse {
locale: LocaleId,
errors: Vec<String>,
},
Missing {
locale: LocaleId,
key: String,
},
NotNegotiated,
Format {
locale: LocaleId,
key: String,
errors: Vec<String>,
},
}
impl fmt::Display for I18nError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidLocale(reason) => write!(formatter, "invalid locale tag: {reason}"),
Self::Parse { locale, errors } => write!(
formatter,
"the `{locale}` catalog did not parse: {}",
errors.join("; ")
),
Self::Missing { locale, key } => write!(
formatter,
"no message `{key}` in the `{locale}` catalog or the default one"
),
Self::NotNegotiated => {
formatter.write_str("no locale on this request: the route is missing `LocaleLayer`")
}
Self::Format {
locale,
key,
errors,
} => write!(
formatter,
"message `{key}` in the `{locale}` catalog could not be formatted: {}",
errors.join("; ")
),
}
}
}
impl std::error::Error for I18nError {}
impl From<I18nError> for crate::Error {
fn from(error: I18nError) -> Self {
report(&error);
crate::Error::Other("translation failed".into())
}
}
fn report(error: &I18nError) {
#[cfg(feature = "observe")]
tracing::error!(%error, "a translation failed; the client gets a generic 500");
#[cfg(not(feature = "observe"))]
let _ = error;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_rejected_tag_is_never_quoted_back() {
let hostile = "../../etc/passwd\nFAKE LOG LINE";
let error = LocaleId::parse(hostile).unwrap_err();
assert!(!error.to_string().contains("passwd"));
assert!(!error.to_string().contains('\n'));
assert!(!format!("{error:?}").contains("passwd"));
}
#[test]
fn the_framework_error_carries_no_catalog_detail() {
let error = I18nError::Missing {
locale: LocaleId::parse("en").unwrap(),
key: "billing-invoice-overdue".into(),
};
assert!(error.to_string().contains("billing-invoice-overdue"));
let framework: crate::Error = error.into();
assert_eq!(framework.status(), 500);
assert_eq!(framework.code(), "internal_error");
assert!(!framework.to_string().contains("billing-invoice-overdue"));
}
}