1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-29 22:01:14 +03:00

templates: translate a lot more stuff

This commit is contained in:
Quentin Gliech
2023-10-05 17:20:02 +02:00
parent 6ff549f5df
commit b2cd8d83f7
32 changed files with 385 additions and 97 deletions

2
Cargo.lock generated
View File

@ -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",

View File

@ -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<EmailVerificationContext>,
) -> Result<Message, Error> {
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<EmailVerificationContext>,
) -> Result<(), Error> {
let message = self.prepare_verification_email(to, context)?;
self.transport.send(message).await?;

View File

@ -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?;

View File

@ -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<Templates>,
cookie_jar: CookieJar,
Path(id): Path<Ulid>,
@ -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());

View File

@ -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);

View File

@ -34,7 +34,6 @@ pub async fn get(
cookie_jar: CookieJar,
PreferredLanguage(locale): PreferredLanguage,
) -> Result<impl IntoResponse, FancyError> {
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)))
}

View File

@ -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()

View File

@ -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::{

View File

@ -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,

View File

@ -239,6 +239,7 @@ mod jobs {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct VerifyEmailJob {
user_email_id: Ulid,
language: Option<String>,
}
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 {

View File

@ -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" }

View File

@ -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?;

View File

@ -109,6 +109,21 @@ pub struct WithLanguage<T> {
inner: T,
}
impl<T> WithLanguage<T> {
/// Get the language of this context
pub fn language(&self) -> &str {
&self.lang
}
}
impl<T> std::ops::Deref for WithLanguage<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<T: TemplateContext> TemplateContext for WithLanguage<T> {
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
where
@ -984,6 +999,7 @@ pub struct ErrorContext {
code: Option<&'static str>,
description: Option<String>,
details: Option<String>,
lang: Option<String>,
}
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> {

View File

@ -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<EmailVerificationContext>) { "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<EmailVerificationContext>) { "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<EmailVerificationContext>) { "emails/verification.subject" }
/// Render the upstream link mismatch message
pub fn render_upstream_oauth2_link_mismatch(WithLanguage<WithCsrf<WithSession<UpstreamExistingLinkContext>>>) { "pages/upstream_oauth2/link_mismatch.html" }

View File

@ -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 %}

View File

@ -26,6 +26,7 @@ limitations under the License.
<input name="{{ name }}"
class="cpd-control"
{% if state.errors is not empty %} data-invalid {% endif %}
{% if state.value %} value="{{ state.value }}" {% endif %}
type="{{ type }}"
inputmode="{{ inputmode }}"
{% if required %} required {% endif %}
@ -40,11 +41,11 @@ limitations under the License.
{% if error.kind != "unspecified" %}
<div class="text-sm text-critical">
{% 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 %}

View File

@ -15,7 +15,6 @@ limitations under the License.
#}
{% macro top() %}
<!-- {{ lang }} -->
<nav class="container mx-auto py-2 flex-initial flex items-center px-8" role="navigation" aria-label="main navigation">
<div class="flex-1"></div>

View File

@ -12,8 +12,10 @@ distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
#}
-#}
Hello {{ user.username }},<br />
{%- set _ = translator(lang) -%}
{{ _("mas.emails.greeting", username=user.username) }}<br />
<br />
Your verification code to confirm this email address is: <strong>{{ verification.code }}</strong><br />
{{ _("mas.emails.verify.body_html", code=verification.code) }}<br />

View File

@ -12,6 +12,8 @@ distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
#}
-#}
Your email verification code is: {{ verification.code }}
{%- set _ = translator(lang) -%}
{{ _("mas.emails.verify.subject", code=verification.code) }}

View File

@ -12,9 +12,10 @@ distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
#}
-#}
{%- set _ = translator(lang) -%}
Hello {{ user.username }},
{{ _("mas.emails.greeting", username=user.username) }}
Your verification code to confirm this email address is: {{ verification.code }}
{{ _("mas.emails.verify.body_text", code=verification.code) }}

View File

@ -21,7 +21,7 @@ limitations under the License.
<section class="flex items-center justify-center flex-1">
<form method="POST" class="grid grid-cols-1 gap-6 w-96 my-2 mx-8">
<div class="text-center">
<h1 class="text-lg text-center font-medium">Add an email address</h1>
<h1 class="text-lg text-center font-medium">{{ _("mas.add_email.heading") }}</h1>
</div>
{% if form.errors is not empty %}
@ -33,7 +33,7 @@ limitations under the License.
{% endif %}
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ field.input(label="Email", name="email", type="email", form_state=form, autocomplete="email", required=true) }}
{{ button.button(text="Next") }}
{{ field.input(label=_("common.email_address"), name="email", type="email", form_state=form, autocomplete="email", required=true) }}
{{ button.button(text=_("action.continue")) }}
</section>
{% endblock content %}

View File

@ -21,8 +21,8 @@ limitations under the License.
<section class="flex items-center justify-center flex-1">
<form method="POST" class="grid grid-cols-1 gap-6 w-96 my-2 mx-8">
<div class="text-center">
<h1 class="text-lg text-center font-medium">Email verification</h1>
<p>Please enter the 6-digit code sent to: <span class="font-semibold">{{ email.email }}</span></p>
<h1 class="text-lg text-center font-medium">{{ _("mas.verify_email.headline") }}</h1>
<p>{{ _("mas.verify_email.description", email=email.email) }}</p>
</div>
{% if form.errors is not empty %}
@ -34,7 +34,8 @@ limitations under the License.
{% endif %}
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ field.input(label="Code", name="code", form_state=form, autocomplete="one-time-code", inputmode="numeric") }}
{{ button.button(text="Submit") }}
{{ field.input(label=_("mas.verify_email.code"), name="code", form_state=form, autocomplete="one-time-code", inputmode="numeric") }}
{{ button.button(text=_("action.submit")) }}
</form>
</section>
{% endblock content %}

