Hydrating SSR React App with react-i18next causes flicker - reactjs

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 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?

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

Related

How to mock i18next-http-backend?

I have i18next-http-backend configured in my react app as follows:
import i18n from 'i18next'
import Backend from 'i18next-http-backend'
import detector from 'i18next-browser-languagedetector'
import { initReactI18next } from 'react-i18next'
i18n
.use(Backend)
.use(detector)
.use(initReactI18next)
.init({
backend: {
loadPath: `${process.env.PUBLIC_URL || ''}/locales/{{lng}}/{{ns}}.json`,
addPath: null
},
fallbackLng: 'en',
saveMissing: true,
interpolation: {
escapeValue: false // not needed for react as it escapes by default
}
})
export default i18n
In my test fixture, I want to mock the i18n aspect. For this I use the following boilerplate:
jest.mock('react-i18next', () => ({
// this mock makes sure any components using the translate hook can use it without a warning being shown
useTranslation: () => {
return {
t: (str: string) => str,
i18n: {
changeLanguage: () => new Promise(() => {})
}
}
},
initReactI18next: {
type: '3rdParty',
init: () => {}
}
}))
The test also uses msw for mocking http endpoints, which indicates that my test still wants to talk to the http backend of i18next:
console.warn
[MSW] Warning: captured a request without a matching request handler:
• GET http://localhost/locales/en/translation.json
How can I mock i18next correctly, to prevent it from trying to talk to the http backend?

Cache issue with localization files React-i18next

I am using react and react-i18next for the localization of my app. The issue is that after updating localization files. Sometimes an old version of my json files are cached in the browser.
It could be solved if the user clean the cache but I can't rely on users to know how to clear the cache.
JSON files are under public\locales.
I just figured out how to disable the cache in i18next translation.json files
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: "en",
debug: true,
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
requestOptions: {
cache: 'no-store',
},
},
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
});
It is not an ideal solution.
The better solution - translations files need to be retrieved fresh after each build.
But now this does not happen, such a feeling that hash is not added to translation files
How to prevent cache after a new build?
I got the same issue using i18next-localstorage-backend which is meant to use localStorage in browser.
I simply added a defaultVersion in the init options, But in order to get a new version every build, I had to generate a random version number.
Here is my i18n.ts file:
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import resourcesToBackend from 'i18next-resources-to-backend';
import Backend from "i18next-chained-backend";
import LocalStorageBackend from "i18next-localstorage-backend";
function genRandonNumber(length : number) {
const chars = '0123456789';
const charLength = chars.length;
let result = '';
for ( var i = 0; i < length; i++ ) {
result += chars.charAt(Math.floor(Math.random() * charLength));
}
return result;
}
const LOCAL_DOMAINS = ["localhost", "127.0.0.1"];
const HASH = genRandonNumber(10);
i18n
.use(Backend)
// detect user language
.use(LanguageDetector)
// pass the i18n instance to react-i18next.
.use(initReactI18next)
// init i18next
.init({
// default fallback will be french
fallbackLng: 'fr',
// default namespace to load
ns: 'header',
// load languages ressources with lazy loading and caching client side
backend: {
backends: [
LocalStorageBackend, // primary ressources (cache = window.localStorage)
resourcesToBackend((language, namespace, callback) => {
import(`/public/locales/${language}/${namespace}.json`)
.then((resources) => {
callback(null, resources)
})
.catch((error) => {
callback(error, null)
})
}) // fallback ressources (json)
],
backendOptions: [{
expirationTime: 24 * 60 * 60 * 1000 * 7, // 7 days
defaultVersion: "v" + HASH // generate a new version every build to refresh
}]
},
// debug only in dev
debug: LOCAL_DOMAINS.includes(window.location.hostname),
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
});
i18n.on('languageChanged', () => {
document.documentElement.lang = i18n.language;
});
export default i18n;
My i18n instance is a bit more complicated, but in the end it will:
Use localStorage to reduce networking and enable faster translation.
Load only the required JSON files for a given page thanks to namespaces.
Update the localStorage every new build.
If you are using multiple backends (JSON files and localStorage), you have to use i18next-chained-backend.

How to properly load i18n resources in next-i18next from api endpoints?

