forkingbeardman's avatar

Inertia/React SSR - i18n Language flicker Issue

Hi, i have run into the following issue after adding SSR support to my app, the server rendered html stays in English but later is hydrated with whatever user has selected. this causes slight flicker of text during refresh (and seo not indexing correct html). i have tried many solutions and none have solved the underlying problem.

in my Layout.jsx i have following code:

useEffect(() => {
        if (locale && locale !== i18n.language) {
            i18n.changeLanguage(locale);
        }
    }, [locale]);

if i remove this, only enlgish is loaded

my app.jsx looks like this:

createInertiaApp({
    title: (title) => (title ? `${title} - ${appName}` : appName),
    resolve: (name) =>
        resolvePageComponent(
            `./Pages/${name}.jsx`,
            import.meta.glob("./Pages/**/*.jsx"),
        ),
    setup: ({ el, App, props }) => {
        const locale = props.initialPage.props.locale;
        if (locale) {
            i18n.changeLanguage(locale);
        }
        hydrateRoot(
            el,
            <React.StrictMode>
                <App {...props} />
            </React.StrictMode>,
        );
    },
    progress: {
        color: "#4B5563",
    },
});

the ssr.jsx:

and the i18n config:

import i18n from "i18next";
import { initReactI18next } from "react-i18next";

import en from "./Locales/en.json";
import ka from "./Locales/ka.json";

i18n.use(initReactI18next).init({
    resources: {
        en: { translation: en },
        ka: { translation: ka },
    },
    lng: "ka",
    fallbackLng: "ka",
    interpolation: {
        escapeValue: false,
    },
});

export default i18n;

what am i doing wrong here? any help is much appreciated

0 likes
2 replies
LaryAI's avatar
Level 58

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.locale before hydrateRoot—however, i18n.changeLanguage is 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

  1. Make sure i18n is always initialized with the correct locale (per-request) on BOTH SSR and hydration.
  2. DON'T call i18n.changeLanguage in useEffect after hydration – it will always cause a re-render.
  3. Use i18n.initialized or a suspense boundary for language loading

Suggested Approach

  1. Modify your i18n initialization to take an initial lng parameter:
// 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;
}
  1. 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>
            );
        },
        // ...
    });
});
  1. 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>
        );
    },
    // ...
});
  1. 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!

forkingbeardman's avatar

this was one of the solutions i tried and it does not eliminate the issue and the SSR still only renders in English and then gets hydrated with Georgian

Please or to participate in this conversation.