Files
modstagram-next/src/contexts/LanguageProvider.tsx
2026-08-18 20:22:07 +03:30

109 lines
3.4 KiB
TypeScript

"use client";
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import { I18nextProvider } from "react-i18next";
import { createI18nInstance } from "@/lib/i18n/createI18nInstance";
import {
getDirection,
type AppLanguage,
} from "@/lib/i18n/registry";
import {
persistLanguagePreference,
readStoredLanguagePreference,
} from "@/lib/i18n/clientLanguage";
import { syncDocumentLanguage } from "@/lib/i18n/syncDocumentLanguage";
type LanguageContextValue = {
language: AppLanguage;
dir: "rtl" | "ltr";
isRtl: boolean;
ready: boolean;
setLanguage: (lang: AppLanguage) => Promise<void>;
};
const LanguageContext = createContext<LanguageContextValue | null>(null);
export function LanguageProvider({
children,
initialLanguage,
}: {
children: React.ReactNode;
initialLanguage: AppLanguage;
}) {
// One i18next instance per render of this component — since Next.js renders
// "use client" components fresh on the server for each request (not from a
// shared module-level singleton), this naturally gives every request its own
// isolated instance instead of sharing one across the whole server process.
// That's what actually fixes the hydration mismatch: the old shared singleton
// was stuck on the server's default language for the life of the process,
// so every visitor's page content server-rendered in that language and then
// flipped to the real one after hydration.
const [instance] = useState(() => createI18nInstance(initialLanguage));
// initialLanguage is resolved server-side (cookie → Accept-Language → default)
// and passed down as a prop, so server and first client render agree from the
// start — no more guessing a different value on the client than what the
// server already committed to in the HTML.
const [language, setLanguageState] = useState<AppLanguage>(initialLanguage);
const [ready, setReady] = useState(true);
useEffect(() => {
// A stored preference (from an earlier explicit choice) can still disagree
// with initialLanguage — e.g. localStorage survived a cleared cookie. Only
// switch in that case; otherwise there's nothing to reconcile since the
// server already rendered the right language.
const stored = readStoredLanguagePreference();
if (stored && stored !== initialLanguage) {
void instance.changeLanguage(stored).then(() => {
setLanguageState(stored);
syncDocumentLanguage(stored);
});
} else if (!stored) {
persistLanguagePreference(initialLanguage);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const setLanguage = useCallback(
async (lang: AppLanguage) => {
await instance.changeLanguage(lang);
persistLanguagePreference(lang);
setLanguageState(lang);
syncDocumentLanguage(lang);
},
[instance]
);
const value = useMemo<LanguageContextValue>(
() => ({
language,
dir: getDirection(language),
isRtl: getDirection(language) === "rtl",
ready,
setLanguage,
}),
[language, ready, setLanguage]
);
return (
<I18nextProvider i18n={instance}>
<LanguageContext.Provider value={value}>{children}</LanguageContext.Provider>
</I18nextProvider>
);
}
export function useAppLanguage() {
const ctx = useContext(LanguageContext);
if (!ctx) {
throw new Error("useAppLanguage must be used within LanguageProvider");
}
return ctx;
}