You've already forked authentication-service
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:
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -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",
|
||||
|
@ -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?;
|
||||
|
@ -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?;
|
||||
|
@ -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());
|
||||
|
@ -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);
|
||||
|
@ -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)))
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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::{
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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" }
|
||||
|
@ -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?;
|
||||
|
||||
|
@ -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> {
|
||||
|
@ -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" }
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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 />
|
||||
|
@ -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) }}
|
||||
|
@ -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) }}
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user