Wave-3 user feedback (points 1–3): - Email templates: extract de/en/fr/es/it copy into emailTemplates.ts and pick the user's language. Signup forwards navigator.language and the API also respects Accept-Language; resend/reset reuse user.language. - Verify link: the email button now points at the client page instead of the API GET handler, so corporate inbox scanners (M365 Safe Links etc.) can no longer pre-fetch and silently consume the one-shot token. The client page requires a real click before calling PATCH /api/auth/ verify-email. The legacy GET handler now just redirects to the client page so old in-flight emails keep working. - Code expired UX: when users land via an expired-link redirect we no longer leave them with a blank email field — the page exposes an email input so the resend button has something to act on. The verify-email redirect also forwards the email param. Misc: tsconfig migrated to moduleResolution=bundler and dropped downlevelIteration to clear the TS 7.0 deprecation errors. v1.101.0 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
136 lines
6.3 KiB
TypeScript
136 lines
6.3 KiB
TypeScript
// Localised email copy. Keep keys stable across languages so the template
|
||
// builders can swap them by language code without dynamic imports.
|
||
|
||
type Lang = "en" | "de" | "fr" | "es" | "it";
|
||
|
||
interface VerificationCopy {
|
||
subject: (code: string) => string;
|
||
welcome: string;
|
||
yourCode: string;
|
||
orClick: string;
|
||
button: string;
|
||
expiresNote: string;
|
||
footer: string;
|
||
}
|
||
|
||
interface ResetCopy {
|
||
subject: string;
|
||
intro: string;
|
||
button: string;
|
||
expiresNote: string;
|
||
footer: string;
|
||
}
|
||
|
||
const VERIFICATION: Record<Lang, VerificationCopy> = {
|
||
en: {
|
||
subject: (c) => `${c} – Verify your email for My Weekly ToDo's`,
|
||
welcome: "Welcome! Please verify your email address to complete your registration.",
|
||
yourCode: "Your verification code",
|
||
orClick: "Or click the button below to verify instantly:",
|
||
button: "✓ Verify Email",
|
||
expiresNote: "This code expires in 15 minutes. If you didn't create an account, you can safely ignore this email.",
|
||
footer: "Simple. Beautiful. Yours.",
|
||
},
|
||
de: {
|
||
subject: (c) => `${c} – Bestätige deine E-Mail-Adresse für My Weekly ToDo's`,
|
||
welcome: "Willkommen! Bitte bestätige deine E-Mail-Adresse, um deine Registrierung abzuschließen.",
|
||
yourCode: "Dein Bestätigungscode",
|
||
orClick: "Oder klicke unten, um sofort zu bestätigen:",
|
||
button: "✓ E-Mail bestätigen",
|
||
expiresNote: "Dieser Code läuft in 15 Minuten ab. Falls du diesen Account nicht erstellt hast, kannst du diese E-Mail einfach ignorieren.",
|
||
footer: "Einfach. Schön. Deins.",
|
||
},
|
||
fr: {
|
||
subject: (c) => `${c} – Vérifiez votre adresse e-mail pour My Weekly ToDo's`,
|
||
welcome: "Bienvenue ! Veuillez vérifier votre adresse e-mail pour terminer votre inscription.",
|
||
yourCode: "Votre code de vérification",
|
||
orClick: "Ou cliquez sur le bouton ci-dessous pour vérifier instantanément :",
|
||
button: "✓ Vérifier l'e-mail",
|
||
expiresNote: "Ce code expire dans 15 minutes. Si vous n'avez pas créé de compte, vous pouvez ignorer cet e-mail.",
|
||
footer: "Simple. Beau. À vous.",
|
||
},
|
||
es: {
|
||
subject: (c) => `${c} – Verifica tu correo para My Weekly ToDo's`,
|
||
welcome: "¡Bienvenido! Por favor verifica tu dirección de correo para completar tu registro.",
|
||
yourCode: "Tu código de verificación",
|
||
orClick: "O haz clic en el botón de abajo para verificar al instante:",
|
||
button: "✓ Verificar correo",
|
||
expiresNote: "Este código expira en 15 minutos. Si no creaste una cuenta, puedes ignorar este correo.",
|
||
footer: "Simple. Hermoso. Tuyo.",
|
||
},
|
||
it: {
|
||
subject: (c) => `${c} – Verifica la tua email per My Weekly ToDo's`,
|
||
welcome: "Benvenuto! Verifica il tuo indirizzo email per completare la registrazione.",
|
||
yourCode: "Il tuo codice di verifica",
|
||
orClick: "Oppure clicca sul pulsante qui sotto per verificare subito:",
|
||
button: "✓ Verifica email",
|
||
expiresNote: "Questo codice scade tra 15 minuti. Se non hai creato un account, puoi ignorare questa email.",
|
||
footer: "Semplice. Bello. Tuo.",
|
||
},
|
||
};
|
||
|
||
const RESET: Record<Lang, ResetCopy> = {
|
||
en: {
|
||
subject: "Password Reset – My Weekly ToDo's",
|
||
intro: "We received a request to reset your password. Click the button below to choose a new one.",
|
||
button: "Reset Password",
|
||
expiresNote: "This link expires in 1 hour. If you didn't request a password reset, you can safely ignore this email.",
|
||
footer: "Simple. Beautiful. Yours.",
|
||
},
|
||
de: {
|
||
subject: "Passwort zurücksetzen – My Weekly ToDo's",
|
||
intro: "Wir haben eine Anfrage zum Zurücksetzen deines Passworts erhalten. Klicke auf den Button unten, um ein neues Passwort zu wählen.",
|
||
button: "Passwort zurücksetzen",
|
||
expiresNote: "Dieser Link läuft in 1 Stunde ab. Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail einfach ignorieren.",
|
||
footer: "Einfach. Schön. Deins.",
|
||
},
|
||
fr: {
|
||
subject: "Réinitialisation du mot de passe – My Weekly ToDo's",
|
||
intro: "Nous avons reçu une demande de réinitialisation de votre mot de passe. Cliquez sur le bouton ci-dessous pour en choisir un nouveau.",
|
||
button: "Réinitialiser le mot de passe",
|
||
expiresNote: "Ce lien expire dans 1 heure. Si vous n'avez pas demandé de réinitialisation, vous pouvez ignorer cet e-mail.",
|
||
footer: "Simple. Beau. À vous.",
|
||
},
|
||
es: {
|
||
subject: "Restablecimiento de contraseña – My Weekly ToDo's",
|
||
intro: "Recibimos una solicitud para restablecer tu contraseña. Haz clic en el botón de abajo para elegir una nueva.",
|
||
button: "Restablecer contraseña",
|
||
expiresNote: "Este enlace expira en 1 hora. Si no solicitaste un restablecimiento, puedes ignorar este correo.",
|
||
footer: "Simple. Hermoso. Tuyo.",
|
||
},
|
||
it: {
|
||
subject: "Reimpostazione password – My Weekly ToDo's",
|
||
intro: "Abbiamo ricevuto una richiesta di reimpostazione della password. Clicca sul pulsante qui sotto per sceglierne una nuova.",
|
||
button: "Reimposta password",
|
||
expiresNote: "Questo link scade tra 1 ora. Se non hai richiesto la reimpostazione, puoi ignorare questa email.",
|
||
footer: "Semplice. Bello. Tuo.",
|
||
},
|
||
};
|
||
|
||
function pickLang(input?: string | null): Lang {
|
||
if (!input) return "en";
|
||
const code = input.toLowerCase().slice(0, 2);
|
||
if (code === "de" || code === "fr" || code === "es" || code === "it" || code === "en") return code;
|
||
return "en";
|
||
}
|
||
|
||
// Parse the first acceptable locale out of a raw Accept-Language header so we
|
||
// can pre-fill a user's UI language at signup.
|
||
export function languageFromAcceptLanguage(header?: string | null): Lang {
|
||
if (!header) return "en";
|
||
const tags = header.split(",").map((t) => t.trim().split(";")[0]);
|
||
for (const t of tags) {
|
||
const code = t.toLowerCase().slice(0, 2);
|
||
if (code === "de" || code === "fr" || code === "es" || code === "it" || code === "en") return code;
|
||
}
|
||
return "en";
|
||
}
|
||
|
||
export function getVerificationCopy(language?: string | null): VerificationCopy {
|
||
return VERIFICATION[pickLang(language)];
|
||
}
|
||
|
||
export function getResetCopy(language?: string | null): ResetCopy {
|
||
return RESET[pickLang(language)];
|
||
}
|