i18n load translations from s3bucket - reactjs

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.

Related

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.

React-i18next does not load translations on page reload

I am using react-I18next in my nextjs application. I don't want to use next-I18next because I tried to use it but couldn't make it work. But react-i18next is working in my application for now and I am able to change language from english to german and back. However if I reload the page I get this error.
TypeError: i18n.changeLanguage is not a function
What could be the possibel cause of this error and how can I fix it?
P.S In my app.ts file I am not using Suspense because it gives me this error
ReactDOMServer does not yet support Suspense.
Here is my i18.ts file
/* eslint-disable no-duplicate-imports */
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import backend from 'i18next-xhr-backend';
import { initReactI18next } from 'react-i18next';
import loginDE from '../../public/locales/de/loginDE.json';
import loginEN from '../../public/locales/en/loginEN.json';
// the translations
// (tip move them in a JSON file and import them)
const resources = {
en: {
translation: loginEN,
},
de: {
translation: loginDE,
},
};
void i18n
.use(backend)
.use(LanguageDetector)
.use(initReactI18next) // passes i18n down to react-i18next
.init({
fallbackLng: 'en',
interpolation: {
escapeValue: false, // react already safes from xss
},
resources,
lng: 'en',
keySeparator: false, // we do not use keys in form messages.welcome
react: {
useSuspense: true,
},
});
export default i18n;
ReactDOMServer does not yet support Suspense.
Change your useSuspense value like that:
react: { useSuspense: false },
TypeError: i18n.changeLanguage is not a function
Make sure you wrapped a component with I18nextProvider

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

Gatsby - SSR i18n translation are loaded on client

I have a multilingual gatsby website, I try to improve the lighthouse performance score, so i followed this guide to generate my page with the translation in the html. Here is my gatsby-ssr.js file
import { renderToString } from 'react-dom/server'
import Backend from 'i18next-sync-fs-backend'
import i18n from "./src/locales/i18n"
const namespaces = [...my ns...];
export const replaceRenderer = ({ bodyComponent, replaceBodyHTMLString }) => {
i18n
.use(Backend)
.init({
initImmediate: false,
backend: {
loadPath: 'src/locales/{{lng}}/{{ns}}.json',
},
})
// load the common namespace
.loadNamespaces(namespaces, (err) => {
replaceBodyHTMLString(renderToString(bodyComponent))
})
}
My i18n.js file :
import i18n from "i18next";
import Backend from "i18next-xhr-backend";
import LanguageDetector from "i18next-browser-languagedetector";
import { reactI18nextModule } from "react-i18next";
i18n
.use(Backend)
.use(LanguageDetector)
.use(reactI18nextModule)
.init({
fallbackLng: "fr",
ns: ['translation'],
defaultNS: 'translation',
load: 'languageOnly',
debug: false,
interpolation: {
escapeValue: false, // not needed for react!!
},
react: {
wait: true,
},
});
export default i18n;
I use the HOC withNamespaces from react-i18next (v8) in my page like that export default withNamespaces('home')(IndexPage);
This works well because the generated HTML has all the text translated but when I access to my page the client will load a second time the translation and re-render the page, this cause a flickering on the page and big layout shift so lighthouse really not like it. I believe this is in relation with i18next-xhr-backend but I'm stuck.
I expect my page to not load the translation on page load, because the html has already the text translated so I don't need to load translation again

Handle React translations on component level

What i want to achieve is that i have translations available for all components itself. When i install those components they should render on its own. Another thing that i want to achieve is that my project can overrule translations on component level. This in case if somebody decides that the translations that is in there is not what we want.
What i did in my components is loading i18n:
import i18n from 'i18next';
import XHR from 'i18next-xhr-backend';
import Cache from 'i18next-localstorage-cache';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n
.use(XHR)
.use(Cache)
.use(LanguageDetector)
.init({
backend: {
loadPath: '../dist/locales/{{lng}}/{{ns}}.json',
},
fallbackLng: 'en',
react: {
wait: true
},
ns: ['authentication'],
defaultNS: 'authentication',
debug: false,
cache: {
enabled: true
},
interpolation: {
escapeValue: false, // not needed for react!!
formatSeparator: ',',
format: function(value, format) {
if (format === 'uppercase') return value.toUpperCase();
return value;
}
}
});
export default i18n;
This is working fine in my storybook but when i load this in my main project, it is using ../ from my build root which is actually not the component itself but my main implementation project.
How can i solve it and how can i ensure that i can even extend this?
UPDATE:
Below a list of the current folder structure like asked in below questions:
#Component
dist
--.gitkeep
--locales
--commonjs
--es
src
--actions
----index.js
--components
----compname
------index.js
--reducers
----foo.js
--i18n.js
--index.js
test
.babelrc
package.json
Jenkinsfile
#Project
react
--assets
----css
------app.scss
--dist
----.gitkeep
--src
----index.js
----store.js
webpack.config.js
Thanks

Resources