I have a nextjs app, which I want to extend using i18next and next-i18next (https://github.com/isaachinman/next-i18next).
The default configuration is looking for json files under ./public/locales/{lng}/{ns}.json, where lng is the language and ns a namespace.
My requirement however is, that this should be served from an api endpoint. I am struggling to find the correct configuration, as next-i18next does ignore my settings right now and is not firing off any xhr requests to my backend.
next-i18next.config.js:
const HttpApi = require('i18next-http-backend')
module.exports = {
i18n: {
defaultLocale: 'de',
locales: ['en', 'de'],
},
backend: {
referenceLng: 'de',
loadPath: `https://localhost:5001/locales/de/common`,
parse: (data) => {
console.log(data)
return data
}
},
debug: true,
ns: ['common', 'footer', 'second-page'], // the namespaces needs to be listed here, to make sure they got preloaded
serializeConfig: false, // because of the custom use i18next plugin
use: [HttpApi],
}
I am at a loss here. What am I doing wrong?
Eventually I cobbled it together.
const I18NextHttpBackend = require('i18next-http-backend')
module.exports = {
i18n: {
defaultLocale: 'de',
locales: ['de'],
backend: {
loadPath: `${process.env.INTERNAL_API_URI}/api/locales/{{lng}}/{{ns}}`
},
},
debug: true,
ns: ["common", "employees", "projects"],
serializeConfig: false,
use: [I18NextHttpBackend]
}
You might be running into an error saying You are passing a wrong module! Please check the object you are passing to i18next.use(). If this is the case, you can force the http backend to load as commonjs, by using the following import:
const I18NextHttpBackend = require('i18next-http-backend/cjs')
The first one worked on webpack 5, while I had to use the cjs import on webpack 4. Although I could not find the reason for this.
After this, its smooth sailing:
_app.tsx:
/*i18n */
import { appWithTranslation } from 'next-i18next'
import NextI18nextConfig from '../../next-i18next.config'
const MyApp = ({ Component, pageProps }: AppProps) => {
return (
<>
<MsalProvider instance={msalApp}>
<PageLayout>
<Component {...pageProps} />
</PageLayout>
</MsalProvider>
</>
)
}
export default appWithTranslation(MyApp, NextI18nextConfig)
anypage.tsx:
export const getServerSideProps: GetServerSideProps = async ({ locale }) => {
return {
props: {
...(await serverSideTranslations(locale, ['common', 'employees'])),
// Will be passed to the page component as props
},
};
}
If you just need to locales to be fetched once, during build, you can use getStaticProps instead - that is up to you.

react-i18next - How do I add a backend to i18n after it has been initialized?

I want my i18n to lazy load translation files and from the docs, it seems quite simple to do that - add .use(backend) and import backend from "i18next-http-backend".
The only issue is that the i18n instance I use in my provided has been already defined in my org's internal repo as a UI library (which I have to use) which exposes only one method to initialize an i18n instance for you - this doesn't have any provision for .use(backend) and now I'm stuck how to add that to the code.
Here's the library code -
...
export const getDefaultI18nextInstance = (resources) => {
i18n
.use(AlphaLanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
// have a common namespace used around the full app
ns: [
'translations',
'common'
],
nsMode: 'fallback',
defaultNS: 'translations',
// debug: (process.env.NODE_ENV && process.env.NODE_ENV === 'production') ? false : true,
debug: true,
interpolation: {
escapeValue: false, // not needed for react!!
},
resources: resources || null,
react: {
wait: true
}
});
Object.keys(translations).forEach(lang => {
Object.keys(translations[lang]).forEach(namespace => {
i18n.addResourceBundle(lang, namespace, translations[lang][namespace], true, true)
})
});
return i18n;
}
export default {
getDefaultI18nextInstance,
translations,
t
};
...
I tried using it something like this <I18nextProvider i18n={i18n.getDefaultI18nextInstance().use(backend)}> in my index.js file but then I get the error
i18next::backendConnector: No backend was added via i18next.use. Will
not load resources.
FYI, I have my locales at projectRoot/locales/{lang}/translation.json.
You can use i18n.cloneInstance(options, callback) and pass your backend config as options, it will merge all the options that your ui lib has with yours, and return a new instance of i18next which will be able to fetch.
import HttpApi from 'i18next-http-backend';
const original = getDefaultI18nextInstance({});
export const i18n = original
.cloneInstance({
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
})
.use(HttpApi);

Error: Should have a queue. This is likely a bug in React. Please file an issue

I am using react-i18next. Sometime on init function component facing React issue. Any idea what may causing it?
My config
import i18n from "i18next";
import resources from "./locales";
import { initReactI18next } from "react-i18next";
const options = {
debug: false,
lng: "en",
resources: resources,
fallbacking: "en",
};
i18n.use(initReactI18next).init(options);
export default i18n;
My versions
"react": "^16.13.1",
"react-i18next": "^11.7.2",
stupid bug which was inited by myself. I was simple declaring before return dom component, like
const test = (props)=>{
if(props.test){
return <p>test</p>
}
const { t } = useTranslation("Test");//this must be on very top
return <p>Main {t('test_variable')}</p>
}
I wish if react could be more clear in logging bugs
you might try this configuration, it works with me perfectly
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next, Trans } from "react-i18next";
import lang from "assets/lang";
i18n.use(LanguageDetector)
.use(initReactI18next)
.init({
// we init with resources
resources: lang,
fallbackLng: "en",
debug: true,
// have a common namespace used around the full app
ns: ["basic"],
defaultNS: "basic",
keySeparator: false, // we use content as keys
interpolation: {
escapeValue: false,
},
});
window.document.body.dir = i18n.t("dir");
export default i18n;
const Tr = (props) => (
<Trans i18nKey={props.tr} {...props}>
{props.children}
</Trans>
);
Tr.tr = (key, ...rest) => i18n.t(key, ...rest);
export { Tr };
I've also had such a problem and finally I found that the main reason of this problem is using hooks in wrong place, you need to use your hook in parent component and not the child one. For example, you have a todo list and todo component, you have to use it in todo list and not in todo.

Resources