diff --git a/crates/axum-utils/src/fancy_error.rs b/crates/axum-utils/src/fancy_error.rs index 27c238d6..bad289bd 100644 --- a/crates/axum-utils/src/fancy_error.rs +++ b/crates/axum-utils/src/fancy_error.rs @@ -15,8 +15,9 @@ use axum::{ http::StatusCode, response::{IntoResponse, Response}, - Extension, + Extension, TypedHeader, }; +use headers::ContentType; use mas_templates::ErrorContext; use crate::sentry::SentryEventID; @@ -60,10 +61,11 @@ impl From for FancyError { impl IntoResponse for FancyError { fn into_response(self) -> Response { - let error = format!("{:?}", self.context); + let error = format!("{}", self.context); let event_id = sentry::capture_message(&error, sentry::Level::Error); ( StatusCode::INTERNAL_SERVER_ERROR, + TypedHeader(ContentType::text()), SentryEventID::from(event_id), Extension(self.context), error, diff --git a/crates/templates/src/context.rs b/crates/templates/src/context.rs index 91cb4473..15a7fad7 100644 --- a/crates/templates/src/context.rs +++ b/crates/templates/src/context.rs @@ -14,6 +14,8 @@ //! Contexts used in templates +use std::fmt::Formatter; + use chrono::{DateTime, Utc}; use http::{Method, Uri, Version}; use mas_data_model::{ @@ -984,6 +986,23 @@ pub struct ErrorContext { details: Option, } +impl std::fmt::Display for ErrorContext { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if let Some(code) = &self.code { + writeln!(f, "code: {code}")?; + } + if let Some(description) = &self.description { + writeln!(f, "{description}")?; + } + + if let Some(details) = &self.details { + writeln!(f, "details: {details}")?; + } + + Ok(()) + } +} + impl TemplateContext for ErrorContext { fn sample(_now: chrono::DateTime, _rng: &mut impl Rng) -> Vec where diff --git a/crates/templates/src/functions.rs b/crates/templates/src/functions.rs index 8deb65d3..1fd91d4a 100644 --- a/crates/templates/src/functions.rs +++ b/crates/templates/src/functions.rs @@ -56,7 +56,10 @@ pub fn register( vite_manifest, }), ); - env.add_global("_", Value::from_object(Translate { translator })); + env.add_global( + "translator", + Value::from_object(TranslatorFunc { translator }), + ); } fn tester_empty(seq: &dyn SeqObject) -> bool { @@ -189,52 +192,73 @@ fn function_add_params_to_url( Ok(uri.to_string()) } -struct Translate { +struct TranslatorFunc { translator: Arc, } -impl std::fmt::Debug for Translate { +impl std::fmt::Debug for TranslatorFunc { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Translate") - .field("translations", &"..") + f.debug_struct("TranslatorFunc") + .field("translator", &"..") .finish() } } -impl std::fmt::Display for Translate { +impl std::fmt::Display for TranslatorFunc { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("translator") + } +} + +impl Object for TranslatorFunc { + fn call(&self, _state: &State, args: &[Value]) -> Result { + let (lang,): (&str,) = from_args(args)?; + + let lang: DataLocale = lang.parse().map_err(|e| { + Error::new(ErrorKind::InvalidOperation, "Invalid language").with_source(e) + })?; + + Ok(Value::from_object(TranslateFunc { + lang, + translator: Arc::clone(&self.translator), + })) + } +} + +struct TranslateFunc { + translator: Arc, + lang: DataLocale, +} + +impl std::fmt::Debug for TranslateFunc { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Translate") + .field("translator", &"..") + .field("lang", &self.lang) + .finish() + } +} + +impl std::fmt::Display for TranslateFunc { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("translate") } } -impl Object for Translate { +impl Object for TranslateFunc { fn call(&self, state: &State, args: &[Value]) -> Result { - let lang = state.lookup("lang").ok_or(minijinja::Error::new( - ErrorKind::UndefinedError, - "`lang` is not set", - ))?; - - let lang = lang.as_str().ok_or(minijinja::Error::new( - ErrorKind::InvalidOperation, - "`lang` is not a string", - ))?; - - let lang: DataLocale = lang.parse().map_err(|e| { - Error::new(ErrorKind::InvalidOperation, "Could not parse `lang`").with_source(e) - })?; - let (key, kwargs): (&str, Kwargs) = from_args(args)?; let (message, _locale) = if let Some(count) = kwargs.get("count")? { self.translator - .plural_with_fallback(lang, key, count) + .plural_with_fallback(self.lang.clone(), key, count) .ok_or(Error::new( ErrorKind::InvalidOperation, "Missing translation", ))? } else { self.translator - .message_with_fallback(lang, key) + .message_with_fallback(self.lang.clone(), key) .ok_or(Error::new( ErrorKind::InvalidOperation, "Missing translation", diff --git a/templates/app.html b/templates/app.html index fad326e3..2d8a54ba 100644 --- a/templates/app.html +++ b/templates/app.html @@ -15,6 +15,7 @@ limitations under the License. #} {# Must be kept in sync with frontend/index.html #} +{% set _ = translator(lang) %} diff --git a/templates/base.html b/templates/base.html index b9f234a8..f2659ec1 100644 --- a/templates/base.html +++ b/templates/base.html @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. #} +{% set _ = translator(lang) %} + {% import "components/button.html" as button %} {% import "components/field.html" as field %} {% import "components/back_to_client.html" as back_to_client %}