Wrong language in emails (#1541)

* test --wip

* --wip

* --wip

* --wip

* split language into organizer-attendees

* name fix for tAttendees

* --WIP

* added attendee locale migration, --WIP

* --wip

* fixed check types --wip

* updated person language type

* test snapshot updated

* --wip

* --WIP

* --WIP

* --WIP

* test changes revert

* cleanup

* removed extra space from test

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
pull/1633/head^2
Syed Ali Shahbaz 2022-01-28 02:02:53 +05:30 committed by GitHub
parent 1119d7f558
commit af89de8004
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 324 additions and 221 deletions

View File

@ -10,7 +10,6 @@ import { getErrorFromUnknown } from "@lib/errors";
import EventManager from "@lib/events/EventManager";
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
import prisma from "@lib/prisma";
import { Ensure } from "@lib/types/utils";
import { getTranslation } from "@server/lib/i18n";
@ -78,18 +77,35 @@ async function handlePaymentSuccess(event: Stripe.Event) {
if (!user) throw new Error("No user found");
const t = await getTranslation(user.locale ?? "en", "common");
const attendeesListPromises = booking.attendees.map(async (attendee) => {
return {
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: {
translate: await getTranslation(attendee.locale ?? "en", "common"),
locale: attendee.locale ?? "en",
},
};
});
const evt: Ensure<CalendarEvent, "language"> = {
const attendeesList = await Promise.all(attendeesListPromises);
const evt: CalendarEvent = {
type: booking.title,
title: booking.title,
description: booking.description || undefined,
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),
organizer: { email: user.email!, name: user.name!, timeZone: user.timeZone },
attendees: booking.attendees,
organizer: {
email: user.email!,
name: user.name!,
timeZone: user.timeZone,
language: { translate: t, locale: user.locale ?? "en" },
},
attendees: attendeesList,
uid: booking.uid,
destinationCalendar: booking.destinationCalendar || user.destinationCalendar,
language: t,
};
if (booking.location) evt.location = booking.location;

View File

@ -13,14 +13,14 @@ const translator = short();
export const getWhat = (calEvent: CalendarEvent) => {
return `
${calEvent.language("what")}:
${calEvent.organizer.language.translate("what")}:
${calEvent.type}
`;
};
export const getWhen = (calEvent: CalendarEvent) => {
return `
${calEvent.language("invitee_timezone")}:
${calEvent.organizer.language.translate("invitee_timezone")}:
${calEvent.attendees[0].timeZone}
`;
};
@ -29,26 +29,26 @@ export const getWho = (calEvent: CalendarEvent) => {
const attendees = calEvent.attendees
.map((attendee) => {
return `
${attendee?.name || calEvent.language("guest")}
${attendee?.name || calEvent.organizer.language.translate("guest")}
${attendee.email}
`;
})
.join("");
const organizer = `
${calEvent.organizer.name} - ${calEvent.language("organizer")}
${calEvent.organizer.name} - ${calEvent.organizer.language.translate("organizer")}
${calEvent.organizer.email}
`;
return `
${calEvent.language("who")}:
${calEvent.organizer.language.translate("who")}:
${organizer + attendees}
`;
};
export const getAdditionalNotes = (calEvent: CalendarEvent) => {
return `
${calEvent.language("additional_notes")}:
${calEvent.organizer.language.translate("additional_notes")}:
${calEvent.description}
`;
};
@ -74,7 +74,7 @@ export const getLocation = (calEvent: CalendarEvent) => {
export const getManageLink = (calEvent: CalendarEvent) => {
return `
${calEvent.language("need_to_reschedule_or_cancel")}
${calEvent.organizer.language.translate("need_to_reschedule_or_cancel")}
${getCancelLink(calEvent)}
`;
};
@ -96,7 +96,7 @@ export const getRichDescription = (calEvent: CalendarEvent, attendee?: Person) =
${getWhat(calEvent)}
${getWhen(calEvent)}
${getWho(calEvent)}
${calEvent.language("where")}:
${calEvent.organizer.language.translate("where")}:
${getLocation(calEvent)}
${getAdditionalNotes(calEvent)}
`.trim();
@ -106,7 +106,7 @@ ${getAdditionalNotes(calEvent)}
${getWhat(calEvent)}
${getWhen(calEvent)}
${getWho(calEvent)}
${calEvent.language("where")}:
${calEvent.organizer.language.translate("where")}:
${getLocation(calEvent)}
${getAdditionalNotes(calEvent)}
${getManageLink(calEvent)}

View File

@ -25,14 +25,14 @@ export default class AttendeeAwaitingPaymentEmail extends AttendeeScheduledEmail
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: this.calEvent.organizer.email,
subject: `${this.calEvent.language("awaiting_payment_subject", {
subject: `${this.calEvent.attendees[0].language.translate("awaiting_payment_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.calEvent.language(
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
})}`,
@ -43,8 +43,8 @@ export default class AttendeeAwaitingPaymentEmail extends AttendeeScheduledEmail
protected getTextBody(): string {
return `
${this.calEvent.language("meeting_awaiting_payment")}
${this.calEvent.language("emailed_you_and_any_other_attendees")}
${this.calEvent.attendees[0].language.translate("meeting_awaiting_payment")}
${this.calEvent.attendees[0].language.translate("emailed_you_and_any_other_attendees")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
@ -53,14 +53,14 @@ ${this.getAdditionalNotes()}
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.language("awaiting_payment_subject", {
const headerContent = this.calEvent.attendees[0].language.translate("awaiting_payment_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.calEvent.language(
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
});
@ -73,8 +73,8 @@ ${this.getAdditionalNotes()}
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("calendarCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.language("meeting_awaiting_payment"),
this.calEvent.language("emailed_you_and_any_other_attendees")
this.calEvent.attendees[0].language.translate("meeting_awaiting_payment"),
this.calEvent.attendees[0].language.translate("emailed_you_and_any_other_attendees")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
@ -148,7 +148,7 @@ ${this.getAdditionalNotes()}
}
protected getManageLink(): string {
const manageText = this.calEvent.language("pay_now");
const manageText = this.calEvent.attendees[0].language.translate("pay_now");
if (this.calEvent.paymentInfo) {
return `

View File

@ -24,14 +24,14 @@ export default class AttendeeCancelledEmail extends AttendeeScheduledEmail {
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: this.calEvent.organizer.email,
subject: `${this.calEvent.language("event_cancelled_subject", {
subject: `${this.calEvent.attendees[0].language.translate("event_cancelled_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.calEvent.language(
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
})}`,
@ -42,8 +42,8 @@ export default class AttendeeCancelledEmail extends AttendeeScheduledEmail {
protected getTextBody(): string {
return `
${this.calEvent.language("event_request_cancelled")}
${this.calEvent.language("emailed_you_and_any_other_attendees")}
${this.calEvent.attendees[0].language.translate("event_request_cancelled")}
${this.calEvent.attendees[0].language.translate("emailed_you_and_any_other_attendees")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
@ -52,14 +52,14 @@ ${this.getAdditionalNotes()}
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.language("event_cancelled_subject", {
const headerContent = this.calEvent.attendees[0].language.translate("event_cancelled_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.calEvent.language(
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
});
@ -73,8 +73,8 @@ ${this.getAdditionalNotes()}
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("xCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.language("event_request_cancelled"),
this.calEvent.language("emailed_you_and_any_other_attendees")
this.calEvent.attendees[0].language.translate("event_request_cancelled"),
this.calEvent.attendees[0].language.translate("emailed_you_and_any_other_attendees")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->

View File

@ -24,14 +24,14 @@ export default class AttendeeDeclinedEmail extends AttendeeScheduledEmail {
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: this.calEvent.organizer.email,
subject: `${this.calEvent.language("event_declined_subject", {
subject: `${this.calEvent.attendees[0].language.translate("event_declined_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.calEvent.language(
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
})}`,
@ -42,8 +42,8 @@ export default class AttendeeDeclinedEmail extends AttendeeScheduledEmail {
protected getTextBody(): string {
return `
${this.calEvent.language("event_request_declined")}
${this.calEvent.language("emailed_you_and_any_other_attendees")}
${this.calEvent.attendees[0].language.translate("event_request_declined")}
${this.calEvent.attendees[0].language.translate("emailed_you_and_any_other_attendees")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
@ -52,14 +52,14 @@ ${this.getAdditionalNotes()}
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.language("event_declined_subject", {
const headerContent = this.calEvent.attendees[0].language.translate("event_declined_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.calEvent.language(
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
});
@ -73,8 +73,8 @@ ${this.getAdditionalNotes()}
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("xCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.language("event_request_declined"),
this.calEvent.language("emailed_you_and_any_other_attendees")
this.calEvent.attendees[0].language.translate("event_request_declined"),
this.calEvent.attendees[0].language.translate("emailed_you_and_any_other_attendees")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->

View File

@ -30,14 +30,14 @@ export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: this.calEvent.organizer.email,
subject: `${this.calEvent.language("rescheduled_event_type_subject", {
subject: `${this.calEvent.attendees[0].language.translate("rescheduled_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.calEvent.language(
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
})}`,
@ -51,20 +51,20 @@ export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
// Guests cannot
if (this.attendee === this.calEvent.attendees[0]) {
return `
${this.calEvent.language("event_has_been_rescheduled")}
${this.calEvent.language("emailed_you_and_any_other_attendees")}
${this.calEvent.attendees[0].language.translate("event_has_been_rescheduled")}
${this.calEvent.attendees[0].language.translate("emailed_you_and_any_other_attendees")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getAdditionalNotes()}
${this.calEvent.language("need_to_reschedule_or_cancel")}
${this.calEvent.attendees[0].language.translate("need_to_reschedule_or_cancel")}
${getCancelLink(this.calEvent)}
`.replace(/(<([^>]+)>)/gi, "");
}
return `
${this.calEvent.language("event_has_been_rescheduled")}
${this.calEvent.language("emailed_you_and_any_other_attendees")}
${this.calEvent.attendees[0].language.translate("event_has_been_rescheduled")}
${this.calEvent.attendees[0].language.translate("emailed_you_and_any_other_attendees")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
@ -73,14 +73,14 @@ ${this.getAdditionalNotes()}
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.language("rescheduled_event_type_subject", {
const headerContent = this.calEvent.attendees[0].language.translate("rescheduled_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.calEvent.language(
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
});
@ -93,8 +93,8 @@ ${this.getAdditionalNotes()}
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("calendarCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.language("event_has_been_rescheduled"),
this.calEvent.language("emailed_you_and_any_other_attendees")
this.calEvent.attendees[0].language.translate("event_has_been_rescheduled"),
this.calEvent.attendees[0].language.translate("emailed_you_and_any_other_attendees")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->

View File

@ -61,7 +61,7 @@ export default class AttendeeScheduledEmail {
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc",
productId: "calendso/ics",
title: this.calEvent.language("ics_event_title", {
title: this.calEvent.attendees[0].language.translate("ics_event_title", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
}),
@ -89,14 +89,14 @@ export default class AttendeeScheduledEmail {
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: this.calEvent.organizer.email,
subject: `${this.calEvent.language("confirmed_event_type_subject", {
subject: `${this.calEvent.attendees[0].language.translate("confirmed_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.calEvent.language(
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
})}`,
@ -114,8 +114,8 @@ export default class AttendeeScheduledEmail {
protected getTextBody(): string {
return `
${this.calEvent.language("your_event_has_been_scheduled")}
${this.calEvent.language("emailed_you_and_any_other_attendees")}
${this.calEvent.attendees[0].language.translate("your_event_has_been_scheduled")}
${this.calEvent.attendees[0].language.translate("emailed_you_and_any_other_attendees")}
${getRichDescription(this.calEvent)}
`.trim();
@ -126,14 +126,14 @@ ${getRichDescription(this.calEvent)}
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.language("confirmed_event_type_subject", {
const headerContent = this.calEvent.attendees[0].language.translate("confirmed_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.calEvent.language(
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
});
@ -146,8 +146,8 @@ ${getRichDescription(this.calEvent)}
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("checkCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.language("your_event_has_been_scheduled"),
this.calEvent.language("emailed_you_and_any_other_attendees")
this.calEvent.attendees[0].language.translate("your_event_has_been_scheduled"),
this.calEvent.attendees[0].language.translate("emailed_you_and_any_other_attendees")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
@ -219,8 +219,8 @@ ${getRichDescription(this.calEvent)}
// Only the original attendee can make changes to the event
// Guests cannot
if (this.attendee === this.calEvent.attendees[0]) {
const manageText = this.calEvent.language("manage_this_event");
return `<p>${this.calEvent.language(
const manageText = this.calEvent.attendees[0].language.translate("manage_this_event");
return `<p>${this.calEvent.attendees[0].language.translate(
"need_to_reschedule_or_cancel"
)}</p><p style="font-weight: 400; line-height: 24px;"><a href="${getCancelLink(
this.calEvent
@ -233,7 +233,7 @@ ${getRichDescription(this.calEvent)}
protected getWhat(): string {
return `
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.language("what")}</p>
<p style="color: #494949;">${this.calEvent.attendees[0].language.translate("what")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.type}</p>
</div>`;
}
@ -242,11 +242,11 @@ ${getRichDescription(this.calEvent)}
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.language("when")}</p>
<p style="color: #494949;">${this.calEvent.attendees[0].language.translate("when")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">
${this.calEvent.language(
${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format(
"YYYY"
@ -261,7 +261,7 @@ ${getRichDescription(this.calEvent)}
const attendees = this.calEvent.attendees
.map((attendee) => {
return `<div style="color: #494949; font-weight: 400; line-height: 24px;">${
attendee?.name || `${this.calEvent.language("guest")}`
attendee?.name || `${this.calEvent.attendees[0].language.translate("guest")}`
} <span style="color: #888888"><a href="mailto:${attendee.email}" style="color: #888888;">${
attendee.email
}</a></span></div>`;
@ -270,14 +270,16 @@ ${getRichDescription(this.calEvent)}
const organizer = `<div style="color: #494949; font-weight: 400; line-height: 24px;">${
this.calEvent.organizer.name
} - ${this.calEvent.language("organizer")} <span style="color: #888888"><a href="mailto:${
} - ${this.calEvent.attendees[0].language.translate(
"organizer"
)} <span style="color: #888888"><a href="mailto:${
this.calEvent.organizer.email
}" style="color: #888888;">${this.calEvent.organizer.email}</a></span></div>`;
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.language("who")}</p>
<p style="color: #494949;">${this.calEvent.attendees[0].language.translate("who")}</p>
${organizer + attendees}
</div>`;
}
@ -286,7 +288,7 @@ ${getRichDescription(this.calEvent)}
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.language("additional_notes")}</p>
<p style="color: #494949;">${this.calEvent.attendees[0].language.translate("additional_notes")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.description}</p>
</div>
`;
@ -308,30 +310,30 @@ ${getRichDescription(this.calEvent)}
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.language("where")}</p>
<p style="color: #494949;">${this.calEvent.attendees[0].language.translate("where")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${providerName} ${
meetingUrl &&
`<a href="${meetingUrl}" target="_blank" alt="${this.calEvent.language(
`<a href="${meetingUrl}" target="_blank" alt="${this.calEvent.attendees[0].language.translate(
"meeting_url"
)}"><img src="${linkIcon()}" width="12px"></img></a>`
}</p>
${
meetingId &&
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.language(
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.attendees[0].language.translate(
"meeting_id"
)}: <span>${meetingId}</span></div>`
}
${
meetingPassword &&
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.language(
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.attendees[0].language.translate(
"meeting_password"
)}: <span>${meetingPassword}</span></div>`
}
${
meetingUrl &&
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.language(
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.attendees[0].language.translate(
"meeting_url"
)}: <a href="${meetingUrl}" alt="${this.calEvent.language(
)}: <a href="${meetingUrl}" alt="${this.calEvent.attendees[0].language.translate(
"meeting_url"
)}" style="color: #3E3E3E" target="_blank">${meetingUrl}</a></div>`
}
@ -345,14 +347,14 @@ ${getRichDescription(this.calEvent)}
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.language("where")}</p>
<p style="color: #494949;">${this.calEvent.attendees[0].language.translate("where")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${providerName} ${
hangoutLink &&
`<a href="${hangoutLink}" target="_blank" alt="${this.calEvent.language(
`<a href="${hangoutLink}" target="_blank" alt="${this.calEvent.attendees[0].language.translate(
"meeting_url"
)}"><img src="${linkIcon()}" width="12px"></img></a>`
}</p>
<div style="color: #494949; font-weight: 400; line-height: 24px;"><a href="${hangoutLink}" alt="${this.calEvent.language(
<div style="color: #494949; font-weight: 400; line-height: 24px;"><a href="${hangoutLink}" alt="${this.calEvent.attendees[0].language.translate(
"meeting_url"
)}" style="color: #3E3E3E" target="_blank">${hangoutLink}</a></div>
</div>
@ -362,7 +364,7 @@ ${getRichDescription(this.calEvent)}
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.language("where")}</p>
<p style="color: #494949;">${this.calEvent.attendees[0].language.translate("where")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${
providerName || this.calEvent.location
}</p>

View File

@ -33,14 +33,14 @@ export default class OrganizerCancelledEmail extends OrganizerScheduledEmail {
return {
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.calEvent.language("event_cancelled_subject", {
subject: `${this.calEvent.organizer.language.translate("event_cancelled_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
})}`,
@ -51,8 +51,8 @@ export default class OrganizerCancelledEmail extends OrganizerScheduledEmail {
protected getTextBody(): string {
return `
${this.calEvent.language("event_request_cancelled")}
${this.calEvent.language("emailed_you_and_any_other_attendees")}
${this.calEvent.organizer.language.translate("event_request_cancelled")}
${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
@ -61,14 +61,14 @@ ${this.getAdditionalNotes()}
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.language("event_cancelled_subject", {
const headerContent = this.calEvent.organizer.language.translate("event_cancelled_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
});
@ -81,8 +81,8 @@ ${this.getAdditionalNotes()}
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("xCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.language("event_request_cancelled"),
this.calEvent.language("emailed_you_and_any_other_attendees")
this.calEvent.organizer.language.translate("event_request_cancelled"),
this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->

View File

@ -27,14 +27,14 @@ export default class OrganizerPaymentRefundFailedEmail extends OrganizerSchedule
return {
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.calEvent.language("refund_failed_subject", {
subject: `${this.calEvent.organizer.language.translate("refund_failed_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
})}`,
@ -45,11 +45,15 @@ export default class OrganizerPaymentRefundFailedEmail extends OrganizerSchedule
protected getTextBody(): string {
return `
${this.calEvent.language("a_refund_failed")}
${this.calEvent.language("check_with_provider_and_user", { user: this.calEvent.attendees[0].name })}
${this.calEvent.organizer.language.translate("a_refund_failed")}
${this.calEvent.organizer.language.translate("check_with_provider_and_user", {
user: this.calEvent.attendees[0].name,
})}
${
this.calEvent.paymentInfo &&
this.calEvent.language("error_message", { errorMessage: this.calEvent.paymentInfo.reason })
this.calEvent.organizer.language.translate("error_message", {
errorMessage: this.calEvent.paymentInfo.reason,
})
}
${this.getWhat()}
${this.getWhen()}
@ -59,14 +63,14 @@ ${this.getAdditionalNotes()}
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.language("refund_failed_subject", {
const headerContent = this.calEvent.organizer.language.translate("refund_failed_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
});
@ -91,14 +95,14 @@ ${this.getAdditionalNotes()}
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:24px;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:24px;font-weight:700;line-height:24px;text-align:center;color:#292929;">${this.calEvent.language(
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:24px;font-weight:700;line-height:24px;text-align:center;color:#292929;">${this.calEvent.organizer.language.translate(
"a_refund_failed"
)}</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:center;color:#494949;">${this.calEvent.language(
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:center;color:#494949;">${this.calEvent.organizer.language.translate(
"check_with_provider_and_user",
{ user: this.calEvent.attendees[0].name }
)}</div>
@ -174,7 +178,7 @@ ${this.getAdditionalNotes()}
refundInformation = `
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:center;color:#494949;">${this.calEvent.language(
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:center;color:#494949;">${this.calEvent.organizer.language.translate(
"error_message",
{ errorMessage: paymentInfo.reason }
)}</div>

View File

@ -34,14 +34,14 @@ export default class OrganizerRequestEmail extends OrganizerScheduledEmail {
return {
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.calEvent.language("event_awaiting_approval_subject", {
subject: `${this.calEvent.organizer.language.translate("event_awaiting_approval_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
})}`,
@ -52,26 +52,26 @@ export default class OrganizerRequestEmail extends OrganizerScheduledEmail {
protected getTextBody(): string {
return `
${this.calEvent.language("event_awaiting_approval")}
${this.calEvent.language("someone_requested_an_event")}
${this.calEvent.organizer.language.translate("event_awaiting_approval")}
${this.calEvent.organizer.language.translate("someone_requested_an_event")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getAdditionalNotes()}
${this.calEvent.language("confirm_or_reject_request")}
${this.calEvent.organizer.language.translate("confirm_or_reject_request")}
${process.env.BASE_URL} + "/bookings/upcoming"
`.replace(/(<([^>]+)>)/gi, "");
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.language("event_awaiting_approval_subject", {
const headerContent = this.calEvent.organizer.language.translate("event_awaiting_approval_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
});
@ -85,8 +85,8 @@ ${process.env.BASE_URL} + "/bookings/upcoming"
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("calendarCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.language("event_awaiting_approval"),
this.calEvent.language("someone_requested_an_event")
this.calEvent.organizer.language.translate("event_awaiting_approval"),
this.calEvent.organizer.language.translate("someone_requested_an_event")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
@ -166,7 +166,7 @@ ${process.env.BASE_URL} + "/bookings/upcoming"
}
protected getManageLink(): string {
const manageText = this.calEvent.language("confirm_or_reject_request");
const manageText = this.calEvent.organizer.language.translate("confirm_or_reject_request");
const manageLink = process.env.BASE_URL + "/bookings/upcoming";
return `<a style="color: #FFFFFF; text-decoration: none;" href="${manageLink}" target="_blank">${manageText} <img src="${linkIcon()}" width="12px"></img></a>`;
}

View File

@ -34,14 +34,14 @@ export default class OrganizerRequestReminderEmail extends OrganizerScheduledEma
return {
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.calEvent.language("event_awaiting_approval_subject", {
subject: `${this.calEvent.organizer.language.translate("event_awaiting_approval_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
})}`,
@ -52,26 +52,26 @@ export default class OrganizerRequestReminderEmail extends OrganizerScheduledEma
protected getTextBody(): string {
return `
${this.calEvent.language("event_still_awaiting_approval")}
${this.calEvent.language("someone_requested_an_event")}
${this.calEvent.organizer.language.translate("event_still_awaiting_approval")}
${this.calEvent.organizer.language.translate("someone_requested_an_event")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getAdditionalNotes()}
${this.calEvent.language("confirm_or_reject_request")}
${this.calEvent.organizer.language.translate("confirm_or_reject_request")}
${process.env.BASE_URL} + "/bookings/upcoming"
`.replace(/(<([^>]+)>)/gi, "");
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.language("event_awaiting_approval_subject", {
const headerContent = this.calEvent.organizer.language.translate("event_awaiting_approval_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
});
@ -84,8 +84,8 @@ ${process.env.BASE_URL} + "/bookings/upcoming"
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("calendarCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.language("event_still_awaiting_approval"),
this.calEvent.language("someone_requested_an_event")
this.calEvent.organizer.language.translate("event_still_awaiting_approval"),
this.calEvent.organizer.language.translate("someone_requested_an_event")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
@ -165,7 +165,7 @@ ${process.env.BASE_URL} + "/bookings/upcoming"
}
protected getManageLink(): string {
const manageText = this.calEvent.language("confirm_or_reject_request");
const manageText = this.calEvent.organizer.language.translate("confirm_or_reject_request");
const manageLink = process.env.BASE_URL + "/bookings/upcoming";
return `<a style="color: #FFFFFF; text-decoration: none;" href="${manageLink}" target="_blank">${manageText} <img src="${linkIcon()}" width="12px"></img></a>`;
}

View File

@ -39,14 +39,14 @@ export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
},
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.calEvent.language("rescheduled_event_type_subject", {
subject: `${this.calEvent.organizer.language.translate("rescheduled_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
})}`,
@ -57,26 +57,26 @@ export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
protected getTextBody(): string {
return `
${this.calEvent.language("event_has_been_rescheduled")}
${this.calEvent.language("emailed_you_and_any_other_attendees")}
${this.calEvent.organizer.language.translate("event_has_been_rescheduled")}
${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getAdditionalNotes()}
${this.calEvent.language("need_to_reschedule_or_cancel")}
${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")}
${getCancelLink(this.calEvent)}
`.replace(/(<([^>]+)>)/gi, "");
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.language("rescheduled_event_type_subject", {
const headerContent = this.calEvent.organizer.language.translate("rescheduled_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
});
@ -89,8 +89,8 @@ ${getCancelLink(this.calEvent)}
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("calendarCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.language("event_has_been_rescheduled"),
this.calEvent.language("emailed_you_and_any_other_attendees")
this.calEvent.organizer.language.translate("event_has_been_rescheduled"),
this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->

View File

@ -59,7 +59,7 @@ export default class OrganizerScheduledEmail {
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc",
productId: "calendso/ics",
title: this.calEvent.language("ics_event_title", {
title: this.calEvent.organizer.language.translate("ics_event_title", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
}),
@ -96,14 +96,14 @@ export default class OrganizerScheduledEmail {
},
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.calEvent.language("confirmed_event_type_subject", {
subject: `${this.calEvent.organizer.language.translate("confirmed_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
})}`,
@ -121,8 +121,8 @@ export default class OrganizerScheduledEmail {
protected getTextBody(): string {
return `
${this.calEvent.language("new_event_scheduled")}
${this.calEvent.language("emailed_you_and_any_other_attendees")}
${this.calEvent.organizer.language.translate("new_event_scheduled")}
${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")}
${getRichDescription(this.calEvent)}
`.trim();
@ -133,14 +133,14 @@ ${getRichDescription(this.calEvent)}
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.language("confirmed_event_type_subject", {
const headerContent = this.calEvent.organizer.language.translate("confirmed_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
});
@ -153,8 +153,8 @@ ${getRichDescription(this.calEvent)}
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("checkCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.language("new_event_scheduled"),
this.calEvent.language("emailed_you_and_any_other_attendees")
this.calEvent.organizer.language.translate("new_event_scheduled"),
this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
@ -223,8 +223,8 @@ ${getRichDescription(this.calEvent)}
}
protected getManageLink(): string {
const manageText = this.calEvent.language("manage_this_event");
return `<p>${this.calEvent.language(
const manageText = this.calEvent.organizer.language.translate("manage_this_event");
return `<p>${this.calEvent.organizer.language.translate(
"need_to_reschedule_or_cancel"
)}</p><p style="font-weight: 400; line-height: 24px;"><a href="${getCancelLink(
this.calEvent
@ -234,7 +234,7 @@ ${getRichDescription(this.calEvent)}
protected getWhat(): string {
return `
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.language("what")}</p>
<p style="color: #494949;">${this.calEvent.organizer.language.translate("what")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.type}</p>
</div>`;
}
@ -243,11 +243,11 @@ ${getRichDescription(this.calEvent)}
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.language("when")}</p>
<p style="color: #494949;">${this.calEvent.organizer.language.translate("when")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">
${this.calEvent.language(
${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.language(
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format(
"YYYY"
@ -262,7 +262,7 @@ ${getRichDescription(this.calEvent)}
const attendees = this.calEvent.attendees
.map((attendee) => {
return `<div style="color: #494949; font-weight: 400; line-height: 24px;">${
attendee?.name || `${this.calEvent.language("guest")}`
attendee?.name || `${this.calEvent.organizer.language.translate("guest")}`
} <span style="color: #888888"><a href="mailto:${attendee.email}" style="color: #888888;">${
attendee.email
}</a></span></div>`;
@ -271,14 +271,16 @@ ${getRichDescription(this.calEvent)}
const organizer = `<div style="color: #494949; font-weight: 400; line-height: 24px;">${
this.calEvent.organizer.name
} - ${this.calEvent.language("organizer")} <span style="color: #888888"><a href="mailto:${
} - ${this.calEvent.organizer.language.translate(
"organizer"
)} <span style="color: #888888"><a href="mailto:${
this.calEvent.organizer.email
}" style="color: #888888;">${this.calEvent.organizer.email}</a></span></div>`;
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.language("who")}</p>
<p style="color: #494949;">${this.calEvent.organizer.language.translate("who")}</p>
${organizer + attendees}
</div>`;
}
@ -287,7 +289,7 @@ ${getRichDescription(this.calEvent)}
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.language("additional_notes")}</p>
<p style="color: #494949;">${this.calEvent.organizer.language.translate("additional_notes")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.description}</p>
</div>
`;
@ -309,30 +311,30 @@ ${getRichDescription(this.calEvent)}
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.language("where")}</p>
<p style="color: #494949;">${this.calEvent.organizer.language.translate("where")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${providerName} ${
meetingUrl &&
`<a href="${meetingUrl}" target="_blank" alt="${this.calEvent.language(
`<a href="${meetingUrl}" target="_blank" alt="${this.calEvent.organizer.language.translate(
"meeting_url"
)}"><img src="${linkIcon()}" width="12px"></img></a>`
}</p>
${
meetingId &&
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.language(
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.organizer.language.translate(
"meeting_id"
)}: <span>${meetingId}</span></div>`
}
${
meetingPassword &&
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.language(
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.organizer.language.translate(
"meeting_password"
)}: <span>${meetingPassword}</span></div>`
}
${
meetingUrl &&
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.language(
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.organizer.language.translate(
"meeting_url"
)}: <a href="${meetingUrl}" alt="${this.calEvent.language(
)}: <a href="${meetingUrl}" alt="${this.calEvent.organizer.language.translate(
"meeting_url"
)}" style="color: #3E3E3E" target="_blank">${meetingUrl}</a></div>`
}
@ -346,14 +348,14 @@ ${getRichDescription(this.calEvent)}
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.language("where")}</p>
<p style="color: #494949;">${this.calEvent.organizer.language.translate("where")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${providerName} ${
hangoutLink &&
`<a href="${hangoutLink}" target="_blank" alt="${this.calEvent.language(
`<a href="${hangoutLink}" target="_blank" alt="${this.calEvent.organizer.language.translate(
"meeting_url"
)}"><img src="${linkIcon()}" width="12px"></img></a>`
}</p>
<div style="color: #494949; font-weight: 400; line-height: 24px;"><a href="${hangoutLink}" alt="${this.calEvent.language(
<div style="color: #494949; font-weight: 400; line-height: 24px;"><a href="${hangoutLink}" alt="${this.calEvent.organizer.language.translate(
"meeting_url"
)}" style="color: #3E3E3E" target="_blank">${hangoutLink}</a></div>
</div>
@ -363,7 +365,7 @@ ${getRichDescription(this.calEvent)}
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.language("where")}</p>
<p style="color: #494949;">${this.calEvent.organizer.language.translate("where")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${
providerName || this.calEvent.location
}</p>

View File

@ -8,7 +8,6 @@ import { createEvent, updateEvent } from "@lib/integrations/calendar/CalendarMan
import { AdditionInformation, CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
import { LocationType } from "@lib/location";
import prisma from "@lib/prisma";
import { Ensure } from "@lib/types/utils";
import { createMeeting, updateMeeting, VideoCallData } from "@lib/videoClient";
export type Event = AdditionInformation & VideoCallData;
@ -118,7 +117,7 @@ export default class EventManager {
*
* @param event
*/
public async create(event: Ensure<CalendarEvent, "language">): Promise<CreateUpdateResult> {
public async create(event: CalendarEvent): Promise<CreateUpdateResult> {
const evt = processLocation(event);
const isDedicated = evt.location ? isDedicatedIntegration(evt.location) : null;
@ -158,10 +157,7 @@ export default class EventManager {
*
* @param event
*/
public async update(
event: Ensure<CalendarEvent, "language">,
rescheduleUid: string
): Promise<CreateUpdateResult> {
public async update(event: CalendarEvent, rescheduleUid: string): Promise<CreateUpdateResult> {
const evt = processLocation(event);
if (!rescheduleUid) {
@ -293,7 +289,7 @@ export default class EventManager {
* @param event
* @private
*/
private createVideoEvent(event: Ensure<CalendarEvent, "language">): Promise<EventResult> {
private createVideoEvent(event: CalendarEvent): Promise<EventResult> {
const credential = this.getVideoCredential(event);
if (credential) {

View File

@ -13,6 +13,7 @@ export type Person = {
name: string;
email: string;
timeZone: string;
language: { translate: TFunction; locale: string };
};
export interface EntryPoint {
@ -46,7 +47,6 @@ export interface CalendarEvent {
organizer: Person;
attendees: Person[];
conferenceData?: ConferenceData;
language: TFunction;
additionInformation?: AdditionInformation;
uid?: string | null;
videoCallData?: VideoCallData;

View File

@ -10,7 +10,6 @@ import logger from "@lib/logger";
import DailyVideoApiAdapter from "./integrations/Daily/DailyVideoApiAdapter";
import ZoomVideoApiAdapter from "./integrations/Zoom/ZoomVideoApiAdapter";
import { CalendarEvent } from "./integrations/calendar/interfaces/Calendar";
import { Ensure } from "./types/utils";
const log = logger.getChildLogger({ prefix: ["[lib] videoClient"] });
@ -56,10 +55,7 @@ const getBusyVideoTimes = (withCredentials: Credential[]) =>
results.reduce((acc, availability) => acc.concat(availability), [])
);
const createMeeting = async (
credential: Credential,
calEvent: Ensure<CalendarEvent, "language">
): Promise<EventResult> => {
const createMeeting = async (credential: Credential, calEvent: CalendarEvent): Promise<EventResult> => {
const uid: string = getUid(calEvent);
if (!credential) {

View File

@ -4,7 +4,7 @@ import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
type ContentType = "application/json" | "application/x-www-form-urlencoded";
function applyTemplate(template: string, data: Omit<CalendarEvent, "language">, contentType: ContentType) {
function applyTemplate(template: string, data: CalendarEvent, contentType: ContentType) {
const compiled = compile(template)(data);
if (contentType === "application/json") {
return jsonParse(compiled);
@ -25,7 +25,9 @@ const sendPayload = async (
triggerEvent: string,
createdAt: string,
subscriberUrl: string,
data: Omit<CalendarEvent, "language"> & { metadata?: { [key: string]: string } },
data: CalendarEvent & {
metadata?: { [key: string]: string };
},
template?: string | null
) => {
if (!subscriberUrl || !data) {

View File

@ -43,8 +43,6 @@ const authorized = async (
const log = logger.getChildLogger({ prefix: ["[api] book:user"] });
export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
const t = await getTranslation(req.body.language ?? "en", "common");
const session = await getSession({ req: req });
if (!session?.user?.id) {
return res.status(401).json({ message: "Not authenticated" });
@ -71,6 +69,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
name: true,
username: true,
destinationCalendar: true,
locale: true,
},
});
@ -78,6 +77,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(404).json({ message: "User not found" });
}
const tOrganizer = await getTranslation(currentUser.locale ?? "en", "common");
if (req.method === "PATCH") {
const booking = await prisma.booking.findFirst({
where: {
@ -112,6 +113,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(400).json({ message: "booking already confirmed" });
}
const attendeesListPromises = booking.attendees.map(async (attendee) => {
return {
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: {
translate: await getTranslation(attendee.locale ?? "en", "common"),
locale: attendee.locale ?? "en",
},
};
});
const attendeesList = await Promise.all(attendeesListPromises);
const evt: CalendarEvent = {
type: booking.title,
title: booking.title,
@ -122,11 +137,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
email: currentUser.email,
name: currentUser.name || "Unnamed",
timeZone: currentUser.timeZone,
language: { translate: tOrganizer, locale: currentUser.locale ?? "en" },
},
attendees: booking.attendees,
attendees: attendeesList,
location: booking.location ?? "",
uid: booking.uid,
language: t,
destinationCalendar: booking?.destinationCalendar || currentUser.destinationCalendar,
};

View File

@ -136,6 +136,7 @@ const userSelect = Prisma.validator<Prisma.UserArgs>()({
credentials: true,
bufferTime: true,
destinationCalendar: true,
locale: true,
},
});
@ -152,6 +153,7 @@ const getUserNameWithBookingCounts = async (eventTypeId: number, selectedUserNam
select: {
id: true,
username: true,
locale: true,
},
});
@ -180,8 +182,8 @@ type User = Prisma.UserGetPayload<typeof userSelect>;
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const reqBody = req.body as BookingCreateBody;
const eventTypeId = reqBody.eventTypeId;
const t = await getTranslation(reqBody.language ?? "en", "common");
const tAttendees = await getTranslation(reqBody.language ?? "en", "common");
const tGuests = await getTranslation("en", "common");
log.debug(`Booking eventType ${eventTypeId} started`);
const isTimeInPast = (time: string): boolean => {
@ -244,6 +246,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
users.push(eventTypeUser);
}
const organizer = await prisma.user.findUnique({
where: {
id: users[0].id,
},
select: {
locale: true,
},
});
const tOrganizer = await getTranslation(organizer?.locale ?? "en", "common");
if (eventType.schedulingType === SchedulingType.ROUND_ROBIN) {
const bookingCounts = await getUserNameWithBookingCounts(
eventTypeId,
@ -253,25 +266,41 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
users = getLuckyUsers(users, bookingCounts);
}
const invitee = [{ email: reqBody.email, name: reqBody.name, timeZone: reqBody.timeZone }];
const invitee = [
{
email: reqBody.email,
name: reqBody.name,
timeZone: reqBody.timeZone,
language: { translate: tAttendees, locale: reqBody.language ?? "en" },
},
];
const guests = (reqBody.guests || []).map((guest) => {
const g = {
email: guest,
name: "",
timeZone: reqBody.timeZone,
language: { translate: tGuests, locale: "en" },
};
return g;
});
const teamMembers =
const teamMemberPromises =
eventType.schedulingType === SchedulingType.COLLECTIVE
? users.slice(1).map((user) => ({
email: user.email || "",
name: user.name || "",
timeZone: user.timeZone,
}))
? users.slice(1).map(async function (user) {
return {
email: user.email || "",
name: user.name || "",
timeZone: user.timeZone,
language: {
translate: await getTranslation(user.locale ?? "en", "common"),
locale: user.locale ?? "en",
},
};
})
: [];
const teamMembers = await Promise.all(teamMemberPromises);
const attendeesList = [...invitee, ...guests, ...teamMembers];
const seed = `${users[0].username}:${dayjs(req.body.start).utc().format()}:${new Date().getTime()}`;
@ -282,7 +311,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
eventType: eventType.title,
eventName: eventType.eventName,
host: users[0].name || "Nameless",
t,
t: tOrganizer,
};
const description =
@ -294,7 +323,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const evt: CalendarEvent = {
type: eventType.title,
title: getEventName(eventNameObject),
title: getEventName(eventNameObject), //this needs to be either forced in english, or fetched for each attendee and organizer separately
description,
startTime: reqBody.start,
endTime: reqBody.end,
@ -302,10 +331,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
name: users[0].name || "Nameless",
email: users[0].email || "Email-less",
timeZone: users[0].timeZone,
language: { translate: tOrganizer, locale: organizer?.locale ?? "en" },
},
attendees: attendeesList,
location: reqBody.location, // Will be processed by the EventManager later.
language: t,
/** For team events, we will need to handle each member destinationCalendar eventually */
destinationCalendar: eventType.destinationCalendar || users[0].destinationCalendar,
};
@ -343,7 +372,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
attendees: {
createMany: {
data: evt.attendees,
data: evt.attendees.map((attendee) => {
//if attendee is team member, it should fetch their locale not booker's locale
//perhaps make email fetch request to see if his locale is stored, else
const retObj = {
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
locale: attendee.language.locale,
};
return retObj;
}),
},
},
user: {

View File

@ -89,11 +89,25 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
name: true,
email: true,
timeZone: true,
locale: true,
},
rejectOnNotFound: true,
});
const t = await getTranslation(req.body.language ?? "en", "common");
const attendeesListPromises = bookingToDelete.attendees.map(async (attendee) => {
return {
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: {
translate: await getTranslation(attendee.locale ?? "en", "common"),
locale: attendee.locale ?? "en",
},
};
});
const attendeesList = await Promise.all(attendeesListPromises);
const tOrganizer = await getTranslation(organizer.locale ?? "en", "common");
const evt: CalendarEvent = {
title: bookingToDelete?.title,
@ -105,14 +119,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
email: organizer.email,
name: organizer.name ?? "Nameless",
timeZone: organizer.timeZone,
language: { translate: tOrganizer, locale: organizer.locale ?? "en" },
},
attendees: bookingToDelete?.attendees.map((attendee) => {
const retObj = { name: attendee.name, email: attendee.email, timeZone: attendee.timeZone };
return retObj;
}),
attendees: attendeesList,
uid: bookingToDelete?.uid,
location: bookingToDelete?.location,
language: t,
destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar,
};
@ -168,11 +179,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
email: bookingToDelete.user?.email ?? "dev@calendso.com",
name: bookingToDelete.user?.name ?? "no user",
timeZone: bookingToDelete.user?.timeZone ?? "",
language: { translate: tOrganizer, locale: organizer.locale ?? "en" },
},
attendees: bookingToDelete.attendees,
attendees: attendeesList,
location: bookingToDelete.location ?? "",
uid: bookingToDelete.uid ?? "",
language: t,
destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar,
};
await refund(bookingToDelete, evt);

View File

@ -73,7 +73,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
continue;
}
const t = await getTranslation(user.locale ?? "en", "common");
const tOrganizer = await getTranslation(user.locale ?? "en", "common");
const attendeesListPromises = booking.attendees.map(async (attendee) => {
return {
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: {
translate: await getTranslation(attendee.locale ?? "en", "common"),
locale: attendee.locale ?? "en",
},
};
});
const attendeesList = await Promise.all(attendeesListPromises);
const evt: CalendarEvent = {
type: booking.title,
@ -86,10 +100,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
email: user.email,
name,
timeZone: user.timeZone,
language: { translate: tOrganizer, locale: user.locale ?? "en" },
},
attendees: booking.attendees,
attendees: attendeesList,
uid: booking.uid,
language: t,
destinationCalendar: booking.destinationCalendar || user.destinationCalendar,
};

View File

@ -63,8 +63,10 @@ test.describe("integrations", () => {
body.payload.location = dynamic;
for (const attendee of body.payload.attendees) {
attendee.timeZone = dynamic;
attendee.language = dynamic;
}
body.payload.organizer.timeZone = dynamic;
body.payload.organizer.language = dynamic;
body.payload.uid = dynamic;
body.payload.additionInformation = dynamic;

View File

@ -1 +1 @@
{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30min","title":"30min between Pro Example and Test Testson","description":"","startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"Pro Example","email":"pro@example.com","timeZone":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"uid":"[redacted/dynamic]","metadata":{},"additionInformation":"[redacted/dynamic]"}}
{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30min","title":"30min between Pro Example and Test Testson","description":"","startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"Pro Example","email":"pro@example.com","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"uid":"[redacted/dynamic]","metadata":{},"additionInformation":"[redacted/dynamic]"}}

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Attendee" ADD COLUMN "locale" TEXT DEFAULT E'en';

View File

@ -199,6 +199,7 @@ model Attendee {
email String
name String
timeZone String
locale String? @default("en")
booking Booking? @relation(fields: [bookingId], references: [id])
bookingId Int?
}

View File

@ -8,6 +8,7 @@ export const _AttendeeModel = z.object({
email: z.string(),
name: z.string(),
timeZone: z.string(),
locale: z.string().nullish(),
bookingId: z.number().int().nullish(),
});