View File

@ -20,12 +20,12 @@ limitations under the License.
{{ navbar.top() }}
<section class="container mx-auto grid gap-4 grid-cols-1 md:grid-cols-2 xl:grid-cols-3 py-2 px-8">
<form class="rounded border-2 border-grey-50 dark:border-grey-450 p-4 grid gap-4 xl:grid-cols-2 grid-cols-1 place-content-start" method="POST">
<h2 class="text-xl font-semibold xl:col-span-2">Change my password</h2>
<h2 class="text-xl font-semibold xl:col-span-2">{{ _("mas.change_password.heading") }}</h2>
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ field.input(label="Current password", name="current_password", type="password", autocomplete="current-password", class="xl:col-span-2") }}
{{ field.input(label="New password", name="new_password", type="password", autocomplete="new-password") }}
{{ field.input(label="Confirm password", name="new_password_confirm", type="password", autocomplete="new-password") }}
{{ button.button(text="Change password", type="submit", class="xl:col-span-2 place-self-end") }}
{{ field.input(label=_("mas.change_password.current"), name="current_password", type="password", autocomplete="current-password", class="xl:col-span-2") }}
{{ field.input(label=_("mas.change_password.new"), name="new_password", type="password", autocomplete="new-password") }}
{{ field.input(label=_("mas.change_password.confirm"), name="new_password_confirm", type="password", autocomplete="new-password") }}
{{ button.button(text=_("mas.change_password.change"), type="submit", class="xl:col-span-2 place-self-end") }}
</form>
</section>
{% endblock content %}

View File

