12

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?

qwe asd
  • 1,274
  • 3
  • 17
  • 31

2 Answers2

22

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.

rofrol
  • 12,038
  • 7
  • 62
  • 63
ScienceSamovar
  • 412
  • 5
  • 15
  • It works perfectIy with redux. I tried this approach with apollo but it just doesn't work. I am desperate. – schlingel Nov 19 '18 at 12:39
13

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.

Tomasz Mularczyk
  • 27,156
  • 17
  • 99
  • 146
  • 1
    Works pretty well. I'm just curious if it's possible to do this with useContext instead. So you'd be able to useContext(IntlContext) and attach switchToEnglish etc to the context? – Dac0d3r Apr 05 '19 at 17:25
  • 1
    @Dac0d3r sure, that only requires to export the whole context, check the edit and link to codesandbox. – Tomasz Mularczyk Apr 07 '19 at 18:07
  • Thank you so much. Looks great, and just what I needed!! This deserves imo. to be the accepted answer. :-) – Dac0d3r Apr 07 '19 at 18:16
  • Tomasz I did create a related question here: https://stackoverflow.com/questions/55541089/how-to-wrap-react-intl-context-and-extend-with-switch-locale-functionality so either I might remove it, or you could simply link to your answer here and I'll accept it - up to you :-) – Dac0d3r Apr 07 '19 at 18:22
  • @Dac0d3r I'm glad it helps! – Tomasz Mularczyk Apr 08 '19 at 06:20
  • Beaware. Found that in react-intl@3.x addLocaleData was removed https://github.com/formatjs/react-intl/blob/master/docs/Upgrade-Guide.md#breaking-api-changes – woto Aug 25 '19 at 22:24