feat: option to enforce language for bookingPage (#18782)

* feature eventType specific language or bookingPage language

* chore

* fix type checks

* chore

* chore: remove log

* improvement: this updates in DatePicker is not required with context provider method

* added e2e tests for InterfaceLanguage feature

* corrected prev merge conflict issues

* update to align with hooks usage pattern

* chore

* unrelated auto-formatting changes

* unrelated auto-formatting changes

* unrelated auto-formatting changes

* update to affect interfaceLanguage on success page

* undone prettier changes

* update test for success page translation

* chore

* return null from memo

* fix typecheck error due to atom imports in useLocale

* set default interfaceLanguage as null

* updated to use null instead of constant

* enhancements

* fix latest unit test, use prop instead of useSession hook

* fix build issue

* chore

* fix atom build issue

* fix unit test with reqd mock

* update to use server i18n rendering

* nit

* Move import localeOptions to EventSetupTabWebWrapper

* make sure we only display interface language option in web app

* using customI18nProvider

---------

Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: Tushar <tusharbhatt0135@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Ryukemeister <sahalrajiv6900@gmail.com>
Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
This commit is contained in:
Vijay
2025-05-18 01:12:26 -04:00
committed by GitHub
co-authored by amrit Tushar Bhatt Tushar Anik Dhabal Babu Peer Richelsen Ryukemeister Rajiv Sahal Benny Joo
parent 6cb3bdc567
commit ec8f9d5f7a
26 changed files with 390 additions and 21 deletions
@@ -1,7 +1,10 @@
import { CustomI18nProvider } from "app/CustomI18nProvider";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import type { PageProps as ServerPageProps } from "app/_types";
import { cookies, headers } from "next/headers";
import { loadTranslations } from "@calcom/lib/server/i18n";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@server/lib/[user]/[type]/getServerSideProps";
@@ -13,6 +16,18 @@ const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
const props = await getData(context);
const locale = props.eventData?.interfaceLanguage;
if (locale) {
const ns = "common";
const translations = await loadTranslations(locale, ns);
return (
<CustomI18nProvider translations={translations} locale={locale} ns={ns}>
<TypePage {...props} />
</CustomI18nProvider>
);
}
return <TypePage {...props} />;
};
@@ -1,9 +1,11 @@
import { CustomI18nProvider } from "app/CustomI18nProvider";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import { generateMeetingMetadata } from "app/_utils";
import { headers, cookies } from "next/headers";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { loadTranslations } from "@calcom/lib/server/i18n";
import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx";
@@ -55,6 +57,17 @@ const ServerPage = async ({ params, searchParams }: PageProps) => {
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
const props = await getData(legacyCtx);
const locale = props.eventData?.interfaceLanguage;
if (locale) {
const ns = "common";
const translations = await loadTranslations(locale, ns);
return (
<CustomI18nProvider translations={translations} locale={locale} ns={ns}>
<LegacyPage {...props} />
</CustomI18nProvider>
);
}
return <LegacyPage {...props} />;
};
@@ -1,7 +1,10 @@
import { CustomI18nProvider } from "app/CustomI18nProvider";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import type { PageProps as ServerPageProps } from "app/_types";
import { cookies, headers } from "next/headers";
import { loadTranslations } from "@calcom/lib/server/i18n";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
@@ -17,8 +20,32 @@ export type ClientPageProps = UserTypePageProps | TeamTypePageProps;
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
const props = await getData(context);
if ((props as TeamTypePageProps)?.teamId) return <TeamTypePage {...(props as TeamTypePageProps)} />;
return <UserTypePage {...(props as UserTypePageProps)} />;
const eventLocale = props.eventData?.interfaceLanguage;
const ns = "common";
let translations;
if (eventLocale) {
const ns = "common";
translations = await loadTranslations(eventLocale, ns);
}
if ((props as TeamTypePageProps)?.teamId) {
return eventLocale ? (
<CustomI18nProvider translations={translations} locale={eventLocale} ns={ns}>
<TeamTypePage {...(props as TeamTypePageProps)} />
</CustomI18nProvider>
) : (
<TeamTypePage {...(props as TeamTypePageProps)} />
);
}
return eventLocale ? (
<CustomI18nProvider translations={translations} locale={eventLocale} ns={ns}>
<UserTypePage {...(props as UserTypePageProps)} />
</CustomI18nProvider>
) : (
<UserTypePage {...(props as UserTypePageProps)} />
);
};
export default ServerPage;
@@ -1,9 +1,11 @@
import { CustomI18nProvider } from "app/CustomI18nProvider";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import { generateMeetingMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { loadTranslations } from "@calcom/lib/server/i18n";
import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
@@ -63,10 +65,32 @@ const ServerPage = async ({ params, searchParams }: PageProps) => {
const props = await getData(
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
);
if ((props as TeamTypePageProps)?.teamId) {
return <TeamTypePage {...(props as TeamTypePageProps)} />;
const eventLocale = props.eventData?.interfaceLanguage;
const ns = "common";
let translations;
if (eventLocale) {
const ns = "common";
translations = await loadTranslations(eventLocale, ns);
}
return <UserTypePage {...(props as UserTypePageProps)} />;
if ((props as TeamTypePageProps)?.teamId) {
return eventLocale ? (
<CustomI18nProvider translations={translations} locale={eventLocale} ns={ns}>
<TeamTypePage {...(props as TeamTypePageProps)} />
</CustomI18nProvider>
) : (
<TeamTypePage {...(props as TeamTypePageProps)} />
);
}
return eventLocale ? (
<CustomI18nProvider translations={translations} locale={eventLocale} ns={ns}>
<UserTypePage {...(props as UserTypePageProps)} />
</CustomI18nProvider>
) : (
<UserTypePage {...(props as UserTypePageProps)} />
);
};
export default ServerPage;
@@ -1,7 +1,10 @@
import { CustomI18nProvider } from "app/CustomI18nProvider";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import type { PageProps as ServerPageProps } from "app/_types";
import { cookies, headers } from "next/headers";
import { loadTranslations } from "@calcom/lib/server/i18n";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
@@ -12,6 +15,18 @@ const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
const props = await getData(context);
const eventLocale = props.eventData?.interfaceLanguage;
if (eventLocale) {
const ns = "common";
const translations = await loadTranslations(eventLocale, ns);
return (
<CustomI18nProvider translations={translations} locale={eventLocale} ns={ns}>
<TypePage {...props} />
</CustomI18nProvider>
);
}
return <TypePage {...props} />;
};
@@ -1,9 +1,11 @@
import { CustomI18nProvider } from "app/CustomI18nProvider";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import { generateMeetingMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { loadTranslations } from "@calcom/lib/server/i18n";
import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
@@ -54,6 +56,18 @@ const ServerPage = async ({ params, searchParams }: PageProps) => {
const props = await getData(
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
);
const eventLocale = props.eventData?.interfaceLanguage;
if (eventLocale) {
const ns = "common";
const translations = await loadTranslations(eventLocale, ns);
return (
<CustomI18nProvider translations={translations} locale={eventLocale} ns={ns}>
<LegacyPage {...props} />
</CustomI18nProvider>
);
}
return <LegacyPage {...props} />;
};
export default ServerPage;
@@ -1,7 +1,10 @@
import { CustomI18nProvider } from "app/CustomI18nProvider";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import type { PageProps as ServerPageProps } from "app/_types";
import { cookies, headers } from "next/headers";
import { loadTranslations } from "@calcom/lib/server/i18n";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import OldPage from "~/bookings/views/bookings-single-view";
@@ -15,6 +18,18 @@ const getEmbedData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
const props = await getEmbedData(context);
const eventLocale = props.eventType?.interfaceLanguage;
if (eventLocale) {
const ns = "common";
const translations = await loadTranslations(eventLocale, ns);
return (
<CustomI18nProvider translations={translations} locale={eventLocale} ns={ns}>
<OldPage {...props} />
</CustomI18nProvider>
);
}
return <OldPage {...props} />;
};
@@ -1,9 +1,11 @@
import { CustomI18nProvider } from "app/CustomI18nProvider";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { loadTranslations } from "@calcom/lib/server/i18n";
import { BookingStatus } from "@calcom/prisma/enums";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
@@ -36,6 +38,18 @@ const getData = withAppDirSsr<ClientPageProps>(getServerSideProps);
const ServerPage = async ({ params, searchParams }: _PageProps) => {
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
const props = await getData(context);
const eventLocale = props.eventType?.interfaceLanguage;
if (eventLocale) {
const ns = "common";
const translations = await loadTranslations(eventLocale, ns);
return (
<CustomI18nProvider translations={translations} locale={eventLocale} ns={ns}>
<OldPage {...props} />
</CustomI18nProvider>
);
}
return <OldPage {...props} />;
};
export default ServerPage;
+33
View File
@@ -0,0 +1,33 @@
"use client";
import { createContext, useMemo } from "react";
import type { ReactNode } from "react";
type CustomI18nContextType = {
translations: Record<string, string>;
ns: string;
locale: string;
};
export const CustomI18nContext = createContext<CustomI18nContextType | null>(null);
export function CustomI18nProvider({
children,
translations,
locale,
ns,
}: CustomI18nContextType & {
children: ReactNode;
}) {
// Memoize the value to prevent re-renders unless the data changes
const value = useMemo(
() => ({
translations,
locale,
ns,
}),
[locale, ns]
);
return <CustomI18nContext.Provider value={value}>{children}</CustomI18nContext.Provider>;
}
+1
View File
@@ -26,6 +26,7 @@ export const getEventTypesFromDB = async (id: number) => {
id: true,
title: true,
description: true,
interfaceLanguage: true,
length: true,
eventName: true,
recurringEvent: true,
@@ -141,6 +141,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
title: eventData.title,
users: eventHostsUserData,
hidden: eventData.hidden,
interfaceLanguage: eventData.interfaceLanguage,
},
booking,
user: teamSlug,
@@ -206,6 +207,7 @@ const getTeamWithEventsData = async (
hidden: true,
disableCancelling: true,
disableRescheduling: true,
interfaceLanguage: true,
hosts: {
take: 3,
select: {
+139
View File
@@ -421,6 +421,145 @@ test.describe("Event Types tests", () => {
await expect(offerSeatsToggle).toBeDisabled();
});
});
test.describe("Interface Language Tests", () => {
test.use({
locale: "en",
});
test("by default the Interface language has 'Visitor's browser language' selected", async ({
page,
users,
}) => {
await test.step("should create a en user", async () => {
const user = await users.create({
locale: "en",
});
await user.apiLogin();
await page.goto("/event-types");
await page.waitForSelector('[data-testid="event-types"]');
});
await test.step("should open first eventType and check Interface Language", async () => {
await gotoFirstEventType(page);
const interfaceLanguageValue = page
.getByTestId("event-interface-language")
.locator('div[class$="-singleValue"]');
await expect(interfaceLanguageValue).toHaveText("Visitor's browser language");
});
});
test("user can change the interface language to any other language and the booking page should be rendered in that language", async ({
page,
users,
}) => {
await test.step("should create a en user", async () => {
const user = await users.create({
locale: "en",
});
await user.apiLogin();
await page.goto("/event-types");
await page.waitForSelector('[data-testid="event-types"]');
});
await test.step("should open first eventType and change Interface Language to Deutsche", async () => {
await gotoFirstEventType(page);
await page.getByTestId("event-interface-language").click();
await page.locator(`text="Deutsch"`).click();
await saveEventType(page);
});
await test.step("should open corresponding booking page and ensure language rendered is Deutsche", async () => {
await gotoBookingPage(page);
//expect the slot selection page to be rendered in 'Deutsch'
await expect(page.locator(`text="So"`).nth(0)).toBeVisible();
await expect(page.locator(`text="Mo"`).nth(0)).toBeVisible();
await expect(page.locator(`text="Di"`).nth(0)).toBeVisible();
await expect(page.locator(`text="Mi"`).nth(0)).toBeVisible();
await expect(page.locator(`text="Do"`).nth(0)).toBeVisible();
await expect(page.locator(`text="Fr"`).nth(0)).toBeVisible();
await expect(page.locator(`text="Sa"`).nth(0)).toBeVisible();
await expect(page.locator(`text="12 Std"`).nth(0)).toBeVisible();
await expect(page.locator(`text="24 Std"`).nth(0)).toBeVisible();
await selectFirstAvailableTimeSlotNextMonth(page);
//expect the booking inputs page to be rendered in 'Deutsch'
await expect(page.locator(`text="Ihr Name"`).nth(0)).toBeVisible();
await expect(page.locator(`text="E-Mail Adresse"`).nth(0)).toBeVisible();
await expect(page.locator(`text="Zusätzliche Notizen"`).nth(0)).toBeVisible();
await expect(page.locator(`text="+ Weitere Gäste"`).nth(0)).toBeVisible();
await expect(page.locator(`text="Zurück"`).nth(0)).toBeVisible();
await expect(page.locator(`text="Bestätigen"`).nth(0)).toBeVisible();
});
await test.step("should be able to book successfully and ensure success page is rendered in Deutsche", async () => {
await bookTimeSlot(page);
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
await expect(page.locator(`text="Dieser Termin ist geplant"`).nth(0)).toBeVisible();
});
});
test("user locale setting is overridden by event type language setting for booking page", async ({
page,
users,
}) => {
await test.step("should create a de user and ensure app is rendered in de", async () => {
const user = await users.create({
locale: "de",
});
await user.apiLogin();
await page.goto("/event-types");
await page.waitForSelector('[data-testid="event-types"]');
{
const locator = page.getByText("Ereignistypen", { exact: true }).first(); // "general"
await expect(locator).toBeVisible();
}
});
await test.step("should open first eventType and change Interface Language to Español", async () => {
await page.goto("/event-types");
await page.waitForSelector('[data-testid="event-types"]');
await gotoFirstEventType(page);
await page.getByTestId("event-interface-language").click();
await page.getByTestId("select-option-es").click();
await saveEventType(page);
});
await test.step("should go to booking page and verify the Interface language is Español", async () => {
await gotoBookingPage(page);
//expect the slot selection page to be rendered in 'Español'
await expect(page.locator(`text="dom"`).nth(0)).toBeVisible();
await expect(page.locator(`text="lun"`).nth(0)).toBeVisible();
await expect(page.locator(`text="mar"`).nth(0)).toBeVisible();
await expect(page.locator(`text="mié"`).nth(0)).toBeVisible();
await expect(page.locator(`text="jue"`).nth(0)).toBeVisible();
await expect(page.locator(`text="vie"`).nth(0)).toBeVisible();
await expect(page.locator(`text="sáb"`).nth(0)).toBeVisible();
await expect(page.locator(`text="12 h"`).nth(0)).toBeVisible();
await expect(page.locator(`text="24hs"`).nth(0)).toBeVisible();
await selectFirstAvailableTimeSlotNextMonth(page);
//expect the booking inputs page to be rendered in 'Español'
await expect(page.locator(`text="Tu Nombre"`).nth(0)).toBeVisible();
await expect(page.locator(`text="Email"`).nth(0)).toBeVisible();
await expect(page.locator(`text="Notas Adicionales"`).nth(0)).toBeVisible();
await expect(page.locator(`text="Añadir invitados"`).nth(0)).toBeVisible();
await expect(page.locator(`text="Atrás"`).nth(0)).toBeVisible();
await expect(page.locator(`text="Confirmar"`).nth(0)).toBeVisible();
await bookTimeSlot(page);
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
});
await test.step("ensure other components of the App is still rendered in de and not affected by setting eventType Interface Language to Español", async () => {
await page.goto("/event-types");
await page.waitForSelector('[data-testid="event-types"]');
{
const locator = page.getByText("Ereignistypen", { exact: true }).first(); // "general"
await expect(locator).toBeVisible();
}
});
});
});
});
const selectAttendeePhoneNumber = async (page: Page) => {
@@ -3118,6 +3118,8 @@
"could_not_find_slug_to_publish_org": "Could not find slug to publish the organization",
"picklist": "Picklist",
"most_cancelled_bookings": "Most Cancelled Bookings",
"interface_language": "Interface Language",
"visitors_browser_language": "Visitor's browser language",
"name_or_email": "Name or Email",
"salesforce_round_robin_skip_fallback_to_lead_owner": "If no contact is found, fallback to lead owner if it exists",
"credit_purchase_failed": "Credit purchase failed. Please try again or contact support.",
+1
View File
@@ -61,6 +61,7 @@ export type BookerEvent = Pick<
| "autoTranslateDescriptionEnabled"
| "disableCancelling"
| "disableRescheduling"
| "interfaceLanguage"
> & {
subsetOfUsers: BookerEventUser[];
showInstantEventConnectNowModal: boolean;
@@ -18,7 +18,6 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
import { slugify } from "@calcom/lib/slugify";
import turndown from "@calcom/lib/turndownService";
import { Skeleton } from "@calcom/ui/components/skeleton";
import classNames from "@calcom/ui/classNames";
import { Editor } from "@calcom/ui/components/editor";
import { TextAreaField } from "@calcom/ui/components/form";
@@ -26,6 +25,7 @@ import { Label } from "@calcom/ui/components/form";
import { TextField } from "@calcom/ui/components/form";
import { Select } from "@calcom/ui/components/form";
import { SettingsToggle } from "@calcom/ui/components/form";
import { Skeleton } from "@calcom/ui/components/skeleton";
export type EventSetupTabCustomClassNames = {
wrapper?: string;
@@ -58,12 +58,23 @@ export type EventSetupTabProps = Pick<
customClassNames?: EventSetupTabCustomClassNames;
};
export const EventSetupTab = (
props: EventSetupTabProps & { urlPrefix: string; hasOrgBranding: boolean; orgId?: number }
props: EventSetupTabProps & {
urlPrefix: string;
hasOrgBranding: boolean;
orgId?: number;
localeOptions?: { value: string; label: string }[];
}
) => {
const { t } = useLocale();
const isPlatform = useIsPlatform();
const formMethods = useFormContext<FormValues>();
const { eventType, team, urlPrefix, hasOrgBranding, customClassNames, orgId } = props;
const interfaceLanguageOptions =
props.localeOptions && props.localeOptions.length > 0
? [{ label: t("visitors_browser_language"), value: "" }, ...props.localeOptions]
: [];
const [multipleDuration, setMultipleDuration] = useState(
formMethods.getValues("metadata")?.multipleDuration
);
@@ -161,6 +172,34 @@ export const EventSetupTab = (
/>
</div>
)}
{!isPlatform && interfaceLanguageOptions.length > 0 && (
<div>
<Skeleton
as={Label}
loadingClassName="w-16"
htmlFor="interfaceLanguage"
className={customClassNames?.locationSection?.label}>
{t("interface_language")}
{shouldLockIndicator("interfaceLanguage")}
</Skeleton>
<Controller
name="interfaceLanguage"
control={formMethods.control}
defaultValue={eventType.interfaceLanguage ?? ""}
render={({ field: { value, onChange } }) => (
<Select<{ label: string; value: string }>
data-testid="event-interface-language"
className="capitalize"
options={interfaceLanguageOptions}
onChange={(option) => {
onChange(option?.value);
}}
value={interfaceLanguageOptions.find((option) => option.value === value)}
/>
)}
/>
</div>
)}
<TextField
required
label={isPlatform ? "Slug" : t("URL")}
@@ -55,6 +55,7 @@ const getPublicEventSelect = (fetchAllUsers: boolean) => {
id: true,
title: true,
description: true,
interfaceLanguage: true,
eventName: true,
slug: true,
isInstantEvent: true,
@@ -527,6 +528,7 @@ export const getPublicEvent = async (
assignAllTeamMembers: event.assignAllTeamMembers,
disableCancelling: event.disableCancelling,
disableRescheduling: event.disableRescheduling,
interfaceLanguage: event.interfaceLanguage,
};
};
@@ -72,6 +72,7 @@ export type FormValues = {
eventTitle: string;
eventName: string;
slug: string;
interfaceLanguage: string | null;
isInstantEvent: boolean;
instantMeetingParameters: string[];
instantMeetingExpiryTimeOffsetInSeconds: number;
+1
View File
@@ -130,6 +130,7 @@ const commons = {
includeNoShowInRRCalculation: false,
useEventLevelSelectedCalendars: false,
rrResetInterval: null,
interfaceLanguage: null,
customReplyToEmail: null,
};
+3 -1
View File
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
import { useAtomsContext } from "@calcom/atoms/hooks/useAtomsContext";
import { AppRouterI18nContext } from "@calcom/web/app/AppRouterI18nProvider";
import { CustomI18nContext } from "@calcom/web/app/CustomI18nProvider";
type useLocaleReturnType = {
i18n: i18n;
@@ -32,10 +33,11 @@ const serverI18nInstances = new Map();
export const useLocale = (): useLocaleReturnType => {
const appRouterContext = useContext(AppRouterI18nContext);
const customI18nContext = useContext(CustomI18nContext);
const clientI18n = useClientLocale();
if (appRouterContext) {
const { translations, locale, ns } = appRouterContext;
const { translations, locale, ns } = customI18nContext ?? appRouterContext;
const instanceKey = `${locale}-${ns}`;
// Check if we already have an instance for this locale and namespace
+1
View File
@@ -7,6 +7,7 @@ export const eventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
userId: true,
metadata: true,
description: true,
interfaceLanguage: true,
hidden: true,
slug: true,
length: true,
@@ -470,6 +470,7 @@ export class EventTypeRepository {
title: true,
slug: true,
description: true,
interfaceLanguage: true,
length: true,
isInstantEvent: true,
instantMeetingExpiryTimeOffsetInSeconds: true,
+1
View File
@@ -88,6 +88,7 @@ export const buildEventType = (eventType?: Partial<EventType>): EventType => {
title: faker.lorem.sentence(),
slug: faker.lorem.slug(),
description: faker.lorem.paragraph(),
interfaceLanguage: null,
position: 1,
isInstantEvent: false,
instantMeetingParameters: [],
@@ -4,6 +4,7 @@ import { useOrgBranding } from "@calcom/features/ee/organizations/context/provid
import type { EventSetupTabProps } from "@calcom/features/eventtypes/components/tabs/setup/EventSetupTab";
import { EventSetupTab } from "@calcom/features/eventtypes/components/tabs/setup/EventSetupTab";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { localeOptions } from "@calcom/lib/i18n";
const EventSetupTabWebWrapper = (props: EventSetupTabProps) => {
const orgBranding = useOrgBranding();
@@ -16,6 +17,7 @@ const EventSetupTabWebWrapper = (props: EventSetupTabProps) => {
urlPrefix={urlPrefix}
hasOrgBranding={!!orgBranding}
orgId={session.data?.user.org?.id}
localeOptions={localeOptions}
{...props}
/>
);
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "EventType" ADD COLUMN "interfaceLanguage" TEXT;
+14 -13
View File
@@ -69,23 +69,24 @@ model Host {
}
model EventType {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
/// @zod.min(1)
title String
title String
/// @zod.custom(imports.eventTypeSlug)
slug String
description String?
position Int @default(0)
slug String
description String?
interfaceLanguage String?
position Int @default(0)
/// @zod.custom(imports.eventTypeLocations)
locations Json?
locations Json?
/// @zod.min(1)
length Int
offsetStart Int @default(0)
hidden Boolean @default(false)
hosts Host[]
users User[] @relation("user_eventtype")
owner User? @relation("owner", fields: [userId], references: [id], onDelete: Cascade)
userId Int?
length Int
offsetStart Int @default(0)
hidden Boolean @default(false)
hosts Host[]
users User[] @relation("user_eventtype")
owner User? @relation("owner", fields: [userId], references: [id], onDelete: Cascade)
userId Int?
profileId Int?
profile Profile? @relation(fields: [profileId], references: [id], onDelete: Cascade)
+1
View File
@@ -609,6 +609,7 @@ export const downloadLinkSchema = z.object({
export const allManagedEventTypeProps: { [k in keyof Omit<Prisma.EventTypeSelect, "id">]: true } = {
title: true,
description: true,
interfaceLanguage: true,
isInstantEvent: true,
instantMeetingParameters: true,
instantMeetingExpiryTimeOffsetInSeconds: true,