Sending Auth emails in the user's language
SupaHost has one template per Auth email type, with no per-language versions. Three workarounds: bilingual text, localized landing pages, or sending your own sign-in emails with generateLink.
There's one template per email type (Confirm sign-up, Magic link, Reset password, Invite, Change email), and templates can't switch language based on the user. The fields accept plain text and four placeholders: {{ .Email }}, {{ .NewEmail }}, {{ .Token }} and {{ .SiteURL }}. There's no placeholder for user metadata and no conditional logic. You can still give users a localized experience in one of three ways.
Option 1: write each template in two languages
If most users speak one of two or three languages, put short versions of each in the same email. Dashboard → your project → Auth tab → Auth emails, choose the template, and write something like:
Subject: Your sign-in link / Votre lien de connexion
Message:
Use the button below to sign in to Bookly. The link works once.
Utilisez le bouton ci-dessous pour vous connecter à Bookly. Le lien ne fonctionne qu'une fois.
Keep it short; the Subject is limited to 150 characters and the Button text to 40.
Option 2: send users to a page in their language
The email stays the same, but where the link lands can follow the user's language. Pass a localized redirect when you start the flow:
const locale = navigator.language.startsWith("fr") ? "fr" : "en";
await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: `https://bookings.example.com/${locale}/auth/callback`,
data: { locale }, // saved in user_metadata if this creates the user
},
});
Add https://bookings.example.com/** to Redirect URLs so every locale path is allowed. Storing locale in user metadata lets your app, and any emails you send yourself, use the same language later.
Option 3: send the email yourself
For fully translated emails, generate the sign-in link on the server and send it through your own email provider. auth.admin.generateLink() returns the link and one-time code without sending anything. This Edge Function sends a magic link in English or French:
import { createClient } from "npm:@supabase/supabase-js@2";
const admin = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!, {
auth: { autoRefreshToken: false, persistSession: false },
});
const copy = {
en: { subject: "Your Bookly sign-in link", body: "Click to sign in:" },
fr: { subject: "Votre lien de connexion Bookly", body: "Cliquez pour vous connecter :" },
};
Deno.serve(async (req) => {
const { email, locale } = await req.json();
const lang = locale === "fr" ? "fr" : "en";
const t = copy[lang];
const { data, error } = await admin.auth.admin.generateLink({
type: "magiclink",
email,
options: { redirectTo: `https://bookings.example.com/${lang}/auth/callback` },
});
if (error) return Response.json({ error: "Could not send link" }, { status: 400 });
await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${Deno.env.get("RESEND_API_KEY")}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "Bookly <[email protected]>",
to: email,
subject: t.subject,
html: `<p>${t.body}</p><p><a href="${data.properties.action_link}">${data.properties.action_link}</a></p>
<p>Code: <b>${data.properties.email_otp}</b></p>`,
}),
});
return Response.json({ sent: true });
});
Things to get right:
- Never return the link or code in the response. That would let anyone sign in as any address.
- Add your own rate limiting, per address and per IP. Auth's email send limit doesn't apply, because Auth isn't sending.
- Use it for people who already have an account. For new sign-ups, create the user first with
admin.createUser(), or keep using Auth's Confirm sign-up email. - Store
RESEND_API_KEYon the Functions tab's Secrets card. Any provider with an HTTP API works the same way.
On SupaHost
Auth's own emails always come from [email protected] with your App name as the sender name, and custom SMTP isn't available. Option 3 is also how to send sign-in emails from your own domain.