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?
Related
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.
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);
I am using the i18n module in my react app which is hosted as an app (which I will refer to as live app) on S3 and has cloudfront sitting in front of it.
I want to store the s3 url in a config file as to avoid having it hardcoded in my app so that I can work against translation files stored locally in the public/locales folder when I'm developing.
Initially I had my backend options set-up so that it would always try and look up the files in the locales path. And this worked locally but stopped working on S3 although the locales folder was present in the S3 bucket. I also noticed that no request was being sent from the app to retrieve the translation files.
backend: {
loadPath: 'locales'
}
I then decided to upload the translation files to another S3 bucket and host them there to see if this would fix the issue.
I changed my config to hardcode the s3 bucket path. This worked both locally and on the live app. But this means that I can't use my config file to determine the loadPath option.
backend: {
loadPath: '<myhardcoded-s3-bucket-url>/{{lng}}/translation.json',
crossDomain: true
}
I then thought I could construct the url in place as so:
/*global AWS_CONFIG */
/*eslint no-undef: "error"*/
...
...
...
backend: {
loadPath: `${AWS_CONFIG.aws_app_translations_path}/{{lng}}/translation.json`,
crossDomain: true
}
Strangely again this worked locally when AWS_CONFIG.aws_app_translations_path was both http://localhost:3000/locales and <myhardcoded-s3-bucket-url>.
However once I pushed it live, it failed again. This time making a request to https://<my-apps-base-path>/undefined/en-GB/translation.json for example. So it's trying to use the app path and append what I have defined in loadPath.
I then saw that I could have loadPath as a function to construct my url. This didn't work for the live app either, but again worked locally.
backend: {
loadPath: function (lng) {
return `${AWS_CONFIG.aws_app_translations_path}/${lng}/translation.json`
},
crossDomain: true
}
This is my entire i18n file
/*global AWS_CONFIG */
/*eslint no-undef: "error"*/
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend";
import { initReactI18next } from "react-i18next";
const options = {
order: ['navigator', 'localStorage'],
caches: ['localStorage'],
fallbackLng: "en-GB",
debug: true,
interpolation: {
escapeValue: false // not needed for react as it escapes by default
},
backend: {
loadPath: function (lng) {
return `${AWS_CONFIG.aws_app_translations_path}/${lng}/translation.json`
},
crossDomain: true
}
}
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init(options);
export default i18n;
What could explain this odd behaviour? Am I missing configuration here?
Based on your responses in the comments, looks like you are missing the translation part, and it pre-appending undefined as a namespace in the url.
Try this:
// i18n
const options = {
order: ['navigator', 'localStorage'],
caches: ['localStorage'],
fallbackLng: "en-GB",
debug: true,
interpolation: {
escapeValue: false // not needed for react as it escapes by default
},
backend: {
loadPath: function () {
return `${AWS_CONFIG.aws_app_translations_path}/{{lng}}/{{ns}}.json`
},
crossDomain: true
}
}
Based on the loadPath config doc:
path where resources get loaded from, or a function
returning a path:
function(lngs, namespaces) { return customPath; }
The returned path will interpolate lng, ns if provided like giving a static path
Once I changed the way I was importing the config file it worked. I am guessing there is a loading issue on live, but having changed my i18n file from
/*global AWS_CONFIG */
/*eslint no-undef: "error"*/
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend";
import { initReactI18next } from "react-i18next";
const options = {
order: ['navigator', 'localStorage'],
caches: ['localStorage'],
fallbackLng: "en-GB",
debug: true,
interpolation: {
escapeValue: false // not needed for react as it escapes by default
},
backend: {
loadPath: function (lng) {
return `${AWS_CONFIG.aws_app_translations_path}/${lng}/translation.json`
},
crossDomain: true
}
}
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init(options);
export default i18n;
to
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend";
import { initReactI18next } from "react-i18next";
import {config} from './config';
const {aws_app_translations_path} = config;
const options = {
order: ['navigator', 'localStorage'],
caches: ['localStorage'],
fallbackLng: "en-GB",
debug: true,
defaultNS: 'translation',
load: 'currentOnly',
interpolation: {
escapeValue: false // not needed for react as it escapes by default
},
backend: {
loadPath: (lng, ns) => {
return `${aws_app_translations_path}/${lng}/${ns}.json`;
},
crossDomain: true
}
}
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init(options);
export default i18n;
Fixed it. Initially AWS_CONFIG was being returned as undefined despite working for the rest of the entire app. Changing the config file to live in the root directory of the project and the way it was imported solved the issue.
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
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;