Your issue is a classic hydration mismatch: SSR content renders in one language (default/lng in your i18n config, e.g., "ka"), but on the client, the language is not guaranteed to match until i18n.changeLanguage(locale) fires. This causes a mismatch and flicker.
Why is this happening?
- On the server, you set i18n's language before rendering: good!
- On the client, your code also tries to set the language from
props.initialPage.props.localebeforehydrateRoot—however,i18n.changeLanguageis async. Even then, React 18 might kick off hydration before the language is switched, leading to client-side re-render and flicker. - Your SSR may still use the fallbackLng (
"ka") unless you explicitly set it for the request.
Solution
- Make sure i18n is always initialized with the correct locale (per-request) on BOTH SSR and hydration.
- DON'T call i18n.changeLanguage in useEffect after hydration – it will always cause a re-render.
- Use i18n.initialized or a suspense boundary for language loading
Suggested Approach
- Modify your i18n initialization to take an initial
lngparameter:
// i18n.js
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import en from "./Locales/en.json";
import ka from "./Locales/ka.json";
export function createI18nInstance(locale = "ka") {
const instance = i18n.createInstance();
instance.use(initReactI18next).init({
resources: {
en: { translation: en },
ka: { translation: ka },
},
lng: locale,
fallbackLng: "ka",
interpolation: { escapeValue: false },
// Remove saveMissing, debug, etc for production
});
return instance;
}
- On the server: per-request i18n instance
// ssr.jsx
import { createI18nInstance } from './i18n';
import { I18nextProvider } from 'react-i18next';
createServer((page) => {
// ...
return createInertiaApp({
// ...
setup: ({ App, props }) => {
const locale = props.initialPage.props.locale || "ka";
const i18nInstance = createI18nInstance(locale);
return (
<I18nextProvider i18n={i18nInstance}>
<App {...props} />
</I18nextProvider>
);
},
// ...
});
});
- On the client: initialize i18n with the correct language BEFORE hydrating
// app.jsx
import { createI18nInstance } from './i18n';
import { I18nextProvider } from 'react-i18next';
createInertiaApp({
// ...
setup: ({ el, App, props }) => {
const locale = props.initialPage.props.locale || "ka";
const i18nInstance = createI18nInstance(locale);
hydrateRoot(
el,
<React.StrictMode>
<I18nextProvider i18n={i18nInstance}>
<App {...props} />
</I18nextProvider>
</React.StrictMode>
);
},
// ...
});
- Remove the useEffect that switches language in your Layout.jsx:
// Layout.jsx
// REMOVE this effect:
// useEffect(() => {
// if (locale && locale !== i18n.language) {
// i18n.changeLanguage(locale);
// }
// }, [locale]);
Recap
- Always initialize i18n with the target locale, both server- and client-side, before rendering anything.
- Remove useEffect-based changes to avoid client re-renders after hydration.
- Use a per-request i18n instance (not global/shared singleton, especially on the server).
This setup prevents SSR/CSR mismatch and thus eliminates flicker—what users see initially matches what React hydrates.
If you're using lazy i18n loading or dynamic imports, consider using React Suspense for i18n. But if your translations are bundled and synchronously available, the above approach works perfectly.
Let me know if you need a more complete example or help adapting to your project!