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:
co-authored by
amrit
Tushar Bhatt
Tushar
Anik Dhabal Babu
Peer Richelsen
Ryukemeister
Rajiv Sahal
Benny Joo
parent
6cb3bdc567
commit
ec8f9d5f7a
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -130,6 +130,7 @@ const commons = {
|
||||
includeNoShowInRRCalculation: false,
|
||||
useEventLevelSelectedCalendars: false,
|
||||
rrResetInterval: null,
|
||||
interfaceLanguage: null,
|
||||
customReplyToEmail: null,
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "EventType" ADD COLUMN "interfaceLanguage" TEXT;
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user