Repository: calcom/cal.com
Pull request #20597: perf: make `useLocale` hook to use server-fetched i18n if in App router context
Author: login: hbjORbj
Labels: name: i18n
description: area: i18n, translations
name: performance
description: area: performance, page load, slow, slow endpoints, loading screen, unresponsive
name: core
description: area: core, team members only
name: foundation
name: high-risk
description: Requires approval by Foundation team
name: ready-for-e2e
Created: 2025-04-07T22:15:39Z | Merged: 2025-04-11T21:28:27Z

## What does this PR do?

- We currently only use a client-side i18n instance coming from `react-i18next` library.
- In App router, we can use server-fetched i18n. I modified `useLocale` so that it returns server-fetched i18n instance when App Router context is available.
- Since `useLocale` hook is used a lot across the codebase, this PR affects the overall app and hence can be considered high-risk. But at the same time, `useLocale` will use server-fetched i18n only if app router context is available and falls back to client-side i18n otherwise, so it's a pretty safe implementation too.

### Demo


https://github.com/user-attachments/assets/a658f163-81cd-4e50-aa08-30726a0c9375




## Mandatory Tasks (DO NOT REMOVE)

- [x] I have self-reviewed the code (A decent size PR without self-review might be rejected).
- [x] N/A - I have updated the developer docs in /docs if this PR makes changes that would require a [documentation change](https://cal.com/docs). If N/A, write N/A here and check the checkbox.
- [x] I confirm automated tests are in place that prove my fix is effective or that my feature works.

## How should this be tested?

- Covered by existing e2e tests


Diff:
diff --git a/apps/web/app/AppRouterI18nProvider.tsx b/apps/web/app/AppRouterI18nProvider.tsx
new file mode 100644
index 00000000000000..39876e93b6dfed
--- /dev/null
+++ b/apps/web/app/AppRouterI18nProvider.tsx
@@ -0,0 +1,33 @@
+"use client";
+
+import { createContext, useMemo } from "react";
+import type { ReactNode } from "react";
+
+type AppRouterI18nContextType = {
+  translations: Record<string, string>;
+  ns: string;
+  locale: string;
+};
+
+export const AppRouterI18nContext = createContext<AppRouterI18nContextType | null>(null);
+
+export function AppRouterI18nProvider({
+  children,
+  translations,
+  locale,
+  ns,
+}: AppRouterI18nContextType & {
+  children: ReactNode;
+}) {
+  // Memoize the value to prevent re-renders unless the data changes
+  const value = useMemo(
+    () => ({
+      translations,
+      locale,
+      ns,
+    }),
+    [locale, ns]
+  );
+
+  return <AppRouterI18nContext.Provider value={value}>{children}</AppRouterI18nContext.Provider>;
+}
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index 77c67a5d7e380a..8725a7b41bce27 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -5,10 +5,12 @@ import { headers, cookies } from "next/headers";
 import React from "react";
 
 import { getLocale } from "@calcom/features/auth/lib/getLocale";
+import { loadTranslations } from "@calcom/lib/server/i18n";
 import { IconSprites } from "@calcom/ui/components/icon";
 import { NotificationSoundHandler } from "@calcom/web/components/notification-sound-handler";
 
 import "../styles/globals.css";
+import { AppRouterI18nProvider } from "./AppRouterI18nProvider";
 import { SpeculationRules } from "./SpeculationRules";
 import { Providers } from "./providers";
 
@@ -83,10 +85,15 @@ const getInitialProps = async (url: string) => {
   const embedColorScheme = searchParams?.get("ui.color-scheme");
 
   const req = { headers: await headers(), cookies: await cookies() };
-  const newLocale = await getLocale(req);
+  const newLocale = (await getLocale(req)) ?? "en";
   const direction = dir(newLocale);
 
-  return { isEmbed, embedColorScheme, locale: newLocale, direction };
+  return {
+    isEmbed,
+    embedColorScheme,
+    locale: newLocale,
+    direction,
+  };
 };
 
 const getFallbackProps = () => ({
@@ -107,6 +114,8 @@ export default async function RootLayout({ children }: { children: React.ReactNo
   const { locale, direction, isEmbed, embedColorScheme } = isSSG
     ? getFallbackProps()
     : await getInitialProps(fullUrl);
+  const ns = "common";
+  const translations = await loadTranslations(locale, ns);
 
   return (
     <html
@@ -153,7 +162,12 @@ export default async function RootLayout({ children }: { children: React.ReactNo
             "/insights",
           ]}
         />
-        <Providers>{children}</Providers>
+
+        <Providers>
+          <AppRouterI18nProvider translations={translations} locale={locale} ns={ns}>
+            {children}
+          </AppRouterI18nProvider>
+        </Providers>
         {!isEmbed && <NotificationSoundHandler />}
         <NotificationSoundHandler />
       </body>
diff --git a/apps/web/components/PageWrapperAppDir.tsx b/apps/web/components/PageWrapperAppDir.tsx
index 53c9850fc65ef0..20b6507911e2e8 100644
--- a/apps/web/components/PageWrapperAppDir.tsx
+++ b/apps/web/components/PageWrapperAppDir.tsx
@@ -1,7 +1,5 @@
 "use client";
 
-import type { SSRConfig } from "next-i18next/dist/types/types";
-// import I18nLanguageHandler from "@components/I18nLanguageHandler";
 import { usePathname } from "next/navigation";
 import Script from "next/script";
 
@@ -16,7 +14,6 @@ export type PageWrapperProps = Readonly<{
   requiresLicense: boolean;
   nonce: string | undefined;
   isBookingPage?: boolean;
-  i18n?: SSRConfig;
 }>;
 
 function PageWrapper(props: PageWrapperProps) {
@@ -45,7 +42,6 @@ function PageWrapper(props: PageWrapperProps) {
 
   return (
     <AppProviders {...providerProps}>
-      {/* <I18nLanguageHandler locales={props.router.locales || []} /> */}
       <>
         <Script
           nonce={nonce}
diff --git a/apps/web/lib/app-providers-app-dir.tsx b/apps/web/lib/app-providers-app-dir.tsx
index c0110e50f5805c..2f21f69fb27c54 100644
--- a/apps/web/lib/app-providers-app-dir.tsx
+++ b/apps/web/lib/app-providers-app-dir.tsx
@@ -1,16 +1,12 @@
 import { TooltipProvider } from "@radix-ui/react-tooltip";
-import { dir } from "i18next";
 import type { Session } from "next-auth";
 import { useSession } from "next-auth/react";
 import { EventCollectionProvider } from "next-collect/client";
-import { appWithTranslation } from "next-i18next";
-import type { SSRConfig } from "next-i18next/dist/types/types";
 import { ThemeProvider } from "next-themes";
-import type { AppProps as NextAppProps, AppProps as NextJsAppProps } from "next/app";
+import type { AppProps as NextAppProps } from "next/app";
 import type { ReadonlyURLSearchParams } from "next/navigation";
 import { usePathname, useSearchParams } from "next/navigation";
 import type { ReactNode } from "react";
-import { useEffect } from "react";
 
 import DynamicPostHogProvider from "@calcom/features/ee/event-tracking/lib/posthog/providerDynamic";
 import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
@@ -23,17 +19,10 @@ import useIsThemeSupported from "@lib/hooks/useIsThemeSupported";
 import type { WithLocaleProps } from "@lib/withLocale";
 import type { WithNonceProps } from "@lib/withNonce";
 
-import { useViewerI18n } from "@components/I18nLanguageHandler";
 import type { PageWrapperProps } from "@components/PageWrapperAppDir";
 
 import { getThemeProviderProps } from "./getThemeProviderProps";
 
-const I18nextAdapter = appWithTranslation<
-  NextJsAppProps<SSRConfig> & {
-    children: React.ReactNode;
-  }
->(({ children }) => <>{children}</>);
-
 // Workaround for https://github.com/vercel/next.js/issues/8592
 export type AppProps = Omit<
   NextAppProps<
@@ -41,7 +30,6 @@ export type AppProps = Omit<
       WithNonceProps<{
         themeBasis?: string;
         session: Session;
-        i18n?: SSRConfig;
       }>
     >
   >,
@@ -64,57 +52,6 @@ const getEmbedNamespace = (searchParams: ReadonlyURLSearchParams) => {
   return typeof window !== "undefined" ? window.getEmbedNamespace() : searchParams.get("embed") ?? null;
 };
 
-const CustomI18nextProvider = (props: { children: React.ReactElement; i18n?: SSRConfig }) => {
-  /**
-   * i18n should never be clubbed with other queries, so that it's caching can be managed independently.
-   **/
-
-  const session = useSession();
-  const fallbackLocale =
-    typeof window !== "undefined" && window.document.documentElement.lang
-      ? window.document.documentElement.lang
-      : "en";
-  const newLocale = typeof window !== "undefined" && window.calNewLocale ? window.calNewLocale : null;
-  const locale = session?.data?.user.locale ?? newLocale ?? fallbackLocale;
-
-  useEffect(() => {
-    try {
-      // @ts-expect-error TS2790: The operand of a 'delete' operator must be optional.
-      delete window.document.documentElement["lang"];
-
-      window.document.documentElement.lang = locale;
-
-      // Next.js writes the locale to the same attribute
-      // https://github.com/vercel/next.js/blob/1609da2d9552fed48ab45969bdc5631230c6d356/packages/next/src/shared/lib/router/router.ts#L1786
-      // which can result in a race condition
-      // this property descriptor ensures this never happens
-      Object.defineProperty(window.document.documentElement, "lang", {
-        configurable: true,
-        // value: locale,
-        set: function (this) {
-          // empty setter on purpose
-        },
-        get: function () {
-          return locale;
-        },
-      });
-    } catch (error) {
-      console.error(error);
-
-      window.document.documentElement.lang = locale;
-    }
-    window.document.dir = dir(locale);
-  }, [locale]);
-
-  const clientViewerI18n = useViewerI18n(locale);
-  const i18n = clientViewerI18n.data?.i18n ?? props.i18n;
-
-  return (
-    // @ts-expect-error AppWithTranslationHoc expects AppProps
-    <I18nextAdapter pageProps={{ _nextI18Next: i18n?._nextI18Next }}>{props.children}</I18nextAdapter>
-  );
-};
-
 type CalcomThemeProps = Readonly<{
   isBookingPage: boolean;
   nonce: string | undefined;
@@ -178,19 +115,17 @@ const AppProviders = (props: PageWrapperProps) => {
 
   const RemainingProviders = (
     <EventCollectionProvider options={{ apiPath: "/api/collect-events" }}>
-      <CustomI18nextProvider i18n={props.i18n}>
-        <TooltipProvider>
-          {/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
-          <CalcomThemeProvider
-            nonce={props.nonce}
-            isThemeSupported={isThemeSupported}
-            isBookingPage={props.isBookingPage || isBookingPage}>
-            <FeatureFlagsProvider>
-              <OrgBrandProvider>{props.children}</OrgBrandProvider>
-            </FeatureFlagsProvider>
-          </CalcomThemeProvider>
-        </TooltipProvider>
-      </CustomI18nextProvider>
+      <TooltipProvider>
+        {/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
+        <CalcomThemeProvider
+          nonce={props.nonce}
+          isThemeSupported={isThemeSupported}
+          isBookingPage={props.isBookingPage || isBookingPage}>
+          <FeatureFlagsProvider>
+            <OrgBrandProvider>{props.children}</OrgBrandProvider>
+          </FeatureFlagsProvider>
+        </CalcomThemeProvider>
+      </TooltipProvider>
     </EventCollectionProvider>
   );
 
diff --git a/packages/lib/hooks/useLocale.ts b/packages/lib/hooks/useLocale.ts
index 723e2692150ebe..8fcd3efe341ac3 100644
--- a/packages/lib/hooks/useLocale.ts
+++ b/packages/lib/hooks/useLocale.ts
@@ -1,18 +1,24 @@
+import { createInstance } from "i18next";
+import type { TFunction, i18n } from "i18next";
+import { useContext } from "react";
 import { useTranslation } from "react-i18next";
 
 import { useAtomsContext } from "@calcom/atoms/hooks/useAtomsContext";
+import { AppRouterI18nContext } from "@calcom/web/app/AppRouterI18nProvider";
 
-export const useLocale = (namespace: Parameters<typeof useTranslation>[0] = "common") => {
+type useLocaleReturnType = {
+  i18n: i18n;
+  t: TFunction;
+  isLocaleReady: boolean;
+};
+
+// @internal
+const useClientLocale = (namespace: Parameters<typeof useTranslation>[0] = "common"): useLocaleReturnType => {
   const context = useAtomsContext();
   const { i18n, t } = useTranslation(namespace);
   const isLocaleReady = Object.keys(i18n).length > 0;
-
   if (context?.clientId) {
-    return { i18n: context.i18n, t: context.t, isLocaleReady: true } as unknown as {
-      i18n: ReturnType<typeof useTranslation>["i18n"];
-      t: ReturnType<typeof useTranslation>["t"];
-      isLocaleReady: boolean;
-    };
+    return { i18n: context.i18n, t: context.t, isLocaleReady: true } as unknown as useLocaleReturnType;
   }
   return {
     i18n,
@@ -20,3 +26,46 @@ export const useLocale = (namespace: Parameters<typeof useTranslation>[0] = "com
     isLocaleReady,
   };
 };
+
+// @internal
+const serverI18nInstances = new Map();
+
+export const useLocale = (): useLocaleReturnType => {
+  const appRouterContext = useContext(AppRouterI18nContext);
+  const clientI18n = useClientLocale();
+
+  if (appRouterContext) {
+    const { translations, locale, ns } = appRouterContext;
+    const instanceKey = `${locale}-${ns}`;
+
+    // Check if we already have an instance for this locale and namespace
+    if (!serverI18nInstances.has(instanceKey)) {
+      const i18n = createInstance();
+      i18n.init({
+        lng: locale,
+        resources: {
+          [locale]: {
+            [ns]: translations,
+          },
+        },
+      });
+
+      serverI18nInstances.set(instanceKey, {
+        t: i18n.getFixedT(locale, ns),
+        isLocaleReady: true,
+        i18n,
+      });
+    }
+
+    return serverI18nInstances.get(instanceKey);
+  }
+
+  console.warn(
+    "useLocale hook is being used outside of App Router - hence this hook will use a global, client-side i18n which can cause a small flicker"
+  );
+  return {
+    t: clientI18n.t,
+    isLocaleReady: clientI18n.isLocaleReady,
+    i18n: clientI18n.i18n,
+  };
+};
diff --git a/packages/lib/server/i18n.ts b/packages/lib/server/i18n.ts
index 2250051aaf6f46..3e092b330917c7 100644
--- a/packages/lib/server/i18n.ts
+++ b/packages/lib/server/i18n.ts
@@ -2,31 +2,79 @@ import { createInstance } from "i18next";
 
 import { WEBAPP_URL } from "@calcom/lib/constants";
 
+const translationCache = new Map<string, Record<string, string>>();
 const i18nInstanceCache = new Map<string, any>();
 
+/**
+ * Loads English fallback translations for when requested locale translations fail
+ * Implements caching to avoid redundant network requests
+ * @returns {Promise<Record<string, string>>} English translations object or empty object on failure
+ */
 async function loadFallbackTranslations() {
+  const cacheKey = "en-common";
+
+  if (translationCache.has(cacheKey)) {
+    return translationCache.get(cacheKey);
+  }
+
   try {
-    const res = await fetch(`${WEBAPP_URL}/static/locales/en/common.json`);
-    if (res) {
-      return await res.json();
+    const res = await fetch(`${WEBAPP_URL}/static/locales/en/common.json`, {
+      cache: process.env.NODE_ENV === "production" ? "force-cache" : "no-store",
+    });
+
+    if (!res.ok) {
+      throw new Error(`Failed to fetch fallback translations: ${res.status}`);
     }
-  } catch {
-    console.error("Could not fetch fallback translations.");
+
+    const translations = await res.json();
+    translationCache.set(cacheKey, translations);
+    return translations;
+  } catch (error) {
+    console.error("Could not fetch fallback translations:", error);
+    return {};
   }
-  return;
 }
 
-async function loadTranslations(locale: string, ns: string) {
+/**
+ * Loads translations for a specific locale and namespace with optimized caching
+ * @param {string} _locale - The locale code (e.g., 'en', 'fr', 'zh')
+ * @param {string} ns - The namespace for the translations
+ * @returns {Promise<Record<string, string>>} Translations object or fallback translations on failure
+ */
+export async function loadTranslations(_locale: string, ns: string) {
+  const locale = _locale === "zh" ? "zh-CN" : _locale;
+  const cacheKey = `${locale}-${ns}`;
+
+  if (translationCache.has(cacheKey)) {
+    return translationCache.get(cacheKey);
+  }
+
   try {
     const url = `${WEBAPP_URL}/static/locales/${locale}/${ns}.json`;
-    const response = await fetch(url);
-    return await response.json();
+    const response = await fetch(url, {
+      cache: process.env.NODE_ENV === "production" ? "force-cache" : "no-store",
+    });
+
+    if (!response.ok) {
+      throw new Error(`Failed to fetch translations: ${response.status}`);
+    }
+
+    const translations = await response.json();
+    translationCache.set(cacheKey, translations);
+    return translations;
   } catch (error) {
+    console.warn(`Failed to load translations for ${locale}/${ns}, falling back to English:`, error);
     const fallbackTranslations = await loadFallbackTranslations();
-    return fallbackTranslations ?? {};
+    return fallbackTranslations;
   }
 }
 
+/**
+ * Creates or retrieves a cached i18next translation function for the specified locale and namespace
+ * @param {string} locale - The locale code (e.g., 'en', 'fr')
+ * @param {string} ns - The namespace for the translations
+ * @returns {Promise<Function>} A translation function bound to the specified locale and namespace
+ */
 export const getTranslation = async (locale: string, ns: string) => {
   const cacheKey = `${locale}-${ns}`;
   if (i18nInstanceCache.has(cacheKey)) {
@@ -46,6 +94,7 @@ export const getTranslation = async (locale: string, ns: string) => {
     fallbackLng: "en",
   });
 
+  // Cache the i18n instance
   i18nInstanceCache.set(cacheKey, _i18n);
   return _i18n.getFixedT(locale, ns);
 };
diff --git a/packages/ui/components/unpublished-entity/UnpublishedEntity.tsx b/packages/ui/components/unpublished-entity/UnpublishedEntity.tsx
index 95423945ca3bc2..e3999957455a76 100644
--- a/packages/ui/components/unpublished-entity/UnpublishedEntity.tsx
+++ b/packages/ui/components/unpublished-entity/UnpublishedEntity.tsx
@@ -32,6 +32,11 @@ export function UnpublishedEntity(props: UnpublishedEntityProps) {
         avatar={<Avatar alt={slug ?? ""} imageSrc={getPlaceholderAvatar(props.logoUrl, slug)} size="lg" />}
         headline={t("team_is_unpublished", {
           team: props.name,
+          interpolation: {
+            // This is needed because React automatically escapes special characters like apostrophes
+            // for security (XSS prevention), but we want to display the actual apostrophe in the UI
+            escapeValue: false,
+          },
         })}
         description={t(`${props.orgSlug ? "org" : "team"}_is_unpublished_description`)}
       />


Reviews:
user: login: hbjORbj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T15:15:56Z
user: login: hbjORbj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T16:24:08Z
user: login: hbjORbj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T18:46:08Z
user: login: hbjORbj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T18:47:59Z
user: login: cubic-dev-ai[bot]
body: AI code reviewer found 3 issues
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T18:55:32Z
user: login: zomars
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T22:17:36Z
user: login: zomars
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T22:18:01Z
user: login: zomars
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T22:18:48Z
user: login: zomars
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T22:20:42Z
user: login: zomars
body: Left some comments before approving
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T22:20:59Z
user: login: hbjORbj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T22:29:24Z
user: login: hbjORbj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T22:29:56Z
user: login: hbjORbj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T22:33:27Z
user: login: hbjORbj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T22:55:40Z
user: login: hbjORbj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:09:38Z
user: login: zomars
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:10:37Z
user: login: hbjORbj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:11:22Z
user: login: zomars
body: Let's not introduce waitFortimeouts for tests
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:12:36Z
user: login: zomars
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:12:37Z
user: login: zomars
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:12:40Z
user: login: hbjORbj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:43:58Z
user: login: hbjORbj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:47:42Z
user: login: hbjORbj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:48:41Z
user: login: hbjORbj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:50:38Z
user: login: zomars
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:53:29Z
user: login: hbjORbj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:54:48Z
user: login: zomars
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:55:46Z
user: login: hbjORbj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:56:37Z
user: login: hbjORbj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T00:01:57Z
user: login: hbjORbj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T00:06:50Z
user: login: hbjORbj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T20:19:21Z
user: login: hbjORbj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T20:32:53Z
user: login: cubic-dev-ai[bot]
body: mrge found 2 issues. View them in [mrge.io](https://mrge.io/pr/calcom/cal.com/pull/20597)
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T21:00:22Z
user: login: zomars
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T21:28:25Z

Review comments:
diff_hunk: @@ -16,7 +16,7 @@ async function loadFallbackTranslations() {
   return;
 }
 
-async function loadTranslations(locale: string, ns: string) {
+export async function loadTranslations(locale: string, ns: string) {
path: packages/lib/server/i18n.ts
user: login: hbjORbj
body: exporting this so that we can use it in `app/AppRouterI18nProvider.tsx`
created_at: 2025-04-08T15:15:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+"use client";
+
+import { createContext } from "react";
+import type { ReactNode } from "react";
+
+type AppRouterI18nContextType = {
+  translations: Record<string, string>;
+  ns: string;
+  locale: string;
+};
+
+export const AppRouterI18nContext = createContext<AppRouterI18nContextType | null>(null);
+
+export function AppRouterI18nProvider({
+  children,
+  translations,
+  locale,
+  ns,
+}: AppRouterI18nContextType & {
+  children: ReactNode;
+}) {
+  return (
+    <AppRouterI18nContext.Provider value={{ translations, locale, ns }}>
path: apps/web/app/AppRouterI18nProvider.tsx
user: login: hbjORbj
body: At first, I tried drilling `t` function itself, but it threw 
```
Error: Functions cannot be passed directly to Client Components ...
```

So, I am instead passing `translations`
created_at: 2025-04-09T16:24:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,10 +24,6 @@ test.describe("Onboarding", () => {
       await expect(page.locator('text="Next Step"')).toBeVisible(); // Fix race condition
 
       await test.step("step 1 - User Settings", async () => {
-        // Check required fields
-        await page.locator("button[type=submit]").click();
-        await expect(page.locator("data-testid=required")).toBeVisible();
path: apps/web/playwright/onboarding.e2e.ts
user: login: hbjORbj
body: not really needed
created_at: 2025-04-09T18:46:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,9 +30,12 @@ export function UnpublishedEntity(props: UnpublishedEntityProps) {
     <div className="m-8 flex items-center justify-center">
       <EmptyScreen
         avatar={<Avatar alt={slug ?? ""} imageSrc={getPlaceholderAvatar(props.logoUrl, slug)} size="lg" />}
+        // Replace HTML entity for apostrophe (&#39;) with the actual character (')
+        // This is needed because React automatically escapes special characters like apostrophes
+        // for security (XSS prevention), but we want to display the actual apostrophe in the UI
         headline={t("team_is_unpublished", {
           team: props.name,
-        })}
+        }).replace("&#39;", "'")}
path: packages/ui/components/unpublished-entity/UnpublishedEntity.tsx
user: login: hbjORbj
body: If `props.name` contains an apostrophe, e.g., `User's Team`, React turns it into `User&#39;s Team`
created_at: 2025-04-09T18:47:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,22 +1,73 @@
+import { createInstance } from "i18next";
+import type { TFunction, i18n } from "i18next";
+import { useContext } from "react";
 import { useTranslation } from "react-i18next";
 
 import { useAtomsContext } from "@calcom/atoms/hooks/useAtomsContext";
+import { AppRouterI18nContext } from "@calcom/web/app/AppRouterI18nProvider";
 
-export const useLocale = (namespace: Parameters<typeof useTranslation>[0] = "common") => {
+type useLocaleReturnType = {
+  i18n: i18n;
+  t: TFunction;
+  isLocaleReady: boolean;
+};
+
+// @internal
+const useClientLocale = (namespace: Parameters<typeof useTranslation>[0] = "common"): useLocaleReturnType => {
   const context = useAtomsContext();
   const { i18n, t } = useTranslation(namespace);
   const isLocaleReady = Object.keys(i18n).length > 0;
-
   if (context?.clientId) {
-    return { i18n: context.i18n, t: context.t, isLocaleReady: true } as unknown as {
-      i18n: ReturnType<typeof useTranslation>["i18n"];
-      t: ReturnType<typeof useTranslation>["t"];
-      isLocaleReady: boolean;
-    };
+    return { i18n: context.i18n, t: context.t, isLocaleReady: true } as unknown as useLocaleReturnType;
   }
   return {
     i18n,
     t,
     isLocaleReady,
   };
 };
+
+// @internal
+const serverI18nInstances = new Map();
+
+export const useLocale = (): useLocaleReturnType => {
+  const appRouterContext = useContext(AppRouterI18nContext);
+  const clientI18n = useClientLocale();
+
+  if (appRouterContext) {
+    const { translations, locale: _locale, ns } = appRouterContext;
+    const locale = _locale === "zh" ? "zh-CN" : _locale; // We use zh-CN for zh translations
+    const instanceKey = `${locale}-${ns}`;
+
+    // Check if we already have an instance for this locale and namespace
+    if (!serverI18nInstances.has(instanceKey)) {
+      const i18n = createInstance();
+      i18n.init({
+        lng: locale,
+        resources: {
+          [locale]: {
+            [ns]: translations,
+          },
+        },
+        fallbackLng: "en",
+      });
+
+      serverI18nInstances.set(instanceKey, {
+        t: i18n.getFixedT(locale, ns),
+        isLocaleReady: true,
+        i18n,
+      });
+    }
+
+    return serverI18nInstances.get(instanceKey);
+  }
+
+  console.warn(
path: packages/lib/hooks/useLocale.ts
user: login: cubic-dev-ai[bot]
body: Console warning message incorrectly refers to 'useT hook' instead of 'useLocale hook'
created_at: 2025-04-09T18:55:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,22 +1,73 @@
+import { createInstance } from "i18next";
+import type { TFunction, i18n } from "i18next";
+import { useContext } from "react";
 import { useTranslation } from "react-i18next";
 
 import { useAtomsContext } from "@calcom/atoms/hooks/useAtomsContext";
+import { AppRouterI18nContext } from "@calcom/web/app/AppRouterI18nProvider";
 
-export const useLocale = (namespace: Parameters<typeof useTranslation>[0] = "common") => {
+type useLocaleReturnType = {
+  i18n: i18n;
+  t: TFunction;
+  isLocaleReady: boolean;
+};
+
+// @internal
+const useClientLocale = (namespace: Parameters<typeof useTranslation>[0] = "common"): useLocaleReturnType => {
   const context = useAtomsContext();
   const { i18n, t } = useTranslation(namespace);
   const isLocaleReady = Object.keys(i18n).length > 0;
-
   if (context?.clientId) {
-    return { i18n: context.i18n, t: context.t, isLocaleReady: true } as unknown as {
-      i18n: ReturnType<typeof useTranslation>["i18n"];
-      t: ReturnType<typeof useTranslation>["t"];
-      isLocaleReady: boolean;
-    };
+    return { i18n: context.i18n, t: context.t, isLocaleReady: true } as unknown as useLocaleReturnType;
   }
   return {
     i18n,
     t,
     isLocaleReady,
   };
 };
+
+// @internal
+const serverI18nInstances = new Map();
+
+export const useLocale = (): useLocaleReturnType => {
path: packages/lib/hooks/useLocale.ts
user: login: cubic-dev-ai[bot]
body: The updated useLocale hook no longer accepts a namespace parameter that was present in the original implementation
created_at: 2025-04-09T18:55:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,22 +1,73 @@
+import { createInstance } from "i18next";
+import type { TFunction, i18n } from "i18next";
+import { useContext } from "react";
 import { useTranslation } from "react-i18next";
 
 import { useAtomsContext } from "@calcom/atoms/hooks/useAtomsContext";
+import { AppRouterI18nContext } from "@calcom/web/app/AppRouterI18nProvider";
 
-export const useLocale = (namespace: Parameters<typeof useTranslation>[0] = "common") => {
+type useLocaleReturnType = {
+  i18n: i18n;
+  t: TFunction;
+  isLocaleReady: boolean;
+};
+
+// @internal
+const useClientLocale = (namespace: Parameters<typeof useTranslation>[0] = "common"): useLocaleReturnType => {
   const context = useAtomsContext();
   const { i18n, t } = useTranslation(namespace);
   const isLocaleReady = Object.keys(i18n).length > 0;
-
   if (context?.clientId) {
-    return { i18n: context.i18n, t: context.t, isLocaleReady: true } as unknown as {
-      i18n: ReturnType<typeof useTranslation>["i18n"];
-      t: ReturnType<typeof useTranslation>["t"];
-      isLocaleReady: boolean;
-    };
+    return { i18n: context.i18n, t: context.t, isLocaleReady: true } as unknown as useLocaleReturnType;
   }
   return {
     i18n,
     t,
     isLocaleReady,
   };
 };
+
+// @internal
+const serverI18nInstances = new Map();
path: packages/lib/hooks/useLocale.ts
user: login: cubic-dev-ai[bot]
body: Map is created without type parameters, losing type safety
created_at: 2025-04-09T18:55:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -55,7 +55,6 @@ test.describe("Can signup from a team invite", async () => {
     await newPage.fill('input[name="username"]', proUser.username); // Invalid username
     await newPage.fill('input[name="password"]', testUser.password);
     await submitAndWaitForResponse(newPage, "/api/auth/signup", { expectedStatusCode: 409 });
-    await expect(newPage.locator('text="Username or email is already taken"')).toBeVisible();
path: apps/web/playwright/auth/auth-index.e2e.ts
user: login: zomars
body: Why were these removed @hbjORbj ?

Aren't these ensuring proper i18n funcationality?
created_at: 2025-04-09T22:17:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,10 +24,6 @@ test.describe("Onboarding", () => {
       await expect(page.locator('text="Next Step"')).toBeVisible(); // Fix race condition
 
       await test.step("step 1 - User Settings", async () => {
-        // Check required fields
-        await page.locator("button[type=submit]").click();
-        await expect(page.locator("data-testid=required")).toBeVisible();
path: apps/web/playwright/onboarding.e2e.ts
user: login: zomars
body: Why not?
created_at: 2025-04-09T22:18:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+"use client";
+
+import { createContext } from "react";
+import type { ReactNode } from "react";
+
+type AppRouterI18nContextType = {
+  translations: Record<string, string>;
+  ns: string;
+  locale: string;
+};
+
+export const AppRouterI18nContext = createContext<AppRouterI18nContextType | null>(null);
+
+export function AppRouterI18nProvider({
+  children,
+  translations,
+  locale,
+  ns,
+}: AppRouterI18nContextType & {
+  children: ReactNode;
+}) {
+  return (
+    <AppRouterI18nContext.Provider value={{ translations, locale, ns }}>
path: apps/web/app/AppRouterI18nProvider.tsx
user: login: zomars
body: Quick Q. Will this pollute the initial HTML downloading all translations on each page render?
created_at: 2025-04-09T22:18:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,9 +30,12 @@ export function UnpublishedEntity(props: UnpublishedEntityProps) {
     <div className="m-8 flex items-center justify-center">
       <EmptyScreen
         avatar={<Avatar alt={slug ?? ""} imageSrc={getPlaceholderAvatar(props.logoUrl, slug)} size="lg" />}
+        // Replace HTML entity for apostrophe (&#39;) with the actual character (')
+        // This is needed because React automatically escapes special characters like apostrophes
+        // for security (XSS prevention), but we want to display the actual apostrophe in the UI
         headline={t("team_is_unpublished", {
           team: props.name,
-        })}
+        }).replace("&#39;", "'")}
path: packages/ui/components/unpublished-entity/UnpublishedEntity.tsx
user: login: zomars
body: Do we have a cleaner approach for proper decoding here? This feels like a hack
created_at: 2025-04-09T22:20:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,6 +16,8 @@ test.describe("Forgot password", async () => {
     await page.goto("/auth/forgot-password");
     await page.waitForSelector("text=Forgot Password?");
 
+    // eslint-disable-next-line playwright/no-wait-for-timeout
+    await page.waitForTimeout(300);
path: apps/web/playwright/auth/forgot-password.e2e.ts
user: login: hbjORbj
body: Because page loads are faster now, fill event fails irregularly, making tests flaky
created_at: 2025-04-09T22:29:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -55,7 +55,6 @@ test.describe("Can signup from a team invite", async () => {
     await newPage.fill('input[name="username"]', proUser.username); // Invalid username
     await newPage.fill('input[name="password"]', testUser.password);
     await submitAndWaitForResponse(newPage, "/api/auth/signup", { expectedStatusCode: 409 });
-    await expect(newPage.locator('text="Username or email is already taken"')).toBeVisible();
path: apps/web/playwright/auth/auth-index.e2e.ts
user: login: hbjORbj
body: Oh it was unintentional. I just added it back
created_at: 2025-04-09T22:29:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,10 +24,6 @@ test.describe("Onboarding", () => {
       await expect(page.locator('text="Next Step"')).toBeVisible(); // Fix race condition
 
       await test.step("step 1 - User Settings", async () => {
-        // Check required fields
-        await page.locator("button[type=submit]").click();
-        await expect(page.locator("data-testid=required")).toBeVisible();
path: apps/web/playwright/onboarding.e2e.ts
user: login: hbjORbj
body: ah I meant to revert this, reverted now!
created_at: 2025-04-09T22:33:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,9 +30,12 @@ export function UnpublishedEntity(props: UnpublishedEntityProps) {
     <div className="m-8 flex items-center justify-center">
       <EmptyScreen
         avatar={<Avatar alt={slug ?? ""} imageSrc={getPlaceholderAvatar(props.logoUrl, slug)} size="lg" />}
+        // Replace HTML entity for apostrophe (&#39;) with the actual character (')
+        // This is needed because React automatically escapes special characters like apostrophes
+        // for security (XSS prevention), but we want to display the actual apostrophe in the UI
         headline={t("team_is_unpublished", {
           team: props.name,
-        })}
+        }).replace("&#39;", "'")}
path: packages/ui/components/unpublished-entity/UnpublishedEntity.tsx
user: login: hbjORbj
body: yeah we can actually do this!
```
headline={t("team_is_unpublished", {
  team: props.name,
  interpolation: {
    escapeValue: false
  }
})}
```
created_at: 2025-04-09T22:55:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,9 +30,12 @@ export function UnpublishedEntity(props: UnpublishedEntityProps) {
     <div className="m-8 flex items-center justify-center">
       <EmptyScreen
         avatar={<Avatar alt={slug ?? ""} imageSrc={getPlaceholderAvatar(props.logoUrl, slug)} size="lg" />}
+        // Replace HTML entity for apostrophe (&#39;) with the actual character (')
+        // This is needed because React automatically escapes special characters like apostrophes
+        // for security (XSS prevention), but we want to display the actual apostrophe in the UI
         headline={t("team_is_unpublished", {
           team: props.name,
-        })}
+        }).replace("&#39;", "'")}
path: packages/ui/components/unpublished-entity/UnpublishedEntity.tsx
user: login: hbjORbj
body: Also, I checked if static translation also has this issue, but fortunately not. t("profile") where `"profile": "Profile's Benny"` will look fine like below:
<img width="227" alt="Screenshot 2025-04-09 at 7 09 04 PM" src="https://github.com/user-attachments/assets/b7ce5c28-c50f-4f47-93cb-925144cc2ffa" />

created_at: 2025-04-09T23:09:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,6 +16,8 @@ test.describe("Forgot password", async () => {
     await page.goto("/auth/forgot-password");
     await page.waitForSelector("text=Forgot Password?");
 
+    // eslint-disable-next-line playwright/no-wait-for-timeout
+    await page.waitForTimeout(300);
path: apps/web/playwright/auth/forgot-password.e2e.ts
user: login: zomars
body: This is an anti-pattern for a reason. Let's rethink our approach here. We might wait for a specific endpoint response or another selector.
created_at: 2025-04-09T23:10:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+"use client";
+
+import { createContext } from "react";
+import type { ReactNode } from "react";
+
+type AppRouterI18nContextType = {
+  translations: Record<string, string>;
+  ns: string;
+  locale: string;
+};
+
+export const AppRouterI18nContext = createContext<AppRouterI18nContextType | null>(null);
+
+export function AppRouterI18nProvider({
+  children,
+  translations,
+  locale,
+  ns,
+}: AppRouterI18nContextType & {
+  children: ReactNode;
+}) {
+  return (
+    <AppRouterI18nContext.Provider value={{ translations, locale, ns }}>
path: apps/web/app/AppRouterI18nProvider.tsx
user: login: hbjORbj
body: ah that's a great point. let me check.
created_at: 2025-04-09T23:11:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -67,7 +69,8 @@ test.describe("Forgot password", async () => {
 
     // Wait for page to fully load
     await page.waitForSelector("text=Reset Password");
-
+    // eslint-disable-next-line playwright/no-wait-for-timeout
+    await page.waitForTimeout(300);
path: apps/web/playwright/auth/forgot-password.e2e.ts
user: login: zomars
body: Same here
created_at: 2025-04-09T23:12:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,6 +56,8 @@ export function createWorkflowPageFixture(page: Page) {
   };
 
   const fillNameInput = async (name: string) => {
+    // eslint-disable-next-line playwright/no-wait-for-timeout
+    await page.waitForTimeout(300);
path: apps/web/playwright/fixtures/workflows.ts
user: login: zomars
body: And here. Now that I think about it. This could be a symptom of flickering since the test might be finding the "workflow-name" selector, then a flicker happens and the fill fails. This could be solved by using proper skeletons or finding the reason for the flicker.
created_at: 2025-04-09T23:12:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,6 +16,8 @@ test.describe("Forgot password", async () => {
     await page.goto("/auth/forgot-password");
     await page.waitForSelector("text=Forgot Password?");
 
+    // eslint-disable-next-line playwright/no-wait-for-timeout
+    await page.waitForTimeout(300);
path: apps/web/playwright/auth/forgot-password.e2e.ts
user: login: hbjORbj
body: true, hmm what could it be

can you use the app locally and see if you have a clue?
created_at: 2025-04-09T23:43:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+"use client";
+
+import { createContext } from "react";
+import type { ReactNode } from "react";
+
+type AppRouterI18nContextType = {
+  translations: Record<string, string>;
+  ns: string;
+  locale: string;
+};
+
+export const AppRouterI18nContext = createContext<AppRouterI18nContextType | null>(null);
+
+export function AppRouterI18nProvider({
+  children,
+  translations,
+  locale,
+  ns,
+}: AppRouterI18nContextType & {
+  children: ReactNode;
+}) {
+  return (
+    <AppRouterI18nContext.Provider value={{ translations, locale, ns }}>
path: apps/web/app/AppRouterI18nProvider.tsx
user: login: hbjORbj
body: yes you're right, this might be the reason for the e2e failures
created_at: 2025-04-09T23:47:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+"use client";
+
+import { createContext } from "react";
+import type { ReactNode } from "react";
+
+type AppRouterI18nContextType = {
+  translations: Record<string, string>;
+  ns: string;
+  locale: string;
+};
+
+export const AppRouterI18nContext = createContext<AppRouterI18nContextType | null>(null);
+
+export function AppRouterI18nProvider({
+  children,
+  translations,
+  locale,
+  ns,
+}: AppRouterI18nContextType & {
+  children: ReactNode;
+}) {
+  return (
+    <AppRouterI18nContext.Provider value={{ translations, locale, ns }}>
path: apps/web/app/AppRouterI18nProvider.tsx
user: login: hbjORbj
body: but if we want to have the translations ready in client immediately via SSR, there's no way to achieve it without not polluting the initial HTML, I think, right?
created_at: 2025-04-09T23:48:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+"use client";
+
+import { createContext } from "react";
+import type { ReactNode } from "react";
+
+type AppRouterI18nContextType = {
+  translations: Record<string, string>;
+  ns: string;
+  locale: string;
+};
+
+export const AppRouterI18nContext = createContext<AppRouterI18nContextType | null>(null);
+
+export function AppRouterI18nProvider({
+  children,
+  translations,
+  locale,
+  ns,
+}: AppRouterI18nContextType & {
+  children: ReactNode;
+}) {
+  return (
+    <AppRouterI18nContext.Provider value={{ translations, locale, ns }}>
path: apps/web/app/AppRouterI18nProvider.tsx
user: login: hbjORbj
body: gzipped `en/common.json` is 54.9KB
created_at: 2025-04-09T23:50:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+"use client";
+
+import { createContext } from "react";
+import type { ReactNode } from "react";
+
+type AppRouterI18nContextType = {
+  translations: Record<string, string>;
+  ns: string;
+  locale: string;
+};
+
+export const AppRouterI18nContext = createContext<AppRouterI18nContextType | null>(null);
+
+export function AppRouterI18nProvider({
+  children,
+  translations,
+  locale,
+  ns,
+}: AppRouterI18nContextType & {
+  children: ReactNode;
+}) {
+  return (
+    <AppRouterI18nContext.Provider value={{ translations, locale, ns }}>
path: apps/web/app/AppRouterI18nProvider.tsx
user: login: zomars
body: We discussed this way back. But I think the dream state here is that each time we use a translation (like with the t function) each page should only download the used translations for that specific page/view.
created_at: 2025-04-09T23:53:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+"use client";
+
+import { createContext } from "react";
+import type { ReactNode } from "react";
+
+type AppRouterI18nContextType = {
+  translations: Record<string, string>;
+  ns: string;
+  locale: string;
+};
+
+export const AppRouterI18nContext = createContext<AppRouterI18nContextType | null>(null);
+
+export function AppRouterI18nProvider({
+  children,
+  translations,
+  locale,
+  ns,
+}: AppRouterI18nContextType & {
+  children: ReactNode;
+}) {
+  return (
+    <AppRouterI18nContext.Provider value={{ translations, locale, ns }}>
path: apps/web/app/AppRouterI18nProvider.tsx
user: login: hbjORbj
body: according to Claude, it will add about 90ms on a standard 4G internet
created_at: 2025-04-09T23:54:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+"use client";
+
+import { createContext } from "react";
+import type { ReactNode } from "react";
+
+type AppRouterI18nContextType = {
+  translations: Record<string, string>;
+  ns: string;
+  locale: string;
+};
+
+export const AppRouterI18nContext = createContext<AppRouterI18nContextType | null>(null);
+
+export function AppRouterI18nProvider({
+  children,
+  translations,
+  locale,
+  ns,
+}: AppRouterI18nContextType & {
+  children: ReactNode;
+}) {
+  return (
+    <AppRouterI18nContext.Provider value={{ translations, locale, ns }}>
path: apps/web/app/AppRouterI18nProvider.tsx
user: login: zomars
body: I'm thinking out loud here, but let me expand. If, we server side render, we already have common.json available. We lookup the translation for each used key and put the translated text. Then we don't need to download common.json at all anymore. Don't we?
created_at: 2025-04-09T23:55:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,6 +56,8 @@ export function createWorkflowPageFixture(page: Page) {
   };
 
   const fillNameInput = async (name: string) => {
+    // eslint-disable-next-line playwright/no-wait-for-timeout
+    await page.waitForTimeout(300);
path: apps/web/playwright/fixtures/workflows.ts
user: login: hbjORbj
body: yeah, true, let me revert these changes to e2e tests
created_at: 2025-04-09T23:56:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+"use client";
+
+import { createContext } from "react";
+import type { ReactNode } from "react";
+
+type AppRouterI18nContextType = {
+  translations: Record<string, string>;
+  ns: string;
+  locale: string;
+};
+
+export const AppRouterI18nContext = createContext<AppRouterI18nContextType | null>(null);
+
+export function AppRouterI18nProvider({
+  children,
+  translations,
+  locale,
+  ns,
+}: AppRouterI18nContextType & {
+  children: ReactNode;
+}) {
+  return (
+    <AppRouterI18nContext.Provider value={{ translations, locale, ns }}>
path: apps/web/app/AppRouterI18nProvider.tsx
user: login: hbjORbj
body: This PR adds a caching object for `loadTranslations` so we only download a translation once, but because we pass these translations to `AppRouterI18nProvider`, doesn't Next.js include that in the initial HTML? 
created_at: 2025-04-10T00:01:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+"use client";
+
+import { createContext } from "react";
+import type { ReactNode } from "react";
+
+type AppRouterI18nContextType = {
+  translations: Record<string, string>;
+  ns: string;
+  locale: string;
+};
+
+export const AppRouterI18nContext = createContext<AppRouterI18nContextType | null>(null);
+
+export function AppRouterI18nProvider({
+  children,
+  translations,
+  locale,
+  ns,
+}: AppRouterI18nContextType & {
+  children: ReactNode;
+}) {
+  return (
+    <AppRouterI18nContext.Provider value={{ translations, locale, ns }}>
path: apps/web/app/AppRouterI18nProvider.tsx
user: login: hbjORbj
body: ah, how about adding a `Script`? and AppRouterI18nProvider can access it

This way, Next.js won't serialize the `translations` prop right?
created_at: 2025-04-10T00:06:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -113,6 +114,12 @@ const GeneralView = ({ localeProp, user, travelSchedules, revalidatePage }: Gene
       if (res.locale) {
         window.calNewLocale = res.locale;
         document.cookie = `calNewLocale=${res.locale}; path=/`;
+        const newLocaleI18nTranslations = await loadTranslations(res.locale, "common");
+        window.APP_ROUTER_I18N = {
+          translations: newLocaleI18nTranslations,
+          locale: res.locale,
+          ns: "common",
+        };
path: apps/web/modules/settings/my-account/general-view.tsx
user: login: hbjORbj
body: updating window.APP_ROUTER_I18N with new translations when user changes its language setting in /settings/my-account/general page
created_at: 2025-04-10T20:19:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -122,6 +131,9 @@ export default async function RootLayout({ children }: { children: React.ReactNo
             --font-cal: ${calFont.style.fontFamily.replace(/\'/g, "")};
           }
         `}</style>
+        <Script id="i18n-data" strategy="beforeInteractive">
path: apps/web/app/layout.tsx
user: login: hbjORbj
body: `beforeInteractive` ensures window.APP_ROUTER_I18N is set before any client-side JS (and hence React hydration)
created_at: 2025-04-10T20:32:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -153,7 +162,12 @@ export default async function RootLayout({ children }: { children: React.ReactNo
             "/insights",
           ]}
         />
-        <Providers>{children}</Providers>
+
+        <Providers>
+          <AppRouterI18nProvider translations={translations} locale={locale} ns={ns}>
path: apps/web/app/layout.tsx
user: login: cubic-dev-ai[bot]
body: Missing key in the useMemo dependency array
created_at: 2025-04-11T21:00:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5,10 +5,12 @@
 import React from "react";
 
 import { getLocale } from "@calcom/features/auth/lib/getLocale";
+import { loadTranslations } from "@calcom/lib/server/i18n";
path: apps/web/app/layout.tsx
user: login: cubic-dev-ai[bot]
body: Rule violated: **Enforce Singular Naming for Single-Item Functions**
      
      Function 'loadTranslations' has plural naming but returns a single translations object
created_at: 2025-04-11T21:00:22Z
author_association: CONTRIBUTOR

PR comments:
user: login: graphite-app[bot]
created_at: 2025-04-07T22:20:11Z
body: ### Graphite Automations
<details><summary><a href="https://app.graphite.dev/automations/?org=calcom&ruleId=qoTwHtX8MnP39HKxIt4V&ref=gt-automation-github-comment">"Add consumer team as reviewer"</a> took an action on this PR • (04/07/25)</summary><p>1 reviewer was added to this PR based on Keith Williams's automation.</p></details>
<details><summary><a href="https://app.graphite.dev/automations/?org=calcom&ruleId=PVL1B0jFo4nd5eKAT0W2&ref=gt-automation-github-comment">"Add foundation team as reviewer"</a> took an action on this PR • (04/07/25)</summary><p>1 reviewer was added to this PR based on Keith Williams's automation.</p></details>
author_association: NONE
user: login: vercel[bot]
created_at: 2025-04-07T22:25:17Z
body: [vc]: #qzqobKTxJpwenGS0laaGMuMTCLHPbPC/8Mi3BlqueBU=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJjYWxjb20td2ViLWNhbmFyeSIsInJvb3REaXJlY3RvcnkiOiJhcHBzL3dlYiIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiIifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2NhbC9jYWxjb20td2ViLWNhbmFyeS9EYUNqMzhEUFdhRE14NTIzUnlUWmh2OFZqa1ZyIiwibmV4dENvbW1pdFN0YXR1cyI6IklHTk9SRUQiLCJwcmV2aWV3VXJsIjoiY2FsY29tLXdlYi1jYW5hcnktZ2l0LXBlcmYtc2VydmVyLXNpZGUtaTE4bi1jYWwudmVyY2VsLmFwcCJ9LHsibmFtZSI6ImNhbCIsInJvb3REaXJlY3RvcnkiOiJhcHBzL3dlYiIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiIifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2NhbC9jYWwvN0c1dkplMmhnbTk3RGlQSHdZYU5BaGU3QkRmbSIsIm5leHRDb21taXRTdGF0dXMiOiJJR05PUkVEIiwicHJldmlld1VybCI6ImNhbC1naXQtcGVyZi1zZXJ2ZXItc2lkZS1pMThuLWNhbC52ZXJjZWwuYXBwIn1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)


<details><summary>2 Skipped Deployments</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **cal** | ⬜️ Ignored ([Inspect](https://vercel.com/cal/cal/7G5vJe2hgm97DiPHwYaNAhe7BDfm)) | [Visit Preview](https://cal-git-perf-server-side-i18n-cal.vercel.app) |  | Apr 11, 2025 7:31pm |
| **calcom-web-canary** | ⬜️ Ignored ([Inspect](https://vercel.com/cal/calcom-web-canary/DaCj38DPWaDMx523RyTZhv8VjkVr)) | [Visit Preview](https://calcom-web-canary-git-perf-server-side-i18n-cal.vercel.app) |  | Apr 11, 2025 7:31pm |
</details>



author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-07T23:24:11Z
body: <!-- GENERATED-E2E-RESULTS -->
## E2E results are ready!
- [Workflow #56418.1 latest results](https://calcom.github.io/test-results/reports/perf/server-side-i18n/14410320321/1)
author_association: CONTRIBUTOR
user: login: anikdhabal
created_at: 2025-04-09T17:31:47Z
body: @hbjORbj In one failed test, I saw this test failing:- 
```
locale.e2e.ts:64 › unauthorized user sees correct translations (zh) › should use correct translations and html attributes
Error: page.waitForSelector: Test timeout of 60000ms exceeded.
Call log:
  - waiting for locator('text=欢迎回来') to be visible
```
  Looks like it's not work:-  
  ```
 test.use({
    locale: "zh",
  });
```

author_association: CONTRIBUTOR
user: login: hbjORbj
created_at: 2025-04-09T18:00:42Z
body: @anikdhabal Yup I was just fixing it indeed
author_association: CONTRIBUTOR
user: login: zomars
created_at: 2025-04-10T00:04:52Z
body: This PR is a good start, but I believe we should take a more comprehensive approach to optimize i18n with App Router before merging.

Instead of just making server-fetched translations available to client components, we should leverage React Server Components (RSC) and streaming capabilities to:

1. Resolve all translations directly in Server Components without sending translation files to the client
2. For Client Components, stream only the specific translations they need rather than entire namespace files
3. Eliminate the need to download translation files for server-rendered content completely

This is a blocking change because:
- The current implementation will likely cause translation flickering that will break e2e tests
- It doesn't fully utilize App Router's capabilities and leaves significant performance gains on the table
- We're introducing a hybrid approach that adds complexity without solving the core issue

By fully leveraging RSC, we can ensure translations are available immediately on page load without any flickering, maintaining test stability while delivering a better user experience.
author_association: CONTRIBUTOR
user: login: hbjORbj
created_at: 2025-04-10T00:14:35Z
body: @zomars 
One intuitive way is to get TFunction via `getTranslate` in RSCs and pass it to client components directly but this will result in lots of prop drilling -> messy code / hundreds of file changes :(

> The current implementation will likely cause translation flickering 

Can you elaborate more on this?
author_association: CONTRIBUTOR
user: login: zomars
created_at: 2025-04-10T00:25:18Z
body: @hbjORbj unless you can prevent `AppRouterI18nProvider.tsx` to re-render when mounted. This will cause a flicker in the whole react tree when the client is mounted. This is the reason why adding a 300 ms waiting time works to fix e2e failures since the original locator that was being waited for re-renders. But it is not an optimal solution IMO.
author_association: CONTRIBUTOR
user: login: hbjORbj
created_at: 2025-04-10T00:31:39Z
body: @zomars 

1. Yeah your points give me a few ideas. Wdyt about adding `Script` to body in `app/layout.tsx` and injecting translations (server-fetched, via loadTranslations) there? This allows us to avoid using React Context. In `useLocale` we can directly access client-side script to get the translation.

2. do you think we should pass translated texts to client components (prop drilling)? Because I avoided that on purpose since that's not scalable.

3. BTW, the ideal would be that we have more granular translation files (insights.json, bookings.json, etc)
author_association: CONTRIBUTOR
user: login: zomars
created_at: 2025-04-10T01:53:21Z
body: We could try you first point and see what happens. I'm also not a fan of prop drilling tho. If somehow we could get to keep the same API that would be nice.
author_association: CONTRIBUTOR