perf: Cache translations per language/per release (#10843)
Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
parent
e8554ed5c5
commit
3f273235a6
|
@ -1,27 +1,33 @@
|
|||
import { useSession } from "next-auth/react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
export function useViewerI18n() {
|
||||
return trpc.viewer.public.i18n.useQuery(undefined, {
|
||||
staleTime: Infinity,
|
||||
/**
|
||||
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
|
||||
* We intend to not cache i18n query
|
||||
**/
|
||||
trpc: {
|
||||
context: { skipBatch: true },
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
const vercelCommitHash = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA || "NA";
|
||||
|
||||
export function useViewerI18n(locale: string) {
|
||||
return trpc.viewer.public.i18n.useQuery(
|
||||
{ locale, CalComVersion: vercelCommitHash },
|
||||
{
|
||||
/**
|
||||
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
|
||||
**/
|
||||
trpc: {
|
||||
context: { skipBatch: true },
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-switches locale client-side to the logged in user's preference
|
||||
*/
|
||||
const I18nLanguageHandler = () => {
|
||||
const session = useSession();
|
||||
const { i18n } = useTranslation("common");
|
||||
const locale = useViewerI18n().data?.locale || i18n.language;
|
||||
const locale = useViewerI18n(session.data?.user.locale || "en").data?.locale || i18n.language;
|
||||
|
||||
useEffect(() => {
|
||||
// bail early when i18n = {}
|
||||
|
|
|
@ -12,9 +12,6 @@ const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonati
|
|||
showToast(t("your_user_profile_updated_successfully"), "success");
|
||||
await utils.viewer.me.invalidate();
|
||||
},
|
||||
async onSettled() {
|
||||
await utils.viewer.public.i18n.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
@ -95,7 +95,6 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||
debouncedApiCall(inputUsernameValue);
|
||||
}, [debouncedApiCall, inputUsernameValue]);
|
||||
|
||||
const utils = trpc.useContext();
|
||||
const updateUsername = trpc.viewer.updateProfile.useMutation({
|
||||
onSuccess: async () => {
|
||||
onSuccessMutation && (await onSuccessMutation());
|
||||
|
@ -105,9 +104,6 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||
onError: (error) => {
|
||||
onErrorMutation && onErrorMutation(error);
|
||||
},
|
||||
async onSettled() {
|
||||
await utils.viewer.public.i18n.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
// when current username isn't set - Go to stripe to check what username he wanted to buy and was it a premium and was it paid for
|
||||
|
|
|
@ -66,8 +66,6 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
|
|||
}
|
||||
}, [inputUsernameValue, debouncedApiCall, currentUsername]);
|
||||
|
||||
const utils = trpc.useContext();
|
||||
|
||||
const updateUsernameMutation = trpc.viewer.updateProfile.useMutation({
|
||||
onSuccess: async () => {
|
||||
onSuccessMutation && (await onSuccessMutation());
|
||||
|
@ -78,9 +76,6 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
|
|||
onError: (error) => {
|
||||
onErrorMutation && onErrorMutation(error);
|
||||
},
|
||||
async onSettled() {
|
||||
await utils.viewer.public.i18n.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const ActionButtons = () => {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||
import type { Session } from "next-auth";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { SessionProvider, useSession } from "next-auth/react";
|
||||
import { EventCollectionProvider } from "next-collect/client";
|
||||
import type { SSRConfig } from "next-i18next";
|
||||
import { appWithTranslation } from "next-i18next";
|
||||
|
@ -69,9 +68,10 @@ type AppPropsWithoutNonce = Omit<AppPropsWithChildren, "pageProps"> & {
|
|||
const CustomI18nextProvider = (props: AppPropsWithoutNonce) => {
|
||||
/**
|
||||
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
|
||||
* We intend to not cache i18n query
|
||||
**/
|
||||
const { i18n, locale } = useViewerI18n().data ?? {
|
||||
const session = useSession();
|
||||
const localeToUse = session.data?.user.locale ?? "en";
|
||||
const { i18n, locale } = useViewerI18n(localeToUse).data ?? {
|
||||
locale: "en",
|
||||
};
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { useSession } from "next-auth/react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
|
@ -20,8 +21,6 @@ import {
|
|||
TimezoneSelect,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import { withQuery } from "@lib/QueryCell";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||
|
@ -45,11 +44,6 @@ interface GeneralViewProps {
|
|||
user: RouterOutputs["viewer"]["me"];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const WithQuery = withQuery(trpc.viewer.public.i18n as any, undefined, {
|
||||
trpc: { context: { skipBatch: true } },
|
||||
});
|
||||
|
||||
const GeneralQueryView = () => {
|
||||
const { t } = useLocale();
|
||||
|
||||
|
@ -58,30 +52,26 @@ const GeneralQueryView = () => {
|
|||
if (!user) {
|
||||
throw new Error(t("something_went_wrong"));
|
||||
}
|
||||
return (
|
||||
<WithQuery
|
||||
success={({ data }) => <GeneralView user={user} localeProp={data.locale} />}
|
||||
customLoader={<SkeletonLoader title={t("general")} description={t("general_description")} />}
|
||||
/>
|
||||
);
|
||||
return <GeneralView user={user} localeProp={user.locale} />;
|
||||
};
|
||||
|
||||
const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
||||
const utils = trpc.useContext();
|
||||
const { t } = useLocale();
|
||||
const { update } = useSession();
|
||||
|
||||
const mutation = trpc.viewer.updateProfile.useMutation({
|
||||
onSuccess: async () => {
|
||||
// Invalidate our previous i18n cache
|
||||
await utils.viewer.public.i18n.invalidate();
|
||||
onSuccess: async (res) => {
|
||||
await utils.viewer.me.invalidate();
|
||||
reset(getValues());
|
||||
showToast(t("settings_updated_successfully"), "success");
|
||||
update(res);
|
||||
},
|
||||
onError: () => {
|
||||
showToast(t("error_updating_settings"), "error");
|
||||
},
|
||||
onSettled: async () => {
|
||||
await utils.viewer.public.i18n.invalidate();
|
||||
await utils.viewer.me.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -9,6 +9,10 @@ import { appRouter } from "@calcom/trpc/server/routers/_app";
|
|||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { i18n } = require("@calcom/config/next-i18next.config");
|
||||
|
||||
// TODO: Consolidate this constant
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
const CalComVersion = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA || "NA";
|
||||
|
||||
/**
|
||||
* Initialize static site rendering tRPC helpers.
|
||||
* Provides a method to prefetch tRPC-queries in a `getStaticProps`-function.
|
||||
|
@ -37,7 +41,7 @@ export async function ssgInit<TParams extends { locale?: string }>(opts: GetStat
|
|||
});
|
||||
|
||||
// always preload i18n
|
||||
await ssg.viewer.public.i18n.fetch();
|
||||
await ssg.viewer.public.i18n.fetch({ locale, CalComVersion });
|
||||
|
||||
return ssg;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,10 @@ import { createProxySSGHelpers } from "@calcom/trpc/react/ssg";
|
|||
import { createContext } from "@calcom/trpc/server/createContext";
|
||||
import { appRouter } from "@calcom/trpc/server/routers/_app";
|
||||
|
||||
// TODO: Consolidate this constant
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
const CalComVersion = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA || "NA";
|
||||
|
||||
/**
|
||||
* Initialize server-side rendering tRPC helpers.
|
||||
* Provides a method to prefetch tRPC-queries in a `getServerSideProps`-function.
|
||||
|
@ -25,7 +29,7 @@ export async function ssrInit(context: GetServerSidePropsContext) {
|
|||
});
|
||||
|
||||
// always preload "viewer.public.i18n"
|
||||
await ssr.viewer.public.i18n.fetch();
|
||||
await ssr.viewer.public.i18n.fetch({ locale, CalComVersion });
|
||||
// So feature flags are available on first render
|
||||
await ssr.viewer.features.map.prefetch();
|
||||
// Provides a better UX to the users who have already upgraded.
|
||||
|
|
|
@ -73,6 +73,7 @@ export async function getServerSession(options: {
|
|||
impersonatedByUID: token.impersonatedByUID ?? undefined,
|
||||
belongsToActiveTeam: token.belongsToActiveTeam,
|
||||
organizationId: token.organizationId,
|
||||
locale: user.locale ?? undefined,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { UserPermissionRole, Membership, Team } from "@prisma/client";
|
||||
import type { Membership, Team, UserPermissionRole } from "@prisma/client";
|
||||
import type { AuthOptions, Session } from "next-auth";
|
||||
import { encode } from "next-auth/jwt";
|
||||
import type { Provider } from "next-auth/providers";
|
||||
|
@ -85,6 +85,7 @@ const providers: Provider[] = [
|
|||
organizationId: true,
|
||||
twoFactorEnabled: true,
|
||||
twoFactorSecret: true,
|
||||
locale: true,
|
||||
organization: {
|
||||
select: {
|
||||
id: true,
|
||||
|
@ -181,6 +182,7 @@ const providers: Provider[] = [
|
|||
role: validateRole(user.role),
|
||||
belongsToActiveTeam: hasActiveTeams,
|
||||
organizationId: user.organizationId,
|
||||
locale: user.locale,
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
@ -225,6 +227,7 @@ if (isSAMLLoginEnabled) {
|
|||
email: profile.email || "",
|
||||
name: `${profile.firstName || ""} ${profile.lastName || ""}`.trim(),
|
||||
email_verified: true,
|
||||
locale: profile.locale,
|
||||
};
|
||||
},
|
||||
options: {
|
||||
|
@ -356,6 +359,7 @@ export const AUTH_OPTIONS: AuthOptions = {
|
|||
if (trigger === "update") {
|
||||
return {
|
||||
...token,
|
||||
locale: session?.locale ?? token.locale,
|
||||
name: session?.name ?? token.name,
|
||||
username: session?.username ?? token.username,
|
||||
email: session?.email ?? token.email,
|
||||
|
@ -372,6 +376,7 @@ export const AUTH_OPTIONS: AuthOptions = {
|
|||
email: true,
|
||||
organizationId: true,
|
||||
role: true,
|
||||
locale: true,
|
||||
teams: {
|
||||
include: {
|
||||
team: true,
|
||||
|
@ -386,7 +391,7 @@ export const AUTH_OPTIONS: AuthOptions = {
|
|||
|
||||
// Check if the existingUser has any active teams
|
||||
const belongsToActiveTeam = checkIfUserBelongsToActiveTeam(existingUser);
|
||||
const { teams, ...existingUserWithoutTeamsField } = existingUser;
|
||||
const { teams: _teams, ...existingUserWithoutTeamsField } = existingUser;
|
||||
|
||||
return {
|
||||
...existingUserWithoutTeamsField,
|
||||
|
@ -416,6 +421,7 @@ export const AUTH_OPTIONS: AuthOptions = {
|
|||
impersonatedByUID: user?.impersonatedByUID,
|
||||
belongsToActiveTeam: user?.belongsToActiveTeam,
|
||||
organizationId: user?.organizationId,
|
||||
locale: user?.locale,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -454,6 +460,7 @@ export const AUTH_OPTIONS: AuthOptions = {
|
|||
impersonatedByUID: token.impersonatedByUID as number,
|
||||
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
|
||||
organizationId: token?.organizationId,
|
||||
locale: existingUser.locale,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -473,6 +480,7 @@ export const AUTH_OPTIONS: AuthOptions = {
|
|||
impersonatedByUID: token.impersonatedByUID as number,
|
||||
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
|
||||
organizationId: token?.organizationId,
|
||||
locale: token.locale,
|
||||
},
|
||||
};
|
||||
return calendsoSession;
|
||||
|
|
|
@ -11,7 +11,7 @@ const teamIdschema = z.object({
|
|||
});
|
||||
|
||||
const auditAndReturnNextUser = async (
|
||||
impersonatedUser: Pick<User, "id" | "username" | "email" | "name" | "role" | "organizationId">,
|
||||
impersonatedUser: Pick<User, "id" | "username" | "email" | "name" | "role" | "organizationId" | "locale">,
|
||||
impersonatedByUID: number,
|
||||
hasTeam?: boolean
|
||||
) => {
|
||||
|
@ -40,6 +40,7 @@ const auditAndReturnNextUser = async (
|
|||
impersonatedByUID,
|
||||
belongsToActiveTeam: hasTeam,
|
||||
organizationId: impersonatedUser.organizationId,
|
||||
locale: impersonatedUser.locale,
|
||||
};
|
||||
|
||||
return obj;
|
||||
|
@ -97,6 +98,7 @@ const ImpersonationProvider = CredentialsProvider({
|
|||
email: true,
|
||||
organizationId: true,
|
||||
disableImpersonation: true,
|
||||
locale: true,
|
||||
teams: {
|
||||
where: {
|
||||
disableImpersonation: false, // Ensure they have impersonation enabled
|
||||
|
|
|
@ -23,9 +23,6 @@ const DisableTeamImpersonation = ({
|
|||
showToast(t("your_user_profile_updated_successfully"), "success");
|
||||
await utils.viewer.teams.getMembershipbyUser.invalidate();
|
||||
},
|
||||
async onSettled() {
|
||||
await utils.viewer.public.i18n.invalidate();
|
||||
},
|
||||
});
|
||||
if (query.isLoading) return <></>;
|
||||
|
||||
|
|
|
@ -59,11 +59,12 @@ export function createNextApiHandler(router: AnyRouter, isPublic = false, namesp
|
|||
if (isPublic && paths) {
|
||||
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
|
||||
const FIVE_MINUTES_IN_SECONDS = 5 * 60;
|
||||
const ONE_YEAR_IN_SECONDS = 31536000;
|
||||
|
||||
const cacheRules = {
|
||||
session: "no-cache",
|
||||
|
||||
// i18n data is user specific and thus should not be cached
|
||||
i18n: "no-cache",
|
||||
i18n: `max-age=${ONE_YEAR_IN_SECONDS}`,
|
||||
|
||||
// FIXME: Using `max-age=1, stale-while-revalidate=60` fails some booking tests.
|
||||
"slots.getSchedule": `no-cache`,
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import sessionMiddleware from "./sessionMiddleware";
|
||||
|
||||
const localeMiddleware = sessionMiddleware.unstable_pipe(async ({ ctx, next }) => {
|
||||
const { serverSideTranslations } = await import("next-i18next/serverSideTranslations");
|
||||
const { user } = ctx;
|
||||
|
||||
const i18n = await serverSideTranslations(
|
||||
user?.locale && user?.locale !== ctx.locale ? user.locale : ctx.locale,
|
||||
["common", "vital"]
|
||||
);
|
||||
|
||||
const locale = user?.locale || ctx.locale;
|
||||
|
||||
return next({
|
||||
ctx: { locale, i18n, user: { ...user, locale } },
|
||||
});
|
||||
});
|
||||
|
||||
export default localeMiddleware;
|
|
@ -1,9 +1,9 @@
|
|||
import localeMiddleware from "../../middlewares/localeMiddleware";
|
||||
import sessionMiddleware from "../../middlewares/sessionMiddleware";
|
||||
import publicProcedure from "../../procedures/publicProcedure";
|
||||
import { router } from "../../trpc";
|
||||
import { slotsRouter } from "../viewer/slots/_router";
|
||||
import { ZEventInputSchema } from "./event.schema";
|
||||
import { i18nInputSchema } from "./i18n.schema";
|
||||
import { ZSamlTenantProductInputSchema } from "./samlTenantProduct.schema";
|
||||
import { ZStripeCheckoutSessionInputSchema } from "./stripeCheckoutSession.schema";
|
||||
|
||||
|
@ -36,7 +36,7 @@ export const publicViewerRouter = router({
|
|||
});
|
||||
}),
|
||||
|
||||
i18n: publicProcedure.use(localeMiddleware).query(async ({ ctx }) => {
|
||||
i18n: publicProcedure.input(i18nInputSchema).query(async ({ ctx, input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.i18n) {
|
||||
UNSTABLE_HANDLER_CACHE.i18n = await import("./i18n.handler").then((mod) => mod.i18nHandler);
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ export const publicViewerRouter = router({
|
|||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.i18n({ ctx });
|
||||
return UNSTABLE_HANDLER_CACHE.i18n({ ctx, input });
|
||||
}),
|
||||
|
||||
countryCode: publicProcedure.query(async ({ ctx }) => {
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import type { WithLocale } from "../../createContext";
|
||||
import type { I18nInputSchema } from "./i18n.schema";
|
||||
|
||||
type I18nOptions = {
|
||||
ctx: WithLocale & {
|
||||
req: NextApiRequest | undefined;
|
||||
res: NextApiResponse | undefined;
|
||||
};
|
||||
input: I18nInputSchema;
|
||||
};
|
||||
|
||||
export const i18nHandler = async ({ ctx }: I18nOptions) => {
|
||||
const { locale, i18n } = ctx;
|
||||
export const i18nHandler = async ({ input }: I18nOptions) => {
|
||||
const { locale } = input;
|
||||
const { serverSideTranslations } = await import("next-i18next/serverSideTranslations");
|
||||
const i18n = await serverSideTranslations(locale, ["common", "vital"]);
|
||||
|
||||
return {
|
||||
i18n,
|
||||
|
|
|
@ -1 +1,8 @@
|
|||
export {};
|
||||
import { z } from "zod";
|
||||
|
||||
export const i18nInputSchema = z.object({
|
||||
locale: z.string(),
|
||||
CalComVersion: z.string(),
|
||||
});
|
||||
|
||||
export type I18nInputSchema = z.infer<typeof i18nInputSchema>;
|
||||
|
|
|
@ -19,6 +19,7 @@ declare module "next-auth" {
|
|||
organizationId?: number | null;
|
||||
username?: PrismaUser["username"];
|
||||
role?: PrismaUser["role"] | "INACTIVE_ADMIN";
|
||||
locale?: string | null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,5 +33,6 @@ declare module "next-auth/jwt" {
|
|||
impersonatedByUID?: number | null;
|
||||
belongsToActiveTeam?: boolean;
|
||||
organizationId?: number | null;
|
||||
locale?: string;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user