This commit is contained in:
payacom
2026-08-18 20:22:07 +03:30
parent 0c6e1a7125
commit 1ba6646bf9
19 changed files with 1440 additions and 41 deletions

View File

@@ -8,18 +8,15 @@ import {
useMemo,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import "@/lib/i18n";
import { I18nextProvider } from "react-i18next";
import { createI18nInstance } from "@/lib/i18n/createI18nInstance";
import {
DEFAULT_LANGUAGE,
getDirection,
type AppLanguage,
} from "@/lib/i18n/registry";
import {
persistLanguagePreference,
readStoredLanguagePreference,
resolveBrowserLanguage,
resolveServerRenderedLanguage,
} from "@/lib/i18n/clientLanguage";
import { syncDocumentLanguage } from "@/lib/i18n/syncDocumentLanguage";
@@ -33,43 +30,55 @@ type LanguageContextValue = {
const LanguageContext = createContext<LanguageContextValue | null>(null);
export function LanguageProvider({ children }: { children: React.ReactNode }) {
const { i18n } = useTranslation();
// Always start at the deterministic default on both server and the first client
// render pass — reading cookies/localStorage/navigator here would differ between
// SSR (no window) and hydration (window present), causing a hydration mismatch.
// The real preference is resolved client-side in the effect below instead.
const [language, setLanguageState] = useState<AppLanguage>(DEFAULT_LANGUAGE);
const [ready, setReady] = useState(false);
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();
// Prefer the language the server already rendered `<html lang>` with over an
// independent client-side guess from navigator.language — those two signals
// frequently disagree (see resolveServerRenderedLanguage), which was causing
// a full-page hydration mismatch on first visits.
const initial =
stored ?? resolveServerRenderedLanguage() ?? resolveBrowserLanguage();
if (!stored) {
persistLanguagePreference(initial);
if (stored && stored !== initialLanguage) {
void instance.changeLanguage(stored).then(() => {
setLanguageState(stored);
syncDocumentLanguage(stored);
});
} else if (!stored) {
persistLanguagePreference(initialLanguage);
}
void i18n.changeLanguage(initial).finally(() => {
setLanguageState(initial);
syncDocumentLanguage(initial);
setReady(true);
});
}, [i18n]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const setLanguage = useCallback(
async (lang: AppLanguage) => {
await i18n.changeLanguage(lang);
await instance.changeLanguage(lang);
persistLanguagePreference(lang);
setLanguageState(lang);
syncDocumentLanguage(lang);
},
[i18n]
[instance]
);
const value = useMemo<LanguageContextValue>(
@@ -84,7 +93,9 @@ export function LanguageProvider({ children }: { children: React.ReactNode }) {
);
return (
<LanguageContext.Provider value={value}>{children}</LanguageContext.Provider>
<I18nextProvider i18n={instance}>
<LanguageContext.Provider value={value}>{children}</LanguageContext.Provider>
</I18nextProvider>
);
}