Closed Tubaleviao closed 3 years ago
You may want to use some sort of fallback with the help of i18next-chaned-backend: https://www.i18next.com/how-to/caching
btw: there is also an onFailedLoading event: https://www.i18next.com/overview/api#onfailedloading
Funnily enough, I was just trying to figure out how this works with React too.
Using suspense, I was expecting the promise to reject if a resource could not be fetched (I'm not using a fallbackLng
nor do I want to), and I could then use a React error boundary to catch it. But the page just renders without the translations, showing "translation keys" everywhere (imo, this should never happen).
I'll see what I can do with the onFailedLoading
event, but I still think that it is odd that no error is propagated from the suspense when the fetching fails.
Sorry to post again on this issue, but I think it's related and I am yet to find a solution. How would you handle the following scenario?
From reading the docs, it seems like i18next
provides a reloadResources
function that I could use for the "retry". However, I currently see no way of detecting that a resource failed to load exactly because no error is being thrown. Instead, the page just loads with a bunch of "missing keys" as @Tubaleviao reported.
I can (and probably will), as @adrai mentioned, add i18next-chained-backend
together with local storage caching to minimise the chance of a resource not loading, but the possibility still exists if the connection drops before loading a page for the first time.
Note that if an error was thrown, I could easily catch it within an error boundary and apply the logic that I'm describing here. Especially if the error had the information of exactly what translation resource failed to load (language and namespace).
@nunocastromartins did you ever checked how suspense works? It's a thrown promise that resolves when ready.
In the end all is the same as when your API endpoint fails for loading data -> what do you do...?
You can create some custom useTranslation that acts different...our implementation is made to get out of the way...and render fallback...if your app needs to access those translations...you might add some healthcheck to it...and act as needed if it fails.
@nunocastromartins did you ever checked how suspense works? It's a thrown promise that resolves when ready.
I might be wrong here, but can't the thrown promise reject if the resource failed to load, instead of resolving? See: https://reactjs.org/docs/concurrent-mode-suspense.html#handling-errors and https://chan.dev/posts/catch-your-suspense-errors
In my opinion, you should reject the promise instead of resolving it in this case. In order to prevent a breaking change, it could at least be a configuration option: rejectOnFailedLoading
, or similar (would this need to be implemented in react-i18next
?).
In the end all is the same as when your API endpoint fails for loading data -> what do you do...?
The point here is that I am able to detect that I failed to load data; this is where I'm stuck, how do I detect that a translation resource failed to load when using suspense?
because
either it is loading -> suspense is thrown or it loaded / or gave up after retries
if your webapp and translations are running on the same server nothing will run anyway. if you run multiple instances / microservices reload mechanism in i18next should be enough.
but feel free to add a PR
Thanks for the reply guys! As @jamuhl and @adrai pointed, I was able to make it work after spending a while discovering how those events and callbacks behave.
The code got a little big since we don't have a library that throws an error when an error occurs, but now it works!
@nunocastromartins I'll paste the code here with some comments, in case it helps you:
The i18n.ts
file:
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import Backend from 'i18next-http-backend'
i18n
.use(initReactI18next)
.use(Backend)
.init({
backend:{
loadPath: '/translations/{{lng}}.json'
},
react:{useSuspense:false},
debug: true,
lng: "en",
fallbackLng: "dev",
keySeparator: false,
interpolation: {
escapeValue: false
}
});
export default i18n;
The App.tsx
file:
import { Backdrop, CircularProgress, Button } from "@material-ui/core"
import { makeStyles } from '@material-ui/core/styles';
import './App.css';
import { useTranslation } from 'react-i18next';
import { useState } from "react"
import { Subject, interval } from 'rxjs';
import { debounce } from 'rxjs/operators';
const useStyles = makeStyles((theme) => ({
backdrop: {
zIndex: theme.zIndex.drawer + 1,
color: '#fff',
},
}));
const fail = new Subject<{ lang: any; i18n: any; msg:string; setError:any; setLoading:any;}>()
const lastFail = fail.pipe(debounce(() => interval(100)))
lastFail.subscribe(({lang, i18n, msg, setError, setLoading}) => {
// msg this is the message of the error!
setError(msg)
setLoading(false)
i18n.changeLanguage(lang)
})
function App() {
const [lang, setLang] = useState("en");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const { t, i18n, ready } = useTranslation();
i18n.on('failedLoading', (lng,ns,msg) => {
// failedLoading is called multiple times for the same error
// so we use a debounce operator to get just the last one
if(lng !== lang) fail.next({lang, i18n, msg, setError, setLoading})
})
const handle = async () => {
setError('')
setLoading(true)
const newLang = lang==="en"?"pt":"en"
i18n.changeLanguage(newLang, async (err, t) => {
if(err){
// since the fallbackLng:dev has no translations, go back to en
i18n.changeLanguage(lang)
} else {
// even if err is null, we test if it's using the falbackLng
if(!error && t('pig') !== 'pig') {
// if we have some translation, we are done!
setLang(newLang)
setLoading(false)
} else {
// this means the resource download failed and we are on falbackLgn
// now every click on the button will lead to here, so we try downloading again
await i18n.reloadResources(newLang, "translation", () => {
// we check if we have some translation here, if we do we are done!
if(t('pig') !== 'pig') setLang(newLang)
});
setLoading(false)
}
}
})
}
const classes = useStyles();
return (
<div className="App">
<Backdrop className={classes.backdrop} open={loading || !ready}>
<CircularProgress/>
</Backdrop>
{ready && (
<>
<h1>{t("pig")}</h1>
<Button onClick={handle} >Click me</Button>
<h1>{lang}</h1>
<h2>{error}</h2>
</>
)}
</div>
);
}
export default App;
And then I created an empty json inside my dev.json file, in the translations folder.
🐛 Bug Report
I installed this library to download the translation files as needed (lazy loading), inside my project. If the file is not in the server or the server is offline for some reason, I want to get whatever error happened and show it nicely to the user.
The main problem happens when the file is not in the server or the server is offline for some reason. This library simply throw some warnings on the console.log and change the language anyway (even if the file was not downloaded), like so:
As a default behavior, I'd expect the i18n.changeLanguage Promise to throw an error so I can treat it inside a try/catch block function. Or maybe an extra configuration key called "ifFileNotDownloadedChangeLanguageAnyway" so we can prevent the language to be changed if the download fails.
To Reproduce
The installation:
Then install the libraries:
My
i18n.ts
file:My
index.tsx
:My huge
App.tsx
:My folder structure/translation files:
Then you can simply exclude the
pt.json
file or block the requests to this file in the chrome dev tools > Network tab and click the "Click me" button.My Environment