I’m working on a chat widget app, that has a series topics/questions in the form of radio buttons that when clicked show a different path of selections to choose from, eventually landing on things like FAQ articles or contact support options.
The issue that I’m experiencing is that the entire app re-mounts when given a non-US-english locale. The app works just fine when it’s en_US
, but for anything else it blows up.
For instance, let’s say the locale is Dutch. When the first component is loaded, the app renders the component with its Dutch translations perfectly fine.
However, when I click on this component’s radio option, which will load another localized component, the entire app re-mounts — including the previously mounted component as well as the next component in the chat path that should render.
This happens, I’m assuming, because each one of my localized components is default loaded with en_US
and when a change occurs to the locale, then the component re-mounts.
To give a bit of background of my project, there are 2 separate repos. One, which I’ll refer to as A, is a collection of reusable components and the other, which I’ll call B, is the main app that uses those components.
Each localized component in repo A looks something roughly like this in the setup:
import React from 'react';
import {Trans} from '@lingui/macro';
import {withI18n} from '@lingui/react';
const SomeRandomComponent = withI18n()(({i18n, ...props}) => {
return (
<div>
<Trans>Hello world </Trans>
</div>
);
});
export default SomeRandomComponent;
Each localized component in repo A is exported like so:
import React, {useEffect} from 'react';
import PropTypes from 'prop-types';
import {i18n} from '@lingui/core';
import {I18nProvider} from '@lingui/react';
import SomeRandomComponent from ‘./SomeRandomComponent’;
import {messages as defaultMessages} from './locales/en_US/messages';
import {en} from 'make-plural/plurals';
const supportedLocales = [
'en_US',
'zh_CN',
'ja_JP',
'ko_KR',
'zh_TW',
'es_MX',
'pt_BR',
'cs_CZ',
'da_DK',
'de_DE',
'es_ES',
'fr_FR',
'it_IT',
'hu_HU',
'nl_NL',
'nb_NO',
'pl_PL',
'pt_PT',
'fi_FI',
'sv_SE',
'ru_RU',
'en_GB',
'tr_TR',
'fr_CA',
'en_MY',
'en_HK',
'en_SG',
'en_NL',
'en_AE',
'en_ZA',
'en_CA',
'en_AU',
'en_IN',
'en_NZ',
'fr_BE',
'fr_CH',
'de_CH',
'es_LA',
'it_CH',
'nl_BE',
];
/*
* Generates locales / plural mapping
* Locales eg. {en_US: en, es_MX: es, zn_TW: zn}
*/
const getLocaleLangMatch = () => {
const locales = {};
supportedLocales.forEach((loc) => {
if (loc.indexOf('_') === 2) {
locales[loc] = loc.slice(0, 2);
}
});
return locales;
};
/*
* Sets plurals based on supported locales
* Locales eg. en_US, es_MX, zn_TW
*/
const localesLangs = getLocaleLangMatch();
for (const locale in localesLangs) {
if (supportedLocales.indexOf(locale) > -1) {
i18n.loadLocaleData(locale, {plurals: pluralLangs[localesLangs[locale]]});
}
}
async function i18nCatalogLoader({lang: locale}) {
try {
const {messages} = await import(`../locales/${locale}/messages`);
i18n.load({[`${locale}`]: messages});
i18n.activate(locale);
} catch (e) {
console.error('i18nCatalogLoader: Error thrown');
}
}
/*
* i18n default locale setup
*/
const defaultLocale = 'en_US';
i18n.loadLocaleData(defaultLocale, {plurals: en});
i18n.load({[`${defaultLocale}`]: defaultMessages});
i18n.activate(defaultLocale);
/*
* i18n Context Provider
*/
const Provider = ({lang, ...props}) => {
useEffect(() => {
const loadCatalogs = async () => {
try {
await i18nCatalogLoader({lang});
} catch (e) {
console.warn('I18nProvider: Error thrown');
}
};
loadCatalogs();
}, [lang]);
return (
<I18nProvider i18n={i18n}>
<SomeRandomComponent lang={lang} {...props} />
</I18nProvider>
);
};
Provider.propTypes = {
lang: PropTypes.oneOf([
'en_US',
'zh_CN',
'ja_JP',
'ko_KR',
'zh_TW',
'es_MX',
'pt_BR',
'cs_CZ',
'da_DK',
'de_DE',
'es_ES',
'fr_FR',
'it_IT',
'hu_HU',
'nl_NL',
'nb_NO',
'pl_PL',
'pt_PT',
'fi_FI',
'sv_SE',
'ru_RU',
'en_GB',
'tr_TR',
'fr_CA',
'en_MY',
'en_HK',
'en_SG',
'en_NL',
'en_AE',
'en_ZA',
'en_CA',
'en_AU',
'en_IN',
'en_NZ',
'fr_BE',
'fr_CH',
'de_CH',
'es_LA',
'it_CH',
'nl_BE',
]),
};
export const defaultProps = {
lang: 'en_US',
};
Provider.defaultProps = {
...defaultProps,
};
export default Provider;
In repo B, each one of these localized components from repo A are lazy loaded in their respective component folders like so:
import React, {lazy, Suspense, memo} from 'react';
/* this is a selector that peels out any of the props from our redux store */
import {getPropsSelector} from '../../../utils';
import {useSelector} from 'react-redux';
const SomeRandomComp = lazy(() =>
import(
/* webpackChunkName: "async-SomeRandomComp" */ ‘@repo_A/some_random_component’
),
);
const SomeRandomComponentPropMapping = [
'lang:settings.language',
];
export const SomeRandomComponent = memo(
(props) => {
const {propNames, propPaths, propsSelectorForComponent} = getPropsSelector(
SomeRandomComponentPropMapping,
);
const PropValues = useSelector(propsSelectorForComponent(propPaths));
let propsToComponent = {};
PropValues.forEach((propValue, index) => {
propsToComponent[propNames[index]] = propValue;
});
propsToComponent = {...propsToComponent, ...props};
return (
<Suspense>
<SomeRandomComp
{...propsToComponent}
/>
</Suspense>
);
},
(prevProps, nextProps) => {
if (
prevProps.lang !== nextProps.lang ||
) {
return false; // Rerender only if lang changes.
}
return true;
},
);
Then in our main app of repo B, we are mapping through an object whose keys are the UUIDs of each repo A component, which looks something like:
// example of what it looks like when you log the object:
const userSessionFlowState = {
start: {
componentData: {someProp: 'example prop'},
componentName: 'StartComponent',
id: '412411eb-16dc-4288-a14f-e8cf3ea3b21d',
},
'0d6021b1-c44e-43d0-8bdd-071406c8c388_SOME_RANDOM_COMPONENT': {
componentData: {},
componentName: 'SomeRandomComponent',
id: '7f16fb3f-7cf3-4506-91da-d9dcefffcb56',
},
};
The above object is a general object that contains all of the potential paths a user can take in the chat.
Then there’s a more specific object that we’re using called visibleWorkFlows
, which is an object that contains the component IDs for a specific workflow (as keys), and booleans (as values) indicating which components should be visible on the screen. This object looks something like:
const visibleWorkFlows = {
start: true,
'0d6021b1-c44e-43d0-8bdd-071406c8c388_SOME_RANDOM_COMPONENT': false,
};
This object will either have every chat path made available or only very niche/specific paths, and its determined based off the type of user credentials/etc.
/*
ComponentLoader is a functional component that renders the corresponding component if it exists
Below is the actual mapping of said object in the main component:
*/
{
Object.keys(userSessionFlowState).map((conditionalProp) => {
const {componentName, componentData = {}} =
userSessionFlowState[conditionalProp];
const conditionalPropKey = conditionalProp === 'start'
? userSessionFlowState['start']['id']
: conditionalProp;
const addlPropsForComponent = isEmpty(propsReceived[conditionalPropKey])
? {}
: propsReceived[conditionalPropKey];
const additionalProps = { ...componentData, ...addlPropsForComponent };
return (
visibleWorkflows[conditionalPropKey] && (
<>
{ComponentLoader({
componentName,
key: conditionalPropKey,
props: {...props},
})}
</>
)
);
}
In the App.js file of repo B, the main app component is setup for localization similar to repo A:
import React, {useEffect} from 'react';
import Main from './components/Main';
import './App.css';
import {i18n} from '@lingui/core';
import {I18nProvider} from '@lingui/react';
import {messages as defaultMessages} from './locales/en_US/messages';
import {useSelector} from 'react-redux';
const supportedLocales = [
'en_US',
'zh_CN',
'ja_JP',
'ko_KR',
'zh_TW',
'es_MX',
'pt_BR',
'cs_CZ',
'da_DK',
'de_DE',
'es_ES',
'fr_FR',
'it_IT',
'hu_HU',
'nl_NL',
'nb_NO',
'pl_PL',
'pt_PT',
'fi_FI',
'sv_SE',
'ru_RU',
'en_GB',
'tr_TR',
'fr_CA',
'en_MY',
'en_HK',
'en_SG',
'en_NL',
'en_AE',
'en_ZA',
'en_CA',
'en_AU',
'en_IN',
'en_NZ',
'fr_BE',
'fr_CH',
'de_CH',
'es_LA',
'it_CH',
'nl_BE',
];
/*
* Generates locales / plural mapping
* Locales eg. {en_US: en, es_MX: es, zn_TW: zn}
*/
const getLocaleLangMatch = () => {
const locales = {};
supportedLocales.forEach((loc) => {
if (loc.indexOf('_') === 2) {
locales[loc] = loc.slice(0, 2);
}
});
return locales;
};
const localesLangs = getLocaleLangMatch();
async function i18nCatalogLoader({language: locale}) {
try {
const {messages} = await import(
/* webpackMode: "lazy", webpackChunkName: "i18n-[request]" */
`./locales/${locale}/messages`
);
i18n.loadLocaleData(locale, {
plurals: pluralLangs[localesLangs[locale]],
});
i18n.load({[`${locale}`]: messages});
i18n.activate(locale);
} catch (e) {
console.error('i18nCatalogLoader: Error thrown', e);
}
}
/*
* i18n default locale setup
*
*/
const defaultLocale = 'en_US';
// i18n.loadLocaleData(defaultLocale);
i18n.load({[`${defaultLocale}`]: defaultMessages});
i18n.activate(defaultLocale);
const App = () => {
const {language} = useSelector((store) => store.settings);
useEffect(() => {
const loadCatalogs = async () => {
try {
await i18nCatalogLoader({language});
} catch (e) {
console.warn('I18nProvider: Error thrown');
}
};
loadCatalogs();
}, [language]);
return (
<I18nProvider i18n={i18n}>
<Main />
</I18nProvider>
);
};
export default App;
The only solution that I’ve found that remotely works, is to set forceOnLocaleChange
to false
and to change any use of the t
tag to a Trans
tag. However, when I’ve implemented this solution, the translations quickly change from Dutch to English, and then back to Dutch. So it looks like some kind of re-rendering issue happening instead all the components re-mounting.
Moreover, I’ve tried removing the I18nProvider
and any localizations
from my main app, but that yields the same re-mounting issue. I’ve tried removing the shouldComponentUpdate useEffect
from my localized components in repo A (ie effect that watches changes to lang
prop), but again that produces the same re-mounting issue.
I’m pretty lost as to what to do. Any and all advice is greatly appreciated. Thanks so much in advance!