React-i18next init callback resolving before Suspense resolves - reactjs

Using react-i18next with Suspense.
I want to use the init callback option to run an axios interceptor to ensure all requests add the correct header language.
My problem is that the init callback doesn't halt the Suspense to render its children. I thought returning a promise from init would work but it doesn't.
Is halting the Suspense to resolve possible?
My code:
i18n
.use(Backend)
.use(initReactI18next)
.init(
{
lng: 'en',
fallbackLng: 'en',
debug: true,
react: {
wait: true,
},
},
function callback() {
// this is the place where I want to halt Suspense until my logic is done
return new Promise((res, rej) => {
i18nInterceptor(i18n.language);
setTimeout(() => {
res();
}, 10000);
});
}
);
export default i18n;

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?

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);

Hydrating SSR React App with react-i18next causes flicker

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

React-i18next suspense

I m using React-i18next just like the example
import React, { Suspense } from 'react';
import { useTranslation } from 'react-i18next';
function App() {
return (
<Suspense fallback="loading">
<MyComponent />
</Suspense>
);
}
But Suspense is breaking one of my other components, namely react-masonry-layout. Is it possible to not using Suspense?
Thanks.
react-i18nnext uses Suspense by default. If you don't want to use it, you have to specify that in your configuration. If you have a i18n config file, you can set the useSuspense flag to false in the react section of the init object.
//Example config
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: "en",
debug: true,
resources: {
},
interpolation: {
escapeValue: false,
},
react: {
wait: true,
useSuspense: false,
},
})
Or you can just set the flag in your component.
<MyComponent useSuspense={false} />
Just be aware that choosing not to use Suspense has its implications. You have to write checks to handle the 'not ready' state ie: have a loading component render when state is not ready and your component render when the state is ready.Not doing so will result in rendering your translations before they loaded.
Date: 02/09/2021
Versions
"next": "^11.0.1",
"next-i18next": "^8.5.1",
In NextJs with SSR Suspense is not supported yet. So if you need to remove that you need to disable it on next-i18next.config.js.
Issue
Error: ReactDOMServer does not yet support Suspense.
Fix next-i18next.config.js
const path = require('path');
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'de'],
},
localePath: path.resolve('./assets/locales'),
react: {
useSuspense: false,
},
};

Resources