My-Weekly-ToDo-List/src/lib/emailTemplates.ts
mARTin-B78 d3a40e32a0 fix: localise auth emails, repair verify link, recover from expired
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>
2026-05-01 20:09:33 +02:00

136 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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)];
}