Merge pull request #312 from open-reception/fix/emails-always-in-proper-language

Fix: E-Mails are always using recipient locale now
This commit is contained in:
Karl Ludwig Weise
2026-07-22 11:55:50 +02:00
committed by GitHub
12 changed files with 106 additions and 136 deletions
+5 -10
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import { m } from "$i18n/messages";
import { setLocale } from "$i18n/runtime";
import type { SupportedLocale } from "$lib/const/locales";
import type { SelectTenant } from "$lib/server/db/central-schema";
import { type SelectAppointment } from "$lib/server/db/tenant-schema";
@@ -34,16 +33,12 @@
};
cancelUrl: string;
} = $props();
$effect(() => {
setLocale(locale);
});
</script>
<EmailLayout>
<EmailText variant="md">{m["emails.greeting"]({ name: user.email })}</EmailText>
<EmailLayout {locale}>
<EmailText variant="md">{m["emails.greeting"]({ name: user.email }, { locale })}</EmailText>
<EmailText variant="md">
{m["emails.appointmentBooked.introduction"]()}
{m["emails.appointmentBooked.introduction"]({}, { locale })}
</EmailText>
<EmailHeadline>{channel}</EmailHeadline>
<EmailText variant="md">
@@ -59,8 +54,8 @@
{address.zip}
{address.city}
</EmailText>
<EmailButton href={cancelUrl}>{m["emails.appointmentBooked.action"]()}</EmailButton>
<EmailButton href={cancelUrl}>{m["emails.appointmentBooked.action"]({}, { locale })}</EmailButton>
<EmailText variant="md" color="text-light">
{m["emails.appointmentBooked.reason"]()}
{m["emails.appointmentBooked.reason"]({}, { locale })}
</EmailText>
</EmailLayout>
+4 -9
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import { m } from "$i18n/messages";
import { setLocale } from "$i18n/runtime";
import type { SupportedLocale } from "$lib/const/locales";
import type { SelectTenant } from "$lib/server/db/central-schema";
import { type SelectAppointment } from "$lib/server/db/tenant-schema";
@@ -31,16 +30,12 @@
city: string;
};
} = $props();
$effect(() => {
setLocale(locale);
});
</script>
<EmailLayout>
<EmailText variant="md">{m["emails.greeting"]({ name: user.email })}</EmailText>
<EmailLayout {locale}>
<EmailText variant="md">{m["emails.greeting"]({ name: user.email }, { locale })}</EmailText>
<EmailText variant="md">
{m["emails.appointmentCancelled.introduction"]()}
{m["emails.appointmentCancelled.introduction"]({}, { locale })}
</EmailText>
<EmailHeadline>{channel}</EmailHeadline>
<EmailText variant="md">
@@ -57,6 +52,6 @@
{address.city}
</EmailText>
<EmailText variant="md" color="text-light">
{m["emails.appointmentCancelled.reason"]()}
{m["emails.appointmentCancelled.reason"]({}, { locale })}
</EmailText>
</EmailLayout>
+4 -9
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import { m } from "$i18n/messages";
import { setLocale } from "$i18n/runtime";
import type { SupportedLocale } from "$lib/const/locales";
import type { SelectTenant } from "$lib/server/db/central-schema";
import { type SelectAppointment } from "$lib/server/db/tenant-schema";
@@ -31,16 +30,12 @@
city: string;
};
} = $props();
$effect(() => {
setLocale(locale);
});
</script>
<EmailLayout>
<EmailText variant="md">{m["emails.greeting"]({ name: user.email })}</EmailText>
<EmailLayout {locale}>
<EmailText variant="md">{m["emails.greeting"]({ name: user.email }, { locale })}</EmailText>
<EmailText variant="md">
{m["emails.appointmentRejected.introduction"]()}
{m["emails.appointmentRejected.introduction"]({}, { locale })}
</EmailText>
<EmailHeadline>{channel}</EmailHeadline>
<EmailText variant="md">
@@ -57,6 +52,6 @@
{address.city}
</EmailText>
<EmailText variant="md" color="text-light">
{m["emails.appointmentRejected.reason"]()}
{m["emails.appointmentRejected.reason"]({}, { locale })}
</EmailText>
</EmailLayout>
+7 -10
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import { m } from "$i18n/messages";
import { setLocale } from "$i18n/runtime";
import type { SupportedLocale } from "$lib/const/locales";
import type { SelectTenant } from "$lib/server/db/central-schema";
import { type SelectAppointment } from "$lib/server/db/tenant-schema";
@@ -34,16 +33,12 @@
};
cancelUrl: string;
} = $props();
$effect(() => {
setLocale(locale);
});
</script>
<EmailLayout>
<EmailText variant="md">{m["emails.greeting"]({ name: user.email })}</EmailText>
<EmailLayout {locale}>
<EmailText variant="md">{m["emails.greeting"]({ name: user.email }, { locale })}</EmailText>
<EmailText variant="md">
{m["emails.appointmentReminder.introduction"]({ tenant: tenant.longName })}
{m["emails.appointmentReminder.introduction"]({ tenant: tenant.longName }, { locale })}
</EmailText>
<EmailHeadline>{channel}</EmailHeadline>
<EmailText variant="md">
@@ -59,8 +54,10 @@
{address.zip}
{address.city}
</EmailText>
<EmailButton href={cancelUrl}>{m["emails.appointmentReminder.action"]()}</EmailButton>
<EmailButton href={cancelUrl}>
{m["emails.appointmentReminder.action"]({}, { locale })}
</EmailButton>
<EmailText variant="md" color="text-light">
{m["emails.appointmentReminder.reason"]()}
{m["emails.appointmentReminder.reason"]({}, { locale })}
</EmailText>
</EmailLayout>
+4 -4
View File
@@ -37,10 +37,10 @@
});
</script>
<EmailLayout>
<EmailText variant="md">{m["emails.greeting"]({ name: user.email })}</EmailText>
<EmailLayout {locale}>
<EmailText variant="md">{m["emails.greeting"]({ name: user.email }, { locale })}</EmailText>
<EmailText variant="md">
{m["emails.appointmentRequest.introduction"]()}
{m["emails.appointmentRequest.introduction"]({}, { locale })}
</EmailText>
<EmailHeadline>{channel}</EmailHeadline>
<EmailText variant="md">
@@ -57,6 +57,6 @@
{address.city}
</EmailText>
<EmailText variant="md" color="text-light">
{m["emails.appointmentRequest.reason"]()}
{m["emails.appointmentRequest.reason"]({}, { locale })}
</EmailText>
</EmailLayout>
+10 -11
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import { m } from "$i18n/messages";
import { setLocale } from "$i18n/runtime";
import type { SupportedLocale } from "$lib/const/locales";
import type { SelectUserEmail } from "$lib/server/email/email-service";
import EmailButton from "./components/EmailButton.svelte";
@@ -18,22 +17,22 @@
confirmUrl: string;
expirationMinutes: number;
} = $props();
$effect(() => {
setLocale(locale);
});
</script>
<EmailLayout>
<EmailText variant="md">{m["emails.greeting"]({ name: user.name })}</EmailText>
<EmailLayout {locale}>
<EmailText variant="md">
{m["emails.confirmation.introduction"]()}
{m["emails.greeting"]({ name: user.name }, { locale })}
</EmailText>
<EmailButton href={confirmUrl}>{m["emails.confirmation.action"]()}</EmailButton>
<EmailText variant="md">
{m["emails.confirmation.hint"]({ expirationMinutes })}
{m["emails.confirmation.introduction"]({}, { locale })}
</EmailText>
<EmailButton {locale} href={confirmUrl}>
{m["emails.confirmation.action"]({}, { locale })}
</EmailButton>
<EmailText variant="md">
{m["emails.confirmation.hint"]({ expirationMinutes }, { locale })}
</EmailText>
<EmailText variant="md" color="text-light">
{m["emails.confirmation.reason"]()}
{m["emails.confirmation.reason"]({}, { locale })}
</EmailText>
</EmailLayout>
+5 -10
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import { m } from "$i18n/messages";
import { setLocale } from "$i18n/runtime";
import type { SupportedLocale } from "$lib/const/locales";
import type { SelectUserEmail } from "$lib/server/email/email-service";
import EmailButton from "./components/EmailButton.svelte";
@@ -16,19 +15,15 @@
user: SelectUserEmail;
dashboardUrl: string;
} = $props();
$effect(() => {
setLocale(locale);
});
</script>
<EmailLayout>
<EmailText variant="md">{m["emails.greeting"]({ name: user.name })}</EmailText>
<EmailLayout {locale}>
<EmailText variant="md">{m["emails.greeting"]({ name: user.name }, { locale })}</EmailText>
<EmailText variant="md">
{m["emails.notification.introduction"]()}
{m["emails.notification.introduction"]({}, { locale })}
</EmailText>
<EmailButton href={dashboardUrl}>{m["emails.notification.action"]()}</EmailButton>
<EmailButton href={dashboardUrl}>{m["emails.notification.action"]({}, { locale })}</EmailButton>
<EmailText variant="md" color="text-light">
{m["emails.notification.reason"]()}
{m["emails.notification.reason"]({}, { locale })}
</EmailText>
</EmailLayout>
+5 -10
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import { m } from "$i18n/messages";
import { setLocale } from "$i18n/runtime";
import type { SupportedLocale } from "$lib/const/locales";
import { type SelectTenant } from "$lib/server/db/central-schema";
import type { SelectClient } from "$lib/server/email/email-service";
@@ -19,19 +18,15 @@
tenant: SelectTenant;
loginUrl: string;
} = $props();
$effect(() => {
setLocale(locale);
});
</script>
<EmailLayout>
<EmailText variant="md">{m["emails.greeting"]({ name: user.email })}</EmailText>
<EmailLayout {locale}>
<EmailText variant="md">{m["emails.greeting"]({ name: user.email }, { locale })}</EmailText>
<EmailText variant="md">
{m["emails.pinReset.introduction"]({ tenant: tenant.longName })}
{m["emails.pinReset.introduction"]({ tenant: tenant.longName }, { locale })}
</EmailText>
<EmailButton href={loginUrl}>{m["emails.pinReset.action"]()}</EmailButton>
<EmailButton href={loginUrl}>{m["emails.pinReset.action"]({}, { locale })}</EmailButton>
<EmailText variant="md" color="text-light">
{m["emails.pinReset.reason"]()}
{m["emails.pinReset.reason"]({}, { locale })}
</EmailText>
</EmailLayout>
@@ -1,4 +1,4 @@
<script lang="js">
<script lang="ts">
let { children, variant = "h2", class: className = "" } = $props();
</script>
+11 -3
View File
@@ -1,8 +1,16 @@
<script lang="js">
<script lang="ts">
import { m } from "$i18n/messages";
import type { SupportedLocale } from "$lib/const/locales";
import type { Snippet } from "svelte";
import EmailText from "./EmailText.svelte";
let { children } = $props();
let {
locale,
children,
}: {
locale: SupportedLocale;
children?: Snippet;
} = $props();
</script>
<div class="page">
@@ -11,7 +19,7 @@
{@render children?.()}
</div>
<EmailText variant="sm" color="text-light" class="branding">
{m["emails.poweredBy"]()} <a href="https://open-reception.org">OpenReception</a>
{m["emails.poweredBy"]({}, { locale })} <a href="https://open-reception.org">OpenReception</a>
</EmailText>
</div>
</div>
+1 -1
View File
@@ -1,4 +1,4 @@
<script lang="js">
<script lang="ts">
let { children, variant, color = "text-darkest", class: className = "" } = $props();
</script>
+49 -58
View File
@@ -10,7 +10,6 @@ import type { SelectTenant, SelectUser } from "$lib/server/db/central-schema";
import { getTenantDb } from "$lib/server/db";
import * as tenantSchema from "$lib/server/db/tenant-schema";
import { eq } from "drizzle-orm";
import { setLocale } from "$i18n/runtime";
import { m } from "$i18n/messages";
import { render } from "svelte/server";
import AppointmentBooked from "$lib/emails/AppointmentBooked.svelte";
@@ -139,9 +138,12 @@ export async function sendPinResetEmail(
const recipient = createEmailRecipient(user);
const locale = (recipient.language as Language) || "en";
const subject = m["emails.pinReset.subject"]({
tenant: tenant.longName,
});
const subject = m["emails.pinReset.subject"](
{
tenant: tenant.longName,
},
{ locale },
);
const emailRender = render(PinReset, {
props: {
locale,
@@ -193,9 +195,12 @@ export async function sendAppointmentReminderEmail(
const { recipient, locale } = await getRecipient(user);
const channelTitle = await getChannelTitle(tenant.id, appointment.channelId, locale);
// Generate email
const subject = m["emails.appointmentReminder.subject"]({
tenant: tenant.longName,
});
const subject = m["emails.appointmentReminder.subject"](
{
tenant: tenant.longName,
},
{ locale },
);
const emailRender = render(AppointmentBooked, {
props: {
locale,
@@ -219,7 +224,6 @@ const getRecipient = async (user: SelectClient | SelectUser) => {
? { email: user.email, language: user.language }
: createEmailRecipient(user);
const locale = (recipient.language as Language) || "en";
setLocale(locale);
return { recipient, locale };
};
@@ -259,10 +263,13 @@ export async function sendAppointmentRejectedEmail(
const { recipient, locale } = await getRecipient(user);
// Generate email
const subject = m["emails.appointmentRejected.subject"]({
channel: channelTitle || appointment.channelId,
tenant: tenant.longName,
});
const subject = m["emails.appointmentRejected.subject"](
{
channel: channelTitle || appointment.channelId,
tenant: tenant.longName,
},
{ locale },
);
const emailRender = render(AppointmentRejected, {
props: {
locale,
@@ -303,10 +310,13 @@ export async function sendAppointmentCreatedEmail(
const { recipient, locale } = await getRecipient(user);
// Generate email
const subject = m["emails.appointmentBooked.subject"]({
channel: channelTitle || appointment.channelId,
tenant: tenant.longName,
});
const subject = m["emails.appointmentBooked.subject"](
{
channel: channelTitle || appointment.channelId,
tenant: tenant.longName,
},
{ locale },
);
const emailRender = render(AppointmentBooked, {
props: {
locale,
@@ -344,10 +354,13 @@ export async function sendAppointmentRequestEmail(
const agent = await agentService.getAgentById(appointment.agentId);
const { recipient, locale } = await getRecipient(user);
// Generate email
const subject = m["emails.appointmentRequest.subject"]({
channel: channelTitle || appointment.channelId,
tenant: tenant.longName,
});
const subject = m["emails.appointmentRequest.subject"](
{
channel: channelTitle || appointment.channelId,
tenant: tenant.longName,
},
{ locale },
);
const emailRender = render(AppointmentRequest, {
props: {
locale,
@@ -364,35 +377,6 @@ export async function sendAppointmentRequestEmail(
await sendEmail(recipient, subject, html, text, tenant.longName);
}
/**
* Send appointment update notification email
* @param {SelectClient | SelectUser} user - Database user object
* @param {SelectTenant} tenant - Tenant information for branding
* @param {SelectAppointment} appointment - Updated appointment details
* @param {string} [channelTitle] - Optional channel title/name
* @param {string} [cancelUrl] - Optional URL to cancel appointment
* @throws {Error} When email sending fails
* @returns {Promise<void>}
*/
export async function sendAppointmentUpdatedEmail(
user: SelectClient | SelectUser,
tenant: SelectTenant,
appointment: SelectAppointment,
channelTitle?: string,
cancelUrl?: string,
): Promise<void> {
const recipient = createEmailRecipient(user);
const language = (recipient.language as Language) || "en";
const subject = language === "en" ? "Appointment Updated" : "Termin aktualisiert";
await sendTemplatedEmail("appointment-updated", recipient, subject, language, tenant, {
appointment,
appointmentDate: appointment.appointmentDate,
title: channelTitle || appointment.channelId,
cancelUrl,
});
}
/**
* Generate base URL for email templates based on request URL and tenant
* @param {URL} requestUrl - The request URL
@@ -427,13 +411,17 @@ export async function sendConfirmationEmail(
const baseUrl = generateBaseUrl(requestUrl);
const confirmUrl = `${baseUrl}/confirm/${confirmationCode}`;
const recipient = user;
const locale = (user.language as Language) ?? "en";
// Generate email
const subject = m["emails.confirmation.subject"]({
tenant: tenant.longName,
});
const subject = m["emails.confirmation.subject"](
{
tenant: tenant.longName,
},
{ locale },
);
const emailRender = render(Confirmation, {
props: {
locale: (user.language as Language) ?? "en",
locale,
user: user as SelectUserEmail,
confirmUrl,
expirationMinutes,
@@ -464,10 +452,13 @@ export async function sendAppointmentCancelledEmail(
// Create recipient directly for SelectClient type, use helper for SelectUser
const { recipient, locale } = await getRecipient(user);
// Generate email
const subject = m["emails.appointmentCancelled.subject"]({
channel: channelTitle || appointment.channelId,
tenant: tenant.longName,
});
const subject = m["emails.appointmentCancelled.subject"](
{
channel: channelTitle || appointment.channelId,
tenant: tenant.longName,
},
{ locale },
);
const emailRender = render(AppointmentCancelled, {
props: {
locale,
@@ -499,7 +490,7 @@ export async function sendNotificationEmail(
// Create recipient directly for SelectClient type, use helper for SelectUser
const { recipient, locale } = await getRecipient(user);
// Generate email
const subject = m["emails.notification.subject"]();
const subject = m["emails.notification.subject"]({}, { locale });
const emailRender = render(Notification, {
props: {
locale,