@ -14,6 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
#}
{# Sometimes we don't have the language set, so we default to english #}
{% set lang = lang | default("en") %}
{% extends "base.html" %}
{% block content %}

View File

@ -22,13 +22,13 @@ limitations under the License.
{% if not password_disabled %}
{% if next and next.kind == "link_upstream" %}
<div class="text-center">
<h1 class="text-lg text-center font-medium">Sign in to link</h1>
<p class="text-sm">Linking your <span class="break-keep text-links">{{ next.provider.issuer }}</span> account</p>
<h1 class="text-lg text-center font-medium">{{ _("mas.login.link.headline") }}</h1>
<p class="text-sm">{{ _("mas.login.link.description", provider=next.provider.issuer) }}</p>
</div>
{% else %}
<div class="text-center">
<h1 class="text-lg text-center font-medium">Sign in</h1>
<p>Please sign in to continue:</p>
<h1 class="text-lg text-center font-medium">{{ _("mas.login.headline") }}</h1>
<p>{{ _("mas.login.description") }}</p>
</div>
{% endif %}
@ -41,30 +41,30 @@ limitations under the License.
{% endif %}
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ field.input(label="Username", name="username", form_state=form, autocomplete="username", autocorrect="off", autocapitalize="none") }}
{{ field.input(label="Password", name="password", type="password", form_state=form, autocomplete="password") }}
{{ field.input(label=_("common.username"), name="username", form_state=form, autocomplete="username", autocorrect="off", autocapitalize="none") }}
{{ field.input(label=_("common.password"), name="password", type="password", form_state=form, autocomplete="password") }}
{% if next and next.kind == "continue_authorization_grant" %}
<div class="grid grid-cols-2 gap-4">
{{ back_to_client.link(
text="Cancel",
text=_("action.cancel"),
kind="destructive",
uri=next.grant.redirect_uri,
mode=next.grant.response_mode,
params=dict(error="access_denied", state=next.grant.state)
) }}
{{ button.button(text="Next") }}
{{ button.button(text=_("action.continue")) }}
</div>
{% else %}
<div class="grid grid-cols-1 gap-4">
{{ button.button(text="Next") }}
{{ button.button(text=_("action.continue")) }}
</div>
{% endif %}
{% if not next or next.kind != "link_upstream" %}
<div class="text-center mt-4">
Don't have an account yet?
{{ _("mas.login.call_to_register") }}
{% set params = next["params"] | default({}) | to_params(prefix="?") %}
{{ button.link_text(text="Create an account", href="/register" ~ params) }}
{{ button.link_text(text=_("action.create_account"), href="/register" ~ params) }}
</div>
{% endif %}
{% endif %}
@ -73,20 +73,20 @@ limitations under the License.
{% if not password_disabled %}
<div class="flex items-center">
<hr class="flex-1" />
<div class="mx-2">Or</div>
<div class="mx-2">{{ _("mas.or_separator") }}</div>
<hr class="flex-1" />
</div>
{% endif %}
{% for provider in providers %}
{% set params = next["params"] | default({}) | to_params(prefix="?") %}
{{ button.link(text="Continue with " ~ provider.issuer, href="/upstream/authorize/" ~ provider.id ~ params) }}
{{ button.link(text=_("mas.login.continue_with_provider", provider=provider.issuer), href="/upstream/authorize/" ~ provider.id ~ params) }}
{% endfor %}
{% endif %}
{% if not providers and password_disabled %}
<div class="text-center">
No login method available.
{{ _("mas.login.no_login_methods") }}
</div>
{% endif %}
</form>

View File

