From b2cd8d83f70930fd169d4e1501025b9a5f1cec4c Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 5 Oct 2023 17:20:02 +0200 Subject: [PATCH] templates: translate a lot more stuff --- Cargo.lock | 2 + crates/email/src/mailer.rs | 7 +- crates/graphql/src/mutations/user_email.rs | 2 + .../handlers/src/compat/login_sso_complete.rs | 7 +- .../handlers/src/views/account/emails/add.rs | 3 +- crates/handlers/src/views/index.rs | 3 - crates/handlers/src/views/register.rs | 2 +- crates/i18n/src/lib.rs | 1 + crates/router/src/endpoints.rs | 2 +- crates/storage/src/job.rs | 15 ++ crates/tasks/Cargo.toml | 2 + crates/tasks/src/email.rs | 12 +- crates/templates/src/context.rs | 23 ++ crates/templates/src/lib.rs | 8 +- templates/components/errors.html | 4 +- templates/components/field.html | 7 +- templates/components/navbar.html | 1 - templates/emails/verification.html | 8 +- templates/emails/verification.subject | 6 +- templates/emails/verification.txt | 7 +- templates/pages/account/emails/add.html | 6 +- templates/pages/account/emails/verify.html | 9 +- templates/pages/account/password.html | 10 +- templates/pages/error.html | 3 + templates/pages/login.html | 28 +- templates/pages/policy_violation.html | 10 +- templates/pages/reauth.html | 6 +- templates/pages/register.html | 2 +- .../pages/upstream_oauth2/do_register.html | 22 +- .../pages/upstream_oauth2/link_mismatch.html | 4 +- .../pages/upstream_oauth2/suggest_link.html | 12 +- translations/en.json | 248 ++++++++++++++++-- 32 files changed, 385 insertions(+), 97 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 73138201..e5a2c51d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3267,9 +3267,11 @@ dependencies = [ "futures-lite", "mas-data-model", "mas-email", + "mas-i18n", "mas-matrix", "mas-storage", "mas-storage-pg", + "mas-templates", "mas-tower", "opentelemetry", "rand 0.8.5", diff --git a/crates/email/src/mailer.rs b/crates/email/src/mailer.rs index 90bc2011..8e561678 100644 --- a/crates/email/src/mailer.rs +++ b/crates/email/src/mailer.rs @@ -18,7 +18,7 @@ use lettre::{ message::{Mailbox, MessageBuilder, MultiPart}, AsyncTransport, Message, }; -use mas_templates::{EmailVerificationContext, Templates}; +use mas_templates::{EmailVerificationContext, Templates, WithLanguage}; use thiserror::Error; use crate::MailTransport; @@ -66,7 +66,7 @@ impl Mailer { fn prepare_verification_email( &self, to: Mailbox, - context: &EmailVerificationContext, + context: &WithLanguage, ) -> Result { let plain = self.templates.render_email_verification_txt(context)?; @@ -95,6 +95,7 @@ impl Mailer { skip_all, fields( email.to = %to, + email.language = %context.language(), user.id = %context.user().id, user_email_verification.id = %context.verification().id, user_email_verification.code = context.verification().code, @@ -104,7 +105,7 @@ impl Mailer { pub async fn send_verification_email( &self, to: Mailbox, - context: &EmailVerificationContext, + context: &WithLanguage, ) -> Result<(), Error> { let message = self.prepare_verification_email(to, context)?; self.transport.send(message).await?; diff --git a/crates/graphql/src/mutations/user_email.rs b/crates/graphql/src/mutations/user_email.rs index 3f2f7557..02786c24 100644 --- a/crates/graphql/src/mutations/user_email.rs +++ b/crates/graphql/src/mutations/user_email.rs @@ -449,6 +449,7 @@ impl UserEmailMutations { .mark_as_verified(&state.clock(), user_email) .await?; } else { + // TODO: figure out the locale repo.job() .schedule_job(VerifyEmailJob::new(&user_email)) .await?; @@ -490,6 +491,7 @@ impl UserEmailMutations { // Schedule a job to verify the email address if needed let needs_verification = user_email.confirmed_at.is_none(); if needs_verification { + // TODO: figure out the locale repo.job() .schedule_job(VerifyEmailJob::new(&user_email)) .await?; diff --git a/crates/handlers/src/compat/login_sso_complete.rs b/crates/handlers/src/compat/login_sso_complete.rs index 916d2e00..9aeeb1e9 100644 --- a/crates/handlers/src/compat/login_sso_complete.rs +++ b/crates/handlers/src/compat/login_sso_complete.rs @@ -105,7 +105,8 @@ pub async fn get( if clock.now() > login.created_at + Duration::minutes(30) { let ctx = ErrorContext::new() .with_code("compat_sso_login_expired") - .with_description("This login session expired.".to_owned()); + .with_description("This login session expired.".to_owned()) + .with_language(&locale); let content = templates.render_error(&ctx)?; return Ok((cookie_jar, Html(content)).into_response()); @@ -131,6 +132,7 @@ pub async fn post( mut rng: BoxRng, clock: BoxClock, mut repo: BoxRepository, + PreferredLanguage(locale): PreferredLanguage, State(templates): State, cookie_jar: CookieJar, Path(id): Path, @@ -173,7 +175,8 @@ pub async fn post( if clock.now() > login.created_at + Duration::minutes(30) { let ctx = ErrorContext::new() .with_code("compat_sso_login_expired") - .with_description("This login session expired.".to_owned()); + .with_description("This login session expired.".to_owned()) + .with_language(&locale); let content = templates.render_error(&ctx)?; return Ok((cookie_jar, Html(content)).into_response()); diff --git a/crates/handlers/src/views/account/emails/add.rs b/crates/handlers/src/views/account/emails/add.rs index bc463717..0a8b7ab8 100644 --- a/crates/handlers/src/views/account/emails/add.rs +++ b/crates/handlers/src/views/account/emails/add.rs @@ -77,6 +77,7 @@ pub(crate) async fn post( mut rng: BoxRng, clock: BoxClock, mut repo: BoxRepository, + PreferredLanguage(locale): PreferredLanguage, mut policy: Policy, cookie_jar: CookieJar, activity_tracker: BoundActivityTracker, @@ -124,7 +125,7 @@ pub(crate) async fn post( // verify page let next = if user_email.confirmed_at.is_none() { repo.job() - .schedule_job(VerifyEmailJob::new(&user_email)) + .schedule_job(VerifyEmailJob::new(&user_email).with_language(locale.to_string())) .await?; let next = mas_router::AccountVerifyEmail::new(user_email.id); diff --git a/crates/handlers/src/views/index.rs b/crates/handlers/src/views/index.rs index 80b51419..1a06a039 100644 --- a/crates/handlers/src/views/index.rs +++ b/crates/handlers/src/views/index.rs @@ -34,7 +34,6 @@ pub async fn get( cookie_jar: CookieJar, PreferredLanguage(locale): PreferredLanguage, ) -> Result { - tracing::info!("{locale}"); let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng); let (session_info, cookie_jar) = cookie_jar.session_info(); let session = session_info.load_session(&mut repo).await?; @@ -52,7 +51,5 @@ pub async fn get( let content = templates.render_index(&ctx)?; - tracing::info!("rendered index page"); - Ok((cookie_jar, Html(content))) } diff --git a/crates/handlers/src/views/register.rs b/crates/handlers/src/views/register.rs index 3471e04e..2cc86f83 100644 --- a/crates/handlers/src/views/register.rs +++ b/crates/handlers/src/views/register.rs @@ -225,7 +225,7 @@ pub(crate) async fn post( .await?; repo.job() - .schedule_job(VerifyEmailJob::new(&user_email)) + .schedule_job(VerifyEmailJob::new(&user_email).with_language(locale.to_string())) .await?; repo.job() diff --git a/crates/i18n/src/lib.rs b/crates/i18n/src/lib.rs index 2aba9271..1ab7d124 100644 --- a/crates/i18n/src/lib.rs +++ b/crates/i18n/src/lib.rs @@ -19,6 +19,7 @@ pub mod sprintf; pub mod translations; mod translator; +pub use icu_locid::locale; pub use icu_provider::DataLocale; pub use self::{ diff --git a/crates/router/src/endpoints.rs b/crates/router/src/endpoints.rs index 904c145c..d7f22549 100644 --- a/crates/router/src/endpoints.rs +++ b/crates/router/src/endpoints.rs @@ -18,7 +18,7 @@ use ulid::Ulid; pub use crate::traits::*; #[derive(Deserialize, Serialize, Clone, Debug)] -#[serde(rename_all = "snake_case", tag = "next")] +#[serde(rename_all = "snake_case", tag = "kind")] pub enum PostAuthAction { ContinueAuthorizationGrant { id: Ulid, diff --git a/crates/storage/src/job.rs b/crates/storage/src/job.rs index 2d2f85fc..58e41326 100644 --- a/crates/storage/src/job.rs +++ b/crates/storage/src/job.rs @@ -239,6 +239,7 @@ mod jobs { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct VerifyEmailJob { user_email_id: Ulid, + language: Option, } impl VerifyEmailJob { @@ -247,9 +248,23 @@ mod jobs { pub fn new(user_email: &UserEmail) -> Self { Self { user_email_id: user_email.id, + language: None, } } + /// Set the language to use for the email. + #[must_use] + pub fn with_language(mut self, language: String) -> Self { + self.language = Some(language); + self + } + + /// The language to use for the email. + #[must_use] + pub fn language(&self) -> Option<&str> { + self.language.as_deref() + } + /// The ID of the email address to verify. #[must_use] pub fn user_email_id(&self) -> Ulid { diff --git a/crates/tasks/Cargo.toml b/crates/tasks/Cargo.toml index 1ca35834..7afe5019 100644 --- a/crates/tasks/Cargo.toml +++ b/crates/tasks/Cargo.toml @@ -32,7 +32,9 @@ serde_json.workspace = true mas-data-model = { path = "../data-model" } mas-email = { path = "../email" } +mas-i18n = { path = "../i18n" } mas-matrix = { path = "../matrix" } mas-storage = { path = "../storage" } mas-storage-pg = { path = "../storage-pg" } +mas-templates = { path = "../templates" } mas-tower = { path = "../tower" } diff --git a/crates/tasks/src/email.rs b/crates/tasks/src/email.rs index 32a9f5eb..99418fc0 100644 --- a/crates/tasks/src/email.rs +++ b/crates/tasks/src/email.rs @@ -15,8 +15,10 @@ use anyhow::Context; use apalis_core::{context::JobContext, executor::TokioExecutor, monitor::Monitor}; use chrono::Duration; -use mas_email::{Address, EmailVerificationContext, Mailbox}; +use mas_email::{Address, Mailbox}; +use mas_i18n::locale; use mas_storage::job::{JobWithSpanContext, VerifyEmailJob}; +use mas_templates::{EmailVerificationContext, TemplateContext}; use rand::{distributions::Uniform, Rng}; use tracing::info; @@ -38,6 +40,11 @@ async fn verify_email( let mailer = state.mailer(); let clock = state.clock(); + let language = job + .language() + .and_then(|l| l.parse().ok()) + .unwrap_or(locale!("en").into()); + // Lookup the user email let user_email = repo .user_email() @@ -68,7 +75,8 @@ async fn verify_email( // And send the verification email let mailbox = Mailbox::new(Some(user.username.clone()), address); - let context = EmailVerificationContext::new(user.clone(), verification.clone()); + let context = + EmailVerificationContext::new(user.clone(), verification.clone()).with_language(language); mailer.send_verification_email(mailbox, &context).await?; diff --git a/crates/templates/src/context.rs b/crates/templates/src/context.rs index 15a7fad7..7a74b937 100644 --- a/crates/templates/src/context.rs +++ b/crates/templates/src/context.rs @@ -109,6 +109,21 @@ pub struct WithLanguage { inner: T, } +impl WithLanguage { + /// Get the language of this context + pub fn language(&self) -> &str { + &self.lang + } +} + +impl std::ops::Deref for WithLanguage { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + impl TemplateContext for WithLanguage { fn sample(now: chrono::DateTime, rng: &mut impl Rng) -> Vec where @@ -984,6 +999,7 @@ pub struct ErrorContext { code: Option<&'static str>, description: Option, details: Option, + lang: Option, } impl std::fmt::Display for ErrorContext { @@ -1047,6 +1063,13 @@ impl ErrorContext { self } + /// Add the language to the context + #[must_use] + pub fn with_language(mut self, lang: &DataLocale) -> Self { + self.lang = Some(lang.to_string()); + self + } + /// Get the error code, if any #[must_use] pub fn code(&self) -> Option<&'static str> { diff --git a/crates/templates/src/lib.rs b/crates/templates/src/lib.rs index b1ed4cb6..5df20b83 100644 --- a/crates/templates/src/lib.rs +++ b/crates/templates/src/lib.rs @@ -29,7 +29,7 @@ use std::{collections::HashSet, sync::Arc}; use anyhow::Context as _; use arc_swap::ArcSwap; use camino::{Utf8Path, Utf8PathBuf}; -use mas_i18n::{Translator}; +use mas_i18n::Translator; use mas_router::UrlBuilder; use mas_spa::ViteManifest; use rand::Rng; @@ -346,13 +346,13 @@ register_templates! { pub fn render_error(ErrorContext) { "pages/error.html" } /// Render the email verification email (plain text variant) - pub fn render_email_verification_txt(EmailVerificationContext) { "emails/verification.txt" } + pub fn render_email_verification_txt(WithLanguage) { "emails/verification.txt" } /// Render the email verification email (HTML text variant) - pub fn render_email_verification_html(EmailVerificationContext) { "emails/verification.html" } + pub fn render_email_verification_html(WithLanguage) { "emails/verification.html" } /// Render the email verification subject - pub fn render_email_verification_subject(EmailVerificationContext) { "emails/verification.subject" } + pub fn render_email_verification_subject(WithLanguage) { "emails/verification.subject" } /// Render the upstream link mismatch message pub fn render_upstream_oauth2_link_mismatch(WithLanguage>>) { "pages/upstream_oauth2/link_mismatch.html" } diff --git a/templates/components/errors.html b/templates/components/errors.html index d398316d..d60723fc 100644 --- a/templates/components/errors.html +++ b/templates/components/errors.html @@ -16,9 +16,9 @@ limitations under the License. {% macro form_error_message(error) -%} {% if error.kind == "invalid_credentials" %} - Invalid credentials + {{ _("mas.errors.invalid_credentials") }} {% elif error.kind == "password_mismatch" %} - Password fields don't match + {{ _("mas.errors.password_mismatch") }} {% else %} {{ error.kind }} {% endif %} diff --git a/templates/components/field.html b/templates/components/field.html index 1a505b27..5dab0aed 100644 --- a/templates/components/field.html +++ b/templates/components/field.html @@ -26,6 +26,7 @@ limitations under the License. {% if error.kind == "required" %} - This field is required + {{ _("mas.errors.field_required") }} {% elif error.kind == "exists" and name == "username" %} - This username is already taken + {{ _("mas.errors.username_taken") }} {% elif error.kind == "policy" %} - Denied by policy: {{ error.message }} + {{ _("mas.errors.denied_policy", message=error.message) }} {% else %} {{ error.kind }} {% endif %} diff --git a/templates/components/navbar.html b/templates/components/navbar.html index 613488ef..9176a8f9 100644 --- a/templates/components/navbar.html +++ b/templates/components/navbar.html @@ -15,7 +15,6 @@ limitations under the License. #} {% macro top() %} -