fix(ui): localise the confirm screen, drop the brand on the error state
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m3s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m42s

The session-less /confirm page defaulted to English, so it showed the English
app title. Carry the recipient's language on the deeplink (?lang) and setLocale on
load so the page matches the email. Show a localised brand wordmark (Эрудит / Erudit,
matching the email) on the success state only; the invalid/expired state now shows
just the message, no header.
This commit is contained in:
Ilia Denisov
2026-07-03 05:54:28 +02:00
parent 356bf1a5ba
commit 54af644429
4 changed files with 24 additions and 19 deletions
+1
View File
@@ -4,6 +4,7 @@
export const en = {
'app.title': 'Scrabble',
'app.brand': 'Erudit',
'dict.loading': 'Loading…',
'connection.connecting': 'Connecting…',
+1
View File
@@ -5,6 +5,7 @@ import type { MessageKey } from './en';
export const ru: Record<MessageKey, string> = {
'app.title': 'Scrabble',
'app.brand': 'Эрудит',
'dict.loading': 'Загрузка…',
'connection.connecting': 'Подключение…',
+16 -14
View File
@@ -1,22 +1,27 @@
<script lang="ts">
import { onMount } from 'svelte';
import { confirmDeeplink } from '../lib/app.svelte';
import { t } from '../lib/i18n/index.svelte';
import { setLocale, t } from '../lib/i18n/index.svelte';
let { token }: { token: string } = $props();
// The token rides the URL fragment (#/confirm/<token>), which is never sent to the
// server, so a plain link prefetch cannot reach it — confirm as soon as the screen
// loads. The manual 6-digit code in the same email is the fallback if an aggressive
// scanner ever runs the page and consumes the single-use token first.
// busy → (a login navigates into the app) | linked | merge | error. The token rides
// the URL fragment (#/confirm/<token>), which is never sent to the server, so a plain
// link prefetch cannot reach it — confirm on load. The manual six-digit code in the
// same email is the fallback if an aggressive scanner runs the page first.
let stage = $state<'busy' | 'linked' | 'merge' | 'error'>('busy');
onMount(async () => {
// The email carries the recipient's language on the link (?lang), so this
// session-less page renders in it rather than the default.
const query = window.location.hash.split('?')[1] ?? '';
const lang = new URLSearchParams(query).get('lang');
if (lang === 'ru' || lang === 'en') setLocale(lang);
if (!token) {
stage = 'error';
return;
}
try {
// A login navigates into the app; a link stays here with a result.
const r = await confirmDeeplink(token);
stage = r === 'merge_required' ? 'merge' : 'linked';
} catch {
@@ -27,17 +32,14 @@
<main class="confirm">
<div class="card">
<div class="brand">{t('app.title')}</div>
{#if stage === 'busy'}
<p class="muted">{t('confirm.busy')}</p>
{:else if stage === 'linked'}
<p class="ok">{t('confirm.linked')}</p>
<p class="muted">{t('confirm.close')}</p>
{:else if stage === 'merge'}
<p class="ok">{t('confirm.merge')}</p>
<p class="muted">{t('confirm.close')}</p>
{:else}
{:else if stage === 'error'}
<p class="err">{t('confirm.expired')}</p>
{:else}
<div class="brand">{t('app.brand')}</div>
<p class="ok">{stage === 'merge' ? t('confirm.merge') : t('confirm.linked')}</p>
<p class="muted">{t('confirm.close')}</p>
{/if}
</div>
</main>