@ -20,8 +20,8 @@ limitations under the License.
<section class="flex items-center justify-center flex-1">
<div class="w-96 my-2 mx-8">
<div class="grid grid-cols-1 gap-6">
<h1 class="text-xl font-semibold">The authorization request was denied the policy enforced by this service.</h1>
<p>This might be because of the client which authored the request, the currently logged in user, or the request itself.</p>
<h1 class="text-xl font-semibold">{{ _("mas.policy_violation.heading") }}</h1>
<p>{{ _("mas.policy_violation.description") }}</p>
<div class="rounded-lg bg-grey-25 dark:bg-grey-450 p-2 flex items-center">
<div class="bg-white rounded w-16 h-16 overflow-hidden mx-auto">
{% if client.logo_uri %}
@ -33,14 +33,14 @@ limitations under the License.
<div class="rounded-lg bg-grey-25 dark:bg-grey-450 p-2 flex items-center">
<div class="text-center flex-1">
Logged as <span class="font-semibold">{{ current_session.user.username }}</span>
{{ _("mas.policy_violation.logged_as", username=current_session.user.username) }}
</div>
{{ logout.button(text="Sign out", csrf_token=csrf_token, post_logout_action=action) }}
{{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token, post_logout_action=action) }}
</div>
{{ back_to_client.link(
text="Cancel",
text=_("action.cancel"),
kind="destructive",
uri=grant.redirect_uri,
mode=grant.response_mode,

View File

@ -26,7 +26,7 @@ limitations under the License.
</div>
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{# TODO: errors #}
{{ field.input(label="Password", name="password", type="password", form_state=form, autocomplete="password") }}
{{ field.input(label=_("common.password"), name="password", type="password", form_state=form, autocomplete="password") }}
{% if next and next.kind == "continue_authorization_grant" %}
<div class="grid grid-cols-2 gap-4">
{{ back_to_client.link(
@ -36,11 +36,11 @@ limitations under the License.
mode=next.grant.response_mode,
params=dict(error="access_denied", state=next.grant.state)
) }}
{{ button.button(text="Next") }}
{{ button.button(text=_("action.continue")) }}
</div>
{% else %}
<div class="grid grid-cols-1 gap-4">
{{ button.button(text="Next") }}
{{ button.button(text=_("action.continue")) }}
</div>
{% endif %}
</form>

View File

@ -54,7 +54,7 @@ limitations under the License.
</div>
{% endif %}
<div class="text-center mt-4">
{{ _("mas.register.already_have_account") }}
{{ _("mas.register.call_to_login") }}
{% set params = next["params"] | default({}) | to_params(prefix="?") %}
{{ button.link_text(text=_("mas.register.sign_in_instead"), href="/login" ~ params) }}
</div>

View File

@ -22,9 +22,9 @@ limitations under the License.
<form method="POST" class="grid grid-cols-1 gap-6">
<h1 class="rounded-lg bg-grey-25 dark:bg-grey-450 p-2 text-center font-medium text-lg">
{% if force_localpart %}
Create a new account
{{ _("mas.upstream_oauth2.register.create_account") }}
{% else %}
Choose your username
{{ _("mas.upstream_oauth2.register.choose_username") }}
{% endif %}
</h1>
@ -32,21 +32,21 @@ limitations under the License.
<input type="hidden" name="action" value="register" />
{% if force_localpart %}
<div class="rounded-lg bg-grey-25 dark:bg-grey-450 p-4">
<div class="font-medium">Will use the following username</div>
<div class="font-medium"> {{ _("mas.upstream_oauth2.register.forced_localpart") }}</div>
<div class="font-mono">{{ suggested_localpart }}</div>
</div>
{% else %}
{{ field.input(label="Username", name="username", autocomplete="username", autocorrect="off", autocapitalize="none") }}
{{ field.input(label=_("common.username"), name="username", autocomplete="username", autocorrect="off", autocapitalize="none") }}
{% endif %}
{% if suggested_email %}
<div class="rounded-lg bg-grey-25 dark:bg-grey-450 p-4">
<div class="font-medium">
{% if force_email %}
Will import the following email address
{{ _("mas.upstream_oauth2.register.forced_email") }}
{% else %}
<input type="checkbox" name="import_email" id="import_email" checked="checked" />
<label for="import_email">Import email address</label>
<label for="import_email">{{ _("mas.upstream_oauth2.register.suggested_email") }}</label>
{% endif %}
</div>
<div class="font-mono">{{ suggested_email }}</div>
@ -57,24 +57,24 @@ limitations under the License.
<div class="rounded-lg bg-grey-25 dark:bg-grey-450 p-4">
<div class="font-medium">
{% if force_display_name %}
Will import the following display name
{{ _("mas.upstream_oauth2.register.forced_display_name") }}
{% else %}
<input type="checkbox" name="import_display_name" id="import_display_name" checked="checked" />
<label for="import_display_name">Import display name</label>
<label for="import_display_name">{{ _("mas.upstream_oauth2.register.suggested_display_name") }}</label>
{% endif %}
</div>
<div class="font-mono">{{ suggested_display_name }}</div>
</div>
{% endif %}
{{ button.button(text="Create a new account") }}
{{ button.button(text=_("action.create_account")) }}
</form>
<div class="flex items-center">
<hr class="flex-1" />
<div class="mx-2">Or</div>
<div class="mx-2">{{ _("mas.or_separator") }}</div>
<hr class="flex-1" />
</div>
{{ button.link_outline(text="Link to an existing account", href=login_link) }}
{{ button.link_outline(text=_("mas.upstream_oauth2.register.link_existing"), href=login_link) }}
</div>
</section>
{% endblock content %}

View File

@ -21,10 +21,10 @@ limitations under the License.
<section class="flex items-center justify-center flex-1">
<div class="grid grid-cols-1 gap-6 w-96 my-2 mx-8">
<h1 class="rounded-lg bg-grey-25 dark:bg-grey-450 p-2 flex flex-col font-medium text-lg text-center">
This upstream account is already linked to another account.
{{ _("mas.upstream_oauth2.link_mismatch.heading") }}
</h1>
<div>{{ logout.button(text="Logout", csrf_token=csrf_token) }}</div>
<div>{{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token) }}</div>
</div>
</section>
{% endblock content %}

View File

@ -21,17 +21,23 @@ limitations under the License.
<section class="flex items-center justify-center flex-1">
<div class="grid grid-cols-1 gap-6 w-96 my-2 mx-8">
<h1 class="rounded-lg bg-grey-25 dark:bg-grey-450 p-2 flex flex-col font-medium text-lg text-center">
Link to your existing account
{{ _("mas.upstream_oauth2.suggest_link.heading") }}
</h1>
<form method="POST" class="flex">
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
<input type="hidden" name="action" value="link" />
{{ button.button(text="Link", class="flex-1") }}
{{ button.button(text=_("mas.upstream_oauth2.suggest_link.action"), class="flex-1") }}
</form>
<div>Or {{ logout.button(text="Logout", csrf_token=csrf_token, post_logout_action=post_logout_action, as_link=true) }}</div>
<div class="flex items-center">
<hr class="flex-1" />
<div class="mx-2">{{ _("mas.or_separator") }}</div>
<hr class="flex-1" />
</div>
{{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token, post_logout_action=post_logout_action) }}
</div>
</section>
{% endblock content %}

View File

@ -2,19 +2,27 @@
"action": {
"cancel": "Cancel",
"@cancel": {
"context": "pages/consent.html:63:13-31, pages/register.html:43:17-35"
"context": "pages/consent.html:63:13-31, pages/login.html:49:19-37, pages/policy_violation.html:43:15-33, pages/register.html:43:17-35"
},
"continue": "Continue",
"@continue": {
"context": "pages/consent.html:59:30-50, pages/register.html:49:32-52, pages/register.html:53:32-52, pages/sso.html:42:30-50"
"context": "pages/account/emails/add.html:37:28-48, pages/consent.html:59:30-50, pages/login.html:55:34-54, pages/login.html:59:34-54, pages/reauth.html:39:34-54, pages/reauth.html:43:34-54, pages/register.html:49:32-52, pages/register.html:53:32-52, pages/sso.html:42:30-50"
},
"create_account": "Create Account",
"@create_account": {
"context": "pages/login.html:67:37-63, pages/upstream_oauth2/do_register.html:70:30-56"
},
"sign_in": "Sign in",
"@sign_in": {
"context": "components/navbar.html:32:28-47"
"context": "components/navbar.html:30:28-47"
},
"sign_out": "Sign out",
"@sign_out": {
"context": "components/navbar.html:30:30-50, pages/consent.html:72:30-50, pages/sso.html:47:30-50"
"context": "components/navbar.html:28:30-50, pages/consent.html:72:30-50, pages/policy_violation.html:39:32-52, pages/sso.html:47:30-50, pages/upstream_oauth2/link_mismatch.html:27:33-53, pages/upstream_oauth2/suggest_link.html:40:28-48"
},
"submit": "Submit",
"@submit": {
"context": "pages/account/emails/verify.html:38:28-46"
}
},
"app": {
@ -25,7 +33,7 @@
},
"name": "matrix-authentication-service",
"@name": {
"context": "app.html:24:14-27, base.html:30:31-44",
"context": "app.html:25:14-27, base.html:32:31-44",
"description": "Name of the application"
},
"technical_description": "OpenID Connect discovery document: <a class=\"cpd-link\" data-kind=\"primary\" href=\"%(discovery_url)s\">%(discovery_url)s</a>",
@ -37,11 +45,11 @@
"common": {
"email_address": "Email address",
"@email_address": {
"context": "pages/register.html:36:27-52"
"context": "pages/account/emails/add.html:36:27-52, pages/register.html:36:27-52"
},
"password": "Password",
"@password": {
"context": "pages/register.html:37:27-47"
"context": "pages/login.html:45:29-49, pages/reauth.html:29:29-49, pages/register.html:37:27-47"
},
"password_confirm": "Confirm password",
"@password_confirm": {
@ -49,33 +57,146 @@
},
"username": "Username",
"@username": {
"context": "pages/register.html:35:27-47"
"context": "pages/login.html:44:29-49, pages/register.html:35:27-47, pages/upstream_oauth2/do_register.html:39:31-51"
}
},
"error": {
"unexpected": "Unexpected error",
"@unexpected": {
"context": "pages/error.html:22:41-62",
"context": "pages/error.html:25:41-62",
"description": "Error message displayed when an unexpected error occurs"
}
},
"mas": {
"add_email": {
"heading": "Add an email address",
"@heading": {
"context": "pages/account/emails/add.html:24:55-81",
"description": "Heading for the page to add an email address"
}
},
"back_to_homepage": "Go back to the homepage",
"@back_to_homepage": {
"context": "pages/404.html:25:64-89"
},
"change_password": {
"change": "Change password",
"@change": {
"context": "pages/account/password.html:28:28-59",
"description": "Button to change the user's password"
},
"confirm": "Confirm password",
"@confirm": {
"context": "pages/account/password.html:27:27-59",
"description": "Confirmation field for the new password"
},
"current": "Current password",
"@current": {
"context": "pages/account/password.html:25:27-59",
"description": "Field for the user's current password"
},
"heading": "Change my password",
"@heading": {
"context": "pages/account/password.html:23:57-89",
"description": "Heading on the change password page"
},
"new": "New password",
"@new": {
"context": "pages/account/password.html:26:27-55",
"description": "Field for the user's new password"
}
},
"emails": {
"greeting": "Hello %(username)s,",
"@greeting": {
"context": "emails/verification.html:19:3-51, emails/verification.txt:19:3-51",
"description": "Greeting at the top of emails sent to the user"
},
"verify": {
"body_html": "Your verification code to confirm this email address is: <strong>%(code)s</strong>",
"@body_html": {
"context": "emails/verification.html:21:3-59",
"description": "The body of the email sent to verify an email address (HTML)"
},
"body_text": "Your verification code to confirm this email address is: %(code)s",
"@body_text": {
"context": "emails/verification.txt:21:3-59",
"description": "The body of the email sent to verify an email address (text)"
},
"subject": "Your email verification code is: %(code)s",
"@subject": {
"context": "emails/verification.subject:19:3-57",
"description": "The subject line of the email sent to verify an email address"
}
}
},
"errors": {
"denied_policy": "Denied by policy: %(policy)s",
"@denied_policy": {
"context": "components/field.html:48:17-69"
},
"field_required": "This field is required",
"@field_required": {
"context": "components/field.html:44:17-47"
},
"invalid_credentials": "Invalid credentials",
"@invalid_credentials": {
"context": "components/errors.html:19:7-42"
},
"password_mismatch": "Password fields don't match",
"@password_mismatch": {
"context": "components/errors.html:21:7-40"
},
"username_taken": "This username is already taken",
"@username_taken": {
"context": "components/field.html:46:17-47"
}
},
"login": {
"call_to_register": "Don't have an account yet?",
"@call_to_register": {
"context": "pages/login.html:65:15-46"
},
"continue_with_provider": "Continue with %(provider)s",
"@continue_with_provider": {
"context": "pages/login.html:83:30-93",
"description": "Button to log in with an upstream provider"
},
"description": "Please sign in to continue:",
"@description": {
"context": "pages/login.html:31:18-44"
},
"headline": "Sign in",
"@headline": {
"context": "pages/login.html:30:59-82"
},
"link": {
"description": "Linking your <span class=\"break-keep text-links\">%(provider)s</span> account",
"@description": {
"context": "pages/login.html:26:34-96"
},
"headline": "Sign in to link",
"@headline": {
"context": "pages/login.html:25:59-87"
}
},
"no_login_methods": "No login methods available.",
"@no_login_methods": {
"context": "pages/login.html:89:13-44"
}
},
"navbar": {
"my_account": "My account",
"@my_account": {
"context": "components/navbar.html:29:28-54"
"context": "components/navbar.html:27:28-54"
},
"register": "Create an account",
"@register": {
"context": "components/navbar.html:33:36-60"
"context": "components/navbar.html:31:36-60"
},
"signed_in_as": "Signed in as <span class=\"font-semibold\">%(username)s</span>.",
"@signed_in_as": {
"context": "components/navbar.html:25:13-81",
"context": "components/navbar.html:24:13-81",
"description": "Displayed in the navbar when the user is signed in"
}
},
@ -94,10 +215,32 @@
"context": "pages/consent.html:71:11-67, pages/sso.html:46:11-67",
"description": "Suggestions for the user to log in as a different user"
},
"or_separator": "Or",
"@or_separator": {
"context": "pages/login.html:76:33-54, pages/upstream_oauth2/do_register.html:74:29-50, pages/upstream_oauth2/suggest_link.html:36:29-50",
"description": "Separator between the login methods"
},
"policy_violation": {
"description": "This might be because of the client which authored the request, the currently logged in user, or the request itself.",
"@description": {
"context": "pages/policy_violation.html:24:14-51",
"description": "Displayed when an authorization request is denied by the policy"
},
"heading": "The authorization request was denied the policy enforced by this service",
"@heading": {
"context": "pages/policy_violation.html:23:45-78",
"description": "Displayed when an authorization request is denied by the policy"
},
"logged_as": "Logged as <span class=\"font-semibold\">%(username)s</span>",
"@logged_as": {
"context": "pages/policy_violation.html:36:15-90"
}
},
"register": {
"already_have_account": "Already have an account?",
"@already_have_account": {
"context": "pages/register.html:57:11-49"
"call_to_login": "Already have an account?",
"@call_to_login": {
"context": "pages/register.html:57:11-42",
"description": "Displayed on the registration page to suggest to log in instead"
},
"create_account": {
"description": "Please create an account to get started:",
@ -149,6 +292,81 @@
"context": "components/scope.html:21:43-70",
"description": "Displayed when the 'openid' scope is requested"
}
},
"upstream_oauth2": {
"link_mismatch": {
"heading": "This upstream account is already linked to another account.",
"@heading": {
"context": "pages/upstream_oauth2/link_mismatch.html:24:11-57",
"description": "Page shown when the user tries to link an upstream account that is already linked to another account"
}
},
"register": {
"choose_username": "Choose your username",
"@choose_username": {
"context": "pages/upstream_oauth2/do_register.html:27:15-64",
"description": "Displayed when creating a new account from an SSO login, and the username is not forced"
},
"create_account": "Create a new account",
"@create_account": {
"context": "pages/upstream_oauth2/do_register.html:25:15-63",
"description": "Displayed when creating a new account from an SSO login, and the username is pre-filled and forced"
},
"forced_display_name": "Will use the following display name",
"@forced_display_name": {
"context": "pages/upstream_oauth2/do_register.html:60:19-72",
"description": "Tells the user what display name will be imported"
},
"forced_email": "Will use the following email address",
"@forced_email": {
"context": "pages/upstream_oauth2/do_register.html:46:19-65",
"description": "Tells the user which email address will be imported"
},
"forced_localpart": "Will use the following username",
"@forced_localpart": {
"context": "pages/upstream_oauth2/do_register.html:35:41-91",
"description": "Tells the user which username will be used"
},
"link_existing": "Link to an existing account",
"@link_existing": {
"context": "pages/upstream_oauth2/do_register.html:77:34-81",
"description": "Button to link an existing account after an SSO login"
},
"suggested_display_name": "Import display name",
"@suggested_display_name": {
"context": "pages/upstream_oauth2/do_register.html:63:50-106",
"description": "Option to let the user import their display name after an SSO login"
},
"suggested_email": "Import email address",
"@suggested_email": {
"context": "pages/upstream_oauth2/do_register.html:49:45-94",
"description": "Option to let the user import their email address after an SSO login"
}
},
"suggest_link": {
"action": "Link",
"@action": {
"context": "pages/upstream_oauth2/suggest_link.html:31:30-74"
},
"heading": "Link to your existing account",
"@heading": {
"context": "pages/upstream_oauth2/suggest_link.html:24:11-56"
}
}
},
"verify_email": {
"code": "Code",
"@code": {
"context": "pages/account/emails/verify.html:37:27-53"
},
"description": "Please enter the 6-digit code sent to: <span class=\"font-semibold\">%(email)s</span>",
"@description": {
"context": "pages/account/emails/verify.html:25:14-66"
},
"headline": "Email verification",
"@headline": {
"context": "pages/account/emails/verify.html:24:55-85"
}
}
}
}