I am working in a React Native application and managing our translations using i18next
, though I am having some difficulty with the following scenario...
What I wish to do is, when the app loads up and i18next
is initialised to attempt to load the latest translations by fetching the translation files from our server, but fallback to a local collection of these files in case there's a problem with the user's connection. (it would seem to me this would be a relatively common use case)
So, this is my i18n.ts
file
import i18n from 'i18next';
import storage from '@react-native-firebase/storage';
import ChainedBackend from 'i18next-chained-backend';
import backend from 'i18next-http-backend';
import resourcesToBackend from 'i18next-resources-to-backend';
import AsyncStoragePlugin from 'i18next-react-native-async-storage';
import namespaces from '../config/translations/namespaces.json';
const firebaseStoragePath = '/assets/translations/latest';
const enTranslations = require('../config/translations/generated/en.json');
const enEuTranslations = require('../config/translations/generated/en-EU.json');
const frTranslations = require('../config/translations/generated/fr-FR.json');
const beTranslations = require('../config/translations/generated/nl-BE.json');
const nlTranslations = require('../config/translations/generated/nl-NL.json');
const prebuildLanguages = {
jsons: {
en: enTranslations,
'en-EU': enEuTranslations,
'fr-FR': frTranslations,
'nl-BE': beTranslations,
'nl-NL': nlTranslations,
},
namespaces,
};
const loadResources = async (language: string, namespace: string) => {
return storage()
.ref(`${firebaseStoragePath}/${language}/${namespace}.json`)
.getDownloadURL()
.then((result) =>
fetch(result)
.then((response) => {
return response.json();
})
.catch(() => {
return prebuildLanguages.jsons[language][namespace];
})
)
.catch((error) => {
return prebuildLanguages.jsons[language][namespace];
});
};
const backendOptions = {
loadPath: '{{lng}}|{{ns}}',
request: (options: any, url: any, payload: any, callback: any) => {
try {
const [lng, ns] = url.split('|');
loadResources(lng, ns).then((response) => {
callback(null, {
data: response,
status: 200,
});
});
} catch (e) {
callback(null, {
status: 500,
});
}
},
};
i18n
.use(AsyncStoragePlugin('en'))
.use(ChainedBackend)
.init({
ns: namespaces,
defaultNS: 'app_common',
supportedLngs: ['en', 'en-EU', 'fr-FR', 'nl-BE', 'nl-NL'],
fallbackLng: 'en',
debug: true,
saveMissing: false,
backend: {
backends: [backend, resourcesToBackend(prebuildLanguages.jsons)],
backendOptions: [backendOptions, null],
},
});
export default i18n;
The idea here is that, by using the ChainedBackend
library I would first try load the backend
using the backendOptions
, which would attempt to load the resources from our Firebase Storage path. If I understand correctly, if this backend
"fails", it should then load the local files using the resourcesToBackend(prebuildLanguages.jsons)
backend.
The problem I'm facing here is that if I shut off my internet on my device I notice that the loadResources
function can take time to resolve because the getDownloadURL()
I think has a timeout of a few seconds.
So during this time, my app loads but the i18n
is not yet initialised and so the app would throw the following errors..
'i18next: hasLoadedNamespace: i18next was not initialized', [ 'en' ]
'i18next::translator: key "new_onboarding_landing_title" for languages "en" won\'t get resolved as namespace "app_onboarding" was not yet loaded', 'This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!'`
This is my App
file where Im passing in the i18n
object
import React, { useEffect, useState, Suspense } from 'react';
import { StatusBar, Platform, LogBox, Appearance, useColorScheme } from 'react-native';
import { I18nextProvider } from 'react-i18next';
import { RootReduxProvider } from './redux';
import { i18n } from './utils';
const App = () => {
return (
<I18nextProvider i18n={i18n}>
<React.Fragment>
<ApplicationProvider mapping={mapping} theme={appTheme}>
<RootReduxProvider>
<WrappedRootNavigator theme={themeSettings} />
</RootReduxProvider>
</ApplicationProvider>
</React.Fragment>
</I18nextProvider>
);
};
export default App;
So I get what's going on... the app is trying to initialise i18n
using the primary backend
option, but the initialisation is taking too long due to fetch request timing out when there is no connection.
I thought then to switch the backends around and initialise with the local translations, ie
backend: {
backends: [resourcesToBackend(prebuildLanguages.jsons), backend],
backendOptions: [null, backendOptions],
},
But then of course, from what im seeing, it doesnt even attempt to load the remote backend
because why would it if nothing went wrong with the local backend I guess?
So my question I have, what is the best way to handle this scenario?
I think one solution would be to always load the local resource/backend first, then at some point later try fetch any updated files and then overwrite the local files with the new translations, but I was hoping to try do it this way where I would load/init the local resources first while the remote backend resolves. I just not sure how to do it that way elegantly.
Any help would be greatly appreciated :)
thanks!