Reputation: 1429
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:
const app = express();
await i18n
.use(Backend)
.use(i18nextMiddleware.LanguageDetector)
.init(options)
app.use(i18nextMiddleware.handle(i18n));
app.use('/locales', express.static(`${appDirectory}/locales`));
app.get('/*', req => {
//...
const html = ReactDOMServer.renderToString(
<I18nextProvider i18n={req.i18n}>>
<App />
</I18nextProvider>
)
// ...
})
initialI18nStore
to 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?
Upvotes: 4
Views: 6711
Reputation: 21
I have tried to reproduce your problem. At the step you mentioned:
I attempted to emulate the same thing as useSSR by waiting for the client i18n instance to be initialized before hydrating the app:
At this step, I found that the SSR result is difference from the CSR result: SSR vs CSR
It was because my language is zh-TW, and no 'fallbackLng' provided on the server-side.
I added this line as the i18n.js
did, and solved the problem
// server.js
i18n
.use(Backend)
.use(i18nextMiddleware.LanguageDetector)
.init(
{
debug: false,
preload: ['en', 'de'],
fallbackLng: 'en', // << ----- this line
ns: ['translations'],
defaultNS: 'translations',
backend: {
loadPath: `${appSrc}/locales/{{lng}}/{{ns}}.json`,
addPath: `${appSrc}/locales/{{lng}}/{{ns}}.missing.json`,
},
},
...
...
...
To make sure that client renders the correct DOMs just at the first time, I set the useSuspense
to false and remove the <Suspense>
component
// i18n.js
const options = {
fallbackLng: 'en',
load: 'languageOnly', // we only provide en, de -> no region specific locals like en-US, de-DE
// have a common namespace used around the full app
ns: ['translations'],
defaultNS: 'translations',
saveMissing: true,
debug: true,
interpolation: {
escapeValue: false, // not needed for react!!
formatSeparator: ',',
format: (value, format, lng) => {
if (format === 'uppercase') return value.toUpperCase();
return value;
},
},
react: {
useSuspense: false, // << ----- this line
},
wait: process && !process.release,
};
// client.js
const BaseApp = () => {
useSSR(window.initialI18nStore, window.initialLanguage);
return (
<BrowserRouter>
<App />
</BrowserRouter>
);
}
And everything works fine
Upvotes: 2