import { match } from "@formatjs/intl-localematcher"; import dayjs from "dayjs"; import { createInstance } from "i18next"; import resourcesToBackend from "i18next-resources-to-backend"; import Negotiator from "negotiator"; import { initReactI18next } from "react-i18next/initReactI18next"; import * as z from "zod"; import { logger } from "@turbostarter/shared/logger"; import { config, getInitOptions } from "../config"; import { env } from "../env"; import { loadTranslation, makeZodI18nMap } from "../utils"; import type { i18n, Namespace, TFunction } from "i18next"; export const initializeServerI18n = async ({ locale, defaultLocale, ns, }: { locale?: string; defaultLocale?: string; ns?: Namespace; }): Promise => { const i18n = createInstance(); const loadedNamespaces = new Set(); await new Promise((resolve) => { void i18n .use( resourcesToBackend( async ( language: (typeof config.locales)[number], namespace: (typeof config.namespaces)[number], callback, ) => { const data = await loadTranslation(language, namespace); loadedNamespaces.add(namespace); return callback(null, data); }, ), ) .use({ type: "3rdParty", init: async (i18next: typeof i18n) => { let iterations = 0; const maxIterations = 100; while (i18next.isInitializing) { iterations++; if (iterations > maxIterations) { logger.error( `i18next is not initialized after ${maxIterations} iterations`, ); break; } await new Promise((resolve) => setTimeout(resolve, 1)); } initReactI18next.init(i18next); resolve(i18next); }, }) .init(getInitOptions({ locale, defaultLocale, ns })); }); const namespaces = ns ? typeof ns === "string" ? [ns] : ns : config.namespaces; // If all namespaces are already loaded, return the i18n instance if (loadedNamespaces.size === namespaces.length) { return i18n; } // Otherwise, wait for all namespaces to be loaded const maxWaitTimeMs = 100; const checkIntervalMs = 5; async function waitForNamespaces() { const startTime = Date.now(); while (Date.now() - startTime < maxWaitTimeMs) { const allNamespacesLoaded = namespaces.every((ns) => loadedNamespaces.has(ns), ); if (allNamespacesLoaded) { return true; } await new Promise((resolve) => setTimeout(resolve, checkIntervalMs)); } return false; } const success = await waitForNamespaces(); if (!success) { logger.warn( `Not all namespaces were loaded after ${maxWaitTimeMs}ms. Initialization may be incomplete.`, ); } return i18n; }; export const getLocaleFromCookies = async () => { try { const { cookies } = await import("next/headers"); return (await cookies()).get(config.cookie)?.value; } catch { return undefined; } }; export const getLocaleFromRequest = (request?: Request) => { if (!request) return env.DEFAULT_LOCALE ?? config.defaultLocale; const localeCookie = request.headers .get("cookie") ?.split(";") .find((cookie) => cookie.trim().startsWith(`${config.cookie}=`)) ?.split("=")[1] ?.trim() .replace(/[.,]/g, ""); if (localeCookie) { return localeCookie; } const negotiatorHeaders: Record = {}; request.headers.forEach((value: string, key: string) => { negotiatorHeaders[key] = value; }); const languages = new Negotiator({ headers: negotiatorHeaders }).languages(); try { return match( languages, config.locales, env.DEFAULT_LOCALE ?? config.defaultLocale, ); } catch { return env.DEFAULT_LOCALE ?? config.defaultLocale; } }; export const getTranslation = async ({ locale: passedLocale, request, ns, }: { locale?: string; request?: Request; ns?: T } = {}) => { const locale = passedLocale ?? (request ? getLocaleFromRequest(request) : null) ?? (await getLocaleFromCookies()) ?? undefined; const i18nextInstance = await initializeServerI18n({ locale, ns }); dayjs.locale(i18nextInstance.language); const t = i18nextInstance.getFixedT( i18nextInstance.language, ns, ) as TFunction; z.config({ localeError: makeZodI18nMap({ t: t as TFunction }), }); return { t, i18n: i18nextInstance, }; };