React-intl multi language app: changing languages and translations storage - reactjs

I have react-router app and would like to add i18n. In react-intl example root component wrapped in IntlProvider:
ReactDOM.render(
<IntlProvider locale="en">
<App />
</IntlProvider>,
document.getElementById('container')
);
But there is only one locale. How to update app for adding other languages and how is the best way to store translations?

I have faced the same problem and this is what I found out:
To change language I used solution provided here, which is basically binding IntlProvider to ReduxStore with Connect function. Also don't forget to include key to re-render components on language change. This is basically all the code:
This is ConnectedIntlProvider.js, just binds default IntlProvider(notice the key prop that is missing in original comment on github)
import { connect } from 'react-redux';
import { IntlProvider } from 'react-intl';
// This function will map the current redux state to the props for the component that it is "connected" to.
// When the state of the redux store changes, this function will be called, if the props that come out of
// this function are different, then the component that is wrapped is re-rendered.
function mapStateToProps(state) {
const { lang, messages } = state.locales;
return { locale: lang, key: lang, messages };
}
export default connect(mapStateToProps)(IntlProvider);
And then in your entry point file:
// index.js (your top-level file)
import ConnectedIntlProvider from 'ConnectedIntlProvider';
const store = applyMiddleware(thunkMiddleware)(createStore)(reducers);
ReactDOM.render((
<Provider store={store}>
<ConnectedIntlProvider>
<Router history={createHistory()}>{routes}</Router>
</ConnectedIntlProvider>
</Provider>
), document.getElementById( APP_DOM_CONTAINER ));
Next thing to do is to just implement reducer for managing locale and make action creators to change languages on demand.
As for the best way to store translations - I found many discussions on this topic and situation seems to be quite confused, honestly I am quite baffled that makers of react-intl focus so much on date and number formats and forget about translation. So, I don't know the absolutely correct way to handle it, but this is what I do:
Create folder "locales" and inside create bunch of files like "en.js", "fi.js", "ru.js", etc. Basically all languages you work with.
In every file export json object with translations like this:
export const ENGLISH_STATE = {
lang: 'en',
messages: {
'app.header.title': 'Awesome site',
'app.header.subtitle': 'check it out',
'app.header.about': 'About',
'app.header.services': 'services',
'app.header.shipping': 'Shipping & Payment',
}
}
Other files have exact same structure but with translated strings inside.
Then in reducer that is responsible for language change import all the states from these files and load them into redux store as soon as action to change language is dispatched. Your component created in previous step will propagate changes to IntlProvider and new locale will take place. Output it on page using <FormattedMessage> or intl.formatMessage({id: 'app.header.title'})}, read more on that at their github wiki.
They have some DefineMessages function there, but honestly I couldn't find any good information how to use it, basically you can forget about it and be OK.

With a new Context API I believe it's not required to use redux now:
IntlContext.jsx
import React from "react";
import { IntlProvider, addLocaleData } from "react-intl";
import en from "react-intl/locale-data/en";
import de from "react-intl/locale-data/de";
const deTranslation = {
//...
};
const enTranslation = {
//...
};
addLocaleData([...en, ...de]);
const Context = React.createContext();
class IntlProviderWrapper extends React.Component {
constructor(...args) {
super(...args);
this.switchToEnglish = () =>
this.setState({ locale: "en", messages: enTranslation });
this.switchToDeutsch = () =>
this.setState({ locale: "de", messages: deTranslation });
// pass everything in state to avoid creating object inside render method (like explained in the documentation)
this.state = {
locale: "en",
messages: enTranslation,
switchToEnglish: this.switchToEnglish,
switchToDeutsch: this.switchToDeutsch
};
}
render() {
const { children } = this.props;
const { locale, messages } = this.state;
return (
<Context.Provider value={this.state}>
<IntlProvider
key={locale}
locale={locale}
messages={messages}
defaultLocale="en"
>
{children}
</IntlProvider>
</Context.Provider>
);
}
}
export { IntlProviderWrapper, Context as IntlContext };
Main App.jsx component:
import { Provider } from "react-redux";
import { IntlProviderWrapper } from "./IntlContext";
class App extends Component {
render() {
return (
<Provider store={store}>
<IntlProviderWrapper>
...
</IntlProviderWrapper>
</Provider>
);
}
}
LanguageSwitch.jsx
import React from "react";
import { IntlContext } from "./IntlContext";
const LanguageSwitch = () => (
<IntlContext.Consumer>
{({ switchToEnglish, switchToDeutsch }) => (
<React.Fragment>
<button onClick={switchToEnglish}>
English
</button>
<button onClick={switchToDeutsch}>
Deutsch
</button>
</React.Fragment>
)}
</IntlContext.Consumer>
);
// with hooks:
const LanguageSwitch2 = () => {
const { switchToEnglish, switchToDeutsch } = React.useContext(IntlContext);
return (
<>
<button onClick={switchToEnglish}>English</button>
<button onClick={switchToDeutsch}>Deutsch</button>
</>
);
};
export default LanguageSwitch;
I've created a repository that demonstrates this idea.
And also codesandbox example.

Related

NextJS: Context values undefined in production (works fine in development)

A "dark mode" feature has been implemented on my Next.js application using React's Context api.
Everything works fine during development, however, Context provider-related problems have arisen on the built version — global states show as undefined and cannot be handled.
_app.tsx is wrapped with the ThemeProvider as such:
// React & Next hooks
import React, { useEffect } from "react";
import type { AppProps } from "next/app";
import { useRouter } from "next/router";
// Irrelevant imports
// Global state management
import { Provider } from "react-redux";
import store from "../redux/store";
import { AuthProvider } from "../context/UserContext";
import { ThemeProvider } from "../context/ThemeContext";
// Components
import Layout from "../components/Layout/Layout";
import Footer from "../components/Footer/Footer";
// Irrelevant code
function MyApp({ Component, pageProps }: AppProps) {
const router = useRouter();
// Applying different layouts depending on page
switch (Component.name) {
case "HomePage":
return (
<Provider store={store}>
<ThemeProvider>
<AuthProvider>
<Component {...pageProps} />
<Footer color="fff" />
</AuthProvider>
</ThemeProvider>
</Provider>
);
case "PageNotFound":
return (
<>
<Component {...pageProps} />
<Footer color="#f2f2f5" />
</>
);
default:
// Irrelevant code
}
}
export default MyApp;
The ThemeContext correctly exports both its Provider and Context:
import { createContext, ReactNode, useState, useEffect } from "react";
type themeContextType = {
darkMode: boolean | null;
toggleDarkMode: () => void;
};
type Props = {
children: ReactNode;
};
// Checks for user's preference.
const getPrefColorScheme = () => {
return !window.matchMedia
? null
: window.matchMedia("(prefers-color-scheme: dark)").matches;
};
// Gets previously stored theme if it exists.
const getInitialMode = () => {
const isReturningUser = "dark-mode" in localStorage; // Returns true if user already used the website.
const savedMode = localStorage.getItem("dark-mode") === "true" ? true : false;
const userPrefersDark = getPrefColorScheme(); // Gets user's colour preference.
// If mode was saved ► return saved mode else get users general preference.
return isReturningUser ? savedMode : userPrefersDark ? true : false;
};
export const ThemeContext = createContext<themeContextType>(
{} as themeContextType
);
export const ThemeProvider = ({ children }: Props) => {
// localStorage only exists on the browser (window), not on the server
const [darkMode, setDarkMode] = useState<boolean | null>(null);
// Getting theme from local storage upon first render
useEffect(() => {
setDarkMode(getInitialMode);
}, []);
// Prefered theme stored in local storage
useEffect(() => {
localStorage.setItem("dark-mode", JSON.stringify(darkMode));
}, [darkMode]);
const toggleDarkMode = () => {
setDarkMode(!darkMode);
};
return (
<ThemeContext.Provider value={{ darkMode, toggleDarkMode }}>
{children}
</ThemeContext.Provider>
);
};
The ThemeToggler responsible for updating the darkMode state operates properly during development (theme toggled and correct value console.loged upon clicking), however it doesn't do anything during production (console.logs an undefined state):
import React, { FC, useContext } from "react";
import { ThemeContext } from "../../context/ThemeContext";
const ThemeToggler: FC = () => {
const { darkMode, toggleDarkMode } = useContext(ThemeContext);
const toggleTheme = () => {
console.log(darkMode) // <--- darkMode is undefined during production
toggleDarkMode();
};
return (
<div className="theme-toggler">
<i
className={`fas ${darkMode ? "fa-sun" : "fa-moon"}`}
data-testid="dark-mode"
onClick={toggleTheme}
></i>
</div>
);
};
export default ThemeToggler;
The solutions/suggestions I've looked up before posting this were to no avail.
React Context API undefined in production — react and react-dom are on the same version.
Thanks in advance.
P.S. For those wondering why I am using both Redux and Context for global state management:
Context is best suited for low-frequency and simple state updates such as themes and authentication.
Redux is better for high-frequency and complex state updates in addition to providing a better debugging tool — Redux DevTools.
P.S.2 Yes, it is better – performance-wise – to install FontAwesome's dependencies rather than use a CDN.
Thanks for sharing the code. It's well written. By reading it i don't see any problem. Based on your component topology, as long as your ThemeToggler is defined under any page component, your darkMode can't be undefined.
Here's your topology of the site
<MyApp>
<Provider>
// A. will not work
<ThemeProvider>
<HomePage>
// B. should work
</HomePage>
</ThemeProvider>
// C. will not work
</Provider>
</MyApp>
Although your ThemeProvider is a custom provider, inside ThemeContext.Provider is defined with value {{ darkMode, toggleDarkMode }}. So in theory you can't get undefined unless your component ThemeToggler is not under a HomePage component. I marked two non working locations, any component put under location A or C will give you undefined.
Since you have a condition for HomePage, you can run into this problem if you are on other pages. So in general you should wrap the ThemeProvider on top of your router.
<ThemeProvider>
<AuthProvider>
{Component.name != "PageNotFound" && (
<Component {...pageProps} />
)}
</AuthProvider>
</ThemeProvider>
You get the point, you want to first go through a layer that theme always exist before you fire up a router.
You can confirm if this is the case by doing the following test.
function MyApp({ Component, pageProps }: AppProps) {
return (
<ThemeProvider>
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
</ThemeProvider>
)
}
If this works in production, then it confirms it. To be honest, this problem also exists in the dev, however maybe due to your routes change too quickly, it normally hides these issues.

Is it possible to update the store outside of the Provider context in Redux?

I have some configuration settings I want to set and save to redux store before my main app loads. This is so that all the children components already have the configuration data pre-loaded and they can read it from the store using redux-connect.
For now, I only know how to use and update the store inside my connected components that are wrapped in the Provider context.
Is there a way to update it outside of that?
For example:
class App extends React {
constructor(props) {
super(props);
// UPDATE REDUX STORE HERE
}
componentDidUpdate() {
// OR UPDATE REDUX STORE HERE
}
render() {
return (
<Provider store={store}>
<Child />
</Provider>
);
}
}
Is this even possible?
store is just an object which contains dispatch and getState functions. This means that wherever the store is accessible outside of the application, you can manipulate it using these attributes.
const store = configureStore();
const App = () => (<Provider store={store} />);
store.dispatch({ type: SOME_ACTION_TYPE });
you can call the Provider outside of App Component in index.js
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>
, document.getElementById('root'));
now in your App class it's possile add a connect or an updateAction
import {connect} from "react-redux";
import {updateStuf} from "./actions/projectActions";
class App extends Component {
componentWillMount() {
this.props.updateStuf();
}
render() {
const {stuff} = this.props;
return (
<div className="stuff">
</div>
);
}
}
const mapStateToProps = (state) => {
return {
stuff: state.project.stuff
}
}
export default connect(mapStateToProps, {updateStuf})(App);

Redux Store not populated before Component Render

I am working on the authentification procedure for an app I'm developing.
Currently, the user logins in through Steam. Once the login is validated the server redirects the user to the app index, /, and issues them a pair of JWTs as GET variables. The app then stores these in a Redux store before rewriting the URL to hide the JWT tokens for security purposes.
The app then decodes the tokens to obtain info about the user, such as their username and avatar address. This should be rendered in the app's SiteWrapper component, however, this is where my problem occurs.
What seems to be happening is SiteWrapper component loads before the App component finishes saving the tokens and thus throws errors as variables are not defined. Most of the fixes that seem relevant are for API requests, however, in this case, that is not the case. I already have the data in the URL. I'm not sure if the same applies.
Any suggestions on how to fix this? Any other best practice advice would be appreciated. I'm new to both React and Redux.
Error
Index
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux';
import store from './redux/store';
// console debug setup
window.store = store;
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'));
serviceWorker.unregister();
App
import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import { connect } from 'react-redux';
import "tabler-react/dist/Tabler.css";
import history from './utils/history';
import {
storeRefreshJWTToken,
storeAccessJWTToken,
loadUserFromJWTRefreshToken
} from "./redux/app";
import {
HomePage
} from './pages';
class App extends Component {
componentDidMount() {
//Get tokens from URL when app loads and then hide them from url.
const urlParams = new URLSearchParams(window.location.search);
if(urlParams.has('access_token') && urlParams.has('refresh_token')){
this.props.storeRefreshJWTToken(urlParams.get('refresh_token'));
this.props.storeAccessJWTToken(urlParams.get('access_token'));
//Load user info from obtained tokens.
this.props.loadUserFromJWTRefreshToken();
}
history.push('/');
}
render() {
return (
<React.StrictMode>
<Router basename={process.env.PUBLIC_URL} history={history}>
<Switch>
<Route exact path="/" component={HomePage}/>
</Switch>
</Router>
</React.StrictMode>
);
}
}
const mapStateToProps = (state) => ({});
const mapDispatchToProps = {
storeRefreshJWTToken,
storeAccessJWTToken,
loadUserFromJWTRefreshToken
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
SiteWrapper
import * as React from "react";
import { withRouter } from "react-router-dom";
import { connect } from 'react-redux';
import {
Site,
Nav,
Grid,
List,
Button,
RouterContextProvider,
} from "tabler-react";
class SiteWrapper extends React.Component {
componentDidMount() {
console.log(this.props);
this.accountDropdownProps = {
avatarURL: this.props.user.avatar,
name: this.props.user.display_name,
description: "temp",
options: [
{icon: "user", value: "Profile"},
{icon: "settings", value: "Settings"},
{isDivider: true},
{icon: "log-out", value: "Sign out"},
],
};
}
render(){
return (
<Site.Wrapper
headerProps={{
href: "/",
alt: "Tabler React",
imageURL: "./demo/brand/tabler.svg",
navItems: (
<Nav.Item type="div" className="d-none d-md-flex">
<Button
href="https://github.com/tabler/tabler-react"
target="_blank"
outline
size="sm"
RootComponent="a"
color="primary"
>
Source code
</Button>
</Nav.Item>
),
accountDropdown: this.accountDropdownProps,
}}
navProps={{ itemsObjects: this.props.NavBarLinks }}
routerContextComponentType={withRouter(RouterContextProvider)}
footerProps={{
copyright: (
<React.Fragment>
Copyright © 2018
Thomas Smyth.
All rights reserved.
</React.Fragment>
),
nav: (
<React.Fragment>
<Grid.Col auto={true}>
<List className="list-inline list-inline-dots mb-0">
<List.Item className="list-inline-item">
Developers
</List.Item>
<List.Item className="list-inline-item">
FAQ
</List.Item>
</List>
</Grid.Col>
</React.Fragment>
),
}}
>
{this.props.children}
</Site.Wrapper>
);
}
}
const mapStateToProps = (state) => {
console.log(state);
return {
user: state.App.user
};
};
export default connect(mapStateToProps)(SiteWrapper);
Reducer
import initialState from './initialState';
import jwt_decode from 'jwt-decode';
//JWT Auth
const STORE_JWT_REFRESH_TOKEN = "STORE_JWT_REFRESH_TOKEN";
export const storeRefreshJWTToken = (token) => ({type: STORE_JWT_REFRESH_TOKEN, refresh_token: token});
const STORE_JWT_ACCESS_TOKEN = "STORE_JWT_ACCESS_TOKEN";
export const storeAccessJWTToken = (token) => ({type: STORE_JWT_ACCESS_TOKEN, access_token: token});
// User
const LOAD_USER_FROM_JWT_REFRESH_TOKEN = "DEC0DE_JWT_REFRESH_TOKEN";
export const loadUserFromJWTRefreshToken = () => ({type: LOAD_USER_FROM_JWT_REFRESH_TOKEN});
export default function reducer(state = initialState.app, action) {
switch (action.type) {
case STORE_JWT_REFRESH_TOKEN:
return {
...state,
jwtAuth: {
...state.jwtAuth,
refresh_token: action.refresh_token
}
};
case STORE_JWT_ACCESS_TOKEN:
return {
...state,
JWTAuth: {
...state.jwtAuth,
access_token: action.access_token
}
};
case LOAD_USER_FROM_JWT_REFRESH_TOKEN:
const user = jwt_decode(state.jwtAuth.refresh_token);
return {
...state,
user: user
};
default:
return state;
}
}
Store
import { createStore, combineReducers, applyMiddleware } from 'redux';
import ReduxThunk from 'redux-thunk'
import App from './app';
const combinedReducers = combineReducers({
App
});
const store = createStore(combinedReducers, applyMiddleware(ReduxThunk));
export default store;
componentDidMount() is going to run after first render. So on first render the this.props.user inside will be null at that point.
You could:
move the async call to componentWillMount() (not recommended)
put a guard in the SiteWrapper() so it can handle null case
not render Home from the App until the async call has finished
in componentDidMount() you are expecting user to be set.
You can:
Set user in initial state with some "isAuth" flag that you can easily check
As componentWillMount() is deprecated you can use componentDidUpdate() to check if user state is changed and do some actions.
Use asyns functions when sawing YWT
You could of course check for null and don't try to show the user's details in your SiteWrapper's componentDidMount()-method. But why? Do you have an alternative route to go if your user couldn't be found and is null? I guess no. So you basically have two options:
The ideal solution is to implement async actions and show an activity
indicator (e.g. spinner) until the jwt-token is loaded. Afterwards
you can extract your user information and fully render your
component as soon as the fetch is succesfully completed.
If you can't use async action, for whatever reason, I would suggest
the "avoid-null" approach. Put a default user in your initial
state and it should be done. If you update the user prop, the
component will rerender anyways (if connected properly).
I have solved my issue. It seems this was one of those rare cases where trying to keep things simple and develop was bad.
Now, when my application loads I either show the user information or a login button.
Once the key is loaded from the URL the component rerenders to show the user information.
This does increase the number of renders, however, it is probably the best way of doing it.

How to navigate to different route programmatically in `redux-router5`?

I am using redux-router5 in my app to manage routes. I defined the route as below code:
const Root = () => (
<Provider store={store}>
<RouterProvider router={router}>
<MyComponent />
</RouterProvider>
</Provider>
);
router.start((err, state) => {
ReactDOM.render(<Root/>, document.getElementById('root'));
});
below is the code for store middlewares:
export default function configureStore(router) {
// create the middleware for the store
const createStoreWithMiddleware = applyMiddleware(
router5Middleware(router),
ReduxPromise,
ReduxThunk,
)(createStore);
const store = createStoreWithMiddleware(reducers);
router.usePlugin(reduxPlugin(store.dispatch));
return store;
}
In MyComponent, I can access a router instance through its property but it doesn't have navigate method for me to use. It only has route parameters, name, path, etc. So how can I navigate to a different route inside my component?
I've looked into an example
And it seems that it works like this:
import { connect } from 'react-redux';
import { actions } from 'redux-router5'
class SimpleComponent extends React.Component {
redirectToDashboard = () => this.props.navigateTo('/dashboard');
render() {
return (
<button onClick={this.redirectToDashboard}>go to dashboard</button>
)
}
}
export default connect(null, { navigateTo: actions.navigateTo })(SimpleComponent);
Original answer:
I don't know how to navigate with redux-router5 (at first glance to the documentation it is mainly meant to be used with redux) but to answer your question:
So how can I navigate to a different route inside my component?
Use withRouter HOC from 'react-router':
import { withRouter } from 'react-router'
// A simple component that shows the pathname of the current location
class ShowTheLocation extends React.Component {
static propTypes = {
match: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
}
redirectToDashboard = () => this.props.history.push('/dashboard');
render() {
const { match, location, history } = this.props
return (
<div onClick={this.redirectToDashboard}>You are now at {location.pathname}</div>
)
}
}
// Create a new component that is "connected" (to borrow redux
// terminology) to the router.
const ShowTheLocationWithRouter = withRouter(ShowTheLocation)
Worth to mention that if component is rendered by Route component then it is already "connected" and doesn't need withRouter.

How to use Ant Design LocaleProvider with React-Boilerplate

We have started using this React-Boilerplate . Also we are trying to integrate Ant Design for it's awesome components .
From the docs of Ant Design I have created a wrapper around AppWrapper of React-boilerplate like this -
import { LocaleProvider } from 'antd';
import enUS from 'antd/lib/locale-provider/en_US';
return (
<LocaleProvider locale={enUS}>
<AppWrapper>
...
</AppWrapper>
</LocaleProvider>
);
And it's working perfect for antd components.
I wanted to know how can I use this with i18n of react-boilerplate . Or if this above way is a correct way of doing this ?
This is in general the correct way of using LocaleProvider, yes, but...
You do have to be a careful when mixing an i18n librarys wrapper with antd's LocaleProvider, if you want language changes to propagate to both.
In the case of React-Boilerplate the locale is stored in Redux. For antd to be able to update locale when the app does, <LocaleProvider> must be inserted inside the Redux provider, otherwise it will not have access to the locale state.
So your app.js needs to become something like:
<Provider store={store}>
<LanguageProvider messages={messages}>
<LocaleProvider locale={...}>
<Router ... />
</LocaleProvider>
</LanguageProvider>
</Provider>,
Unfortunately this is not enough, since antd's LocaleProvider does not take a simple locale id string as its argument, but rather an object with the locale info itself.
This means that it is not possible to just insert <LocaleProvider> in the wrapper chain as above. You must create you own component that takes the connects to the Redux locale prop and feeds <LocaleProvider> with the correct locale object based on the string id from Redux.
Here is some (untested) code that I ripped out of a project with a similar structure and adapted to Intl (it was using i18next instead of Intl). Should give you an idea of one way to do it.
import React from 'react';
import { Router } from 'react-router';
import { Provider, connect } from 'react-redux';
import en_US from 'antd/lib/locale-provider/en_US';
import sv_SE from 'antd/lib/locale-provider/sv_SE';
import { LocaleProvidern } from 'antd';
class AntDesignPlusRouter extends React.Component {
state = {
antd_locale: en_US
}
componentWillUpdate( next ) {
let { locale } = next
if( !locale || locale === this.props.locale ) return;
switch( locale ) {
case 'sv':
this.setState( { antd_locale: sv_SE } );
break;
default:
case 'en':
this.setState( { antd_locale: en_US } );
break;
}
}
render() {
return (
<LocaleProvider locale={this.state.antd_locale}>
<Router {...this.props} />
</LocaleProvider>
)
}
}
const WrappedAntDesignPlusRouter = connect(
function mapStateToProps( state ) {
return {
locale: state.locale
}
},
function mapDispatchToProps( dispatch ) {
return {}
}
)( AntDesignPlusRouter );
class App extends React.Component {
render() {
return (
<Provider ...>
<LanguageProvider ...>
<WrappedAntDesignPlusRouter/>
</LanguageProvider >
</Provider>
);
}
}
for me this worked as localprovider deprecated and configprovider is replaced with
Changes done in below piece of code
Week should start with Monday
day changed from Mo to M
import { Calendar, ConfigProvider } from 'antd';
import en_GB from 'antd/lib/locale-provider/en_GB';
import moment from 'moment';
import 'moment/locale/en-gb';
import React from 'react';
import './scheduler.scss';
const Scheduler = () => {
moment.locale('en-gb'); // important!
moment.updateLocale('en', {
weekdaysMin: ["M", "T", "W", "T", "F", "S", "S"]
});
return (
<div class='scheduler-dashboard'>
<ConfigProvider locale={en_GB}>
<Calendar
/>
</ConfigProvider >
</div>
);
};
export default Scheduler;

Resources