I've been trying to get SSR to work with react-i18next for quite some time, the documentation is somewhat lacking so I've pieced together what I could from some other repos and their razzle-ssr example.
I have the server-side working, where I:
- Setup express, call the appropriate middleware, get the appropriate locale:
const app = express();
await i18n
.use(Backend)
.use(i18nextMiddleware.LanguageDetector)
.init(options)
app.use(i18nextMiddleware.handle(i18n));
app.use('/locales', express.static(`${appDirectory}/locales`));
- Get the DOM representation of the App given the request:
app.get('/*', req => {
//...
const html = ReactDOMServer.renderToString(
<I18nextProvider i18n={req.i18n}>>
<App />
</I18nextProvider>
)
// ...
})
- Append the
initialI18nStoreto the request content:
const initialI18nStore = {};
req.i18n.languages.forEach(l => {
initialI18nStore[l] = req.i18n.services.resourceStore.data[l];
});
const initialLanguage = req.i18n.language;
content = content.replace(
/<head>/,
`<head>
<script>
window.initialI18nStore = "${JSON.stringify(initialI18nStore)}";
window.initialLanguage = "${initialLanguage.slice(0, 2)}";
</script>`,
);
This works great when I curl http://localhost:3000/ I get the correct DOM with the loaded/replaced translations
The problem I encounter is hydration.
I tried using useSSR with Suspense but couldn't seem to get it working. But I feel that will have fundamentally the same problem: i18n needs to initialize with languages before we should hydrate the app. Correct(?)
I attempted to emulate the same thing as useSSR by waiting for the client i18n instance to be initialized before hydrating the app:
// client i18n.js
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n
.use(initReactI18next)
.use(Backend)
.use(LanguageDetector);
const i18nInit = () => {
return new Promise(resolve => {
// @todo: shim in moment locale here
i18n.init(options, () => resolve(i18n));
});
};
export default i18nInit;
// client index.js
const renderApp = async () => {
let i18n = await i18ninit();
if (window.initialI18nStore) {
i18n.services.resourceStore.data = window.initialI18nStore;
}
hydrate(<BaseApp />, document.getElementById('root'));
};
renderApp();
The problem with this is: The app renders fine from the server-provided DOM representation. Then when I wait for the client i18n instance to initialize then hydrate the app, I get a huge style-less flicker, then it returns the same view as the DOM representation.
I also tried to do the deferred rendering inside of a functional component:
const BaseApp = () => {
const [render, setRender] = useState(false);
useEffect( () => {
await initI18();
i18n.services.resourceStore.data = INITIALI18NSTORE;
setRender(true);
}, [])
if(!render) return null;
return <App />
}
But this causes similar, but instead of a style-less flicker, a white screen due to return null.
Is there a concept I'm missing here? Or am I doing something out of order? How do I get a seamless transition from the server provided DOM+styles to my client provided ones with translations included?