<!-- React app below -->
<div id="app"></div>
<!-- React app above -->

<p>
  <br><small>Read my blog article <a href="https://dbushell.com/2018/04/05/react-redux-internationalisation/" target="_blank">React &amp; Redux - Internationalisation</a> for more information.</small>
  <br><small>Sorry for any Google translation errors!</small>
</p>
* {
  box-sizing: border-box;
}

body {
  background: #f2f0e6;
  color: #494b4d;
  padding: 1rem;
}

button {
  background: #fff;
  border: 2px solid #1d97bf;
  border-radius: 5px;
  color: #1d97bf;
  cursor: pointer;
  flex: 0 1 auto;
  font-family: Arial;
  font-size: 18px;
  font-weight: 700;
  height: 50px;
  line-height: 30px;
  padding: 8px 20px;
  transition: background 150ms, border 150ms, color 150ms;
  width: calc(33.333% - 10px);
  
  &[aria-pressed="true"] {
    background-color: #39809e;
    border-color: #39809e;
    color: #fff;
  }

  &:focus,
  &:hover {
    background-color: #1d97bf;
    border-color: #1d97bf;
    color: #fff;
  }
}

#app div {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  max-width: 600px;
  min-width: 500px;
}

p {
  font-size: 18px;
  flex: 1 1 auto;
  width: 100%;
  margin-bottom: 20px;

  a {
    border-bottom: 2px solid rgba(#1d97bf, 0.1);
    color: #1d97bf;
    text-decoration: none;
    transition: border 150ms, color 150ms;
    
    &:focus,
    &:hover {
      border-color: #fffbcc;
      color: #39809e;
    }
  }
}

small {
  font-size: 75%;
}
// using global modules for CodePen demo
// replace with import statements
const {connect, Provider} = ReactRedux;
const {createStore} = Redux;

// initial app state for demo includes i18n translations
// i18n could be loaded via JSON file, API call, or global object
const initialState = {
  lang: 'en',
  i18n: {
    en: {
      Menu: {
        desc: 'This app is translated into %1$d languages:',
        enButton: 'English',
        deButton: 'German',
        esButton: 'Spanish'
      }
    },
    de: {
      Menu: {
        desc: 'Diese App ist in %1$d Sprachen übersetzt:',
        enButton: 'Englisch',
        deButton: 'Deutsche',
        esButton: 'Spanisch'
      }
    },
    es: {
      Menu: {
        desc: 'Esta aplicación está traducida a %1$d idiomas:',
        enButton: 'Inglés',
        deButton: 'Alemán',
        esButton: 'Español'
      }
    }
  }
};

// stateless functional component
const Menu = props => {
  // create button props from lang code
  const buttonProps = lang => ({
    onClick: () => props.onLang(lang),
    'aria-pressed': props.lang === lang
  });
  return (
    <div>
      <p>{sprintf(props.i18n.desc, props.langCount)}</p>
      <button {...buttonProps('en')}>{props.i18n.enButton}</button>
      <button {...buttonProps('de')}>{props.i18n.deButton}</button>
      <button {...buttonProps('es')}>{props.i18n.esButton}</button>
    </div>
  );
};

// default props probably are not necessary...
Menu.defaultProps = {
  i18n: {
    desc: 'Description',
    enButton: 'Button 1',
    deButton: 'Button 2',
    esButton: 'Button 3'
  }
};

// connected container with translated component
const mapStateToProps = state => ({
  langCount: Object.keys(state.i18n).length
});
const mapDispatchToProps = dispatch => ({
  onLang: lang => dispatch({type: 'LANG', lang: lang})
});
// use higher-order component translate() instead of connect()
// to pass i18n props and connect to state
const MenuContainer = translate(
  'Menu',
  mapStateToProps,
  mapDispatchToProps
)(Menu);

// basic Redux reducer to update state language settings
const rootReducer = (state, action = {}) => {
  if (action.type === 'LANG') {
    return {...state, lang: action.lang};
  }
  return state;
};

// setup Redux store with reducer and state
const store = createStore(rootReducer, initialState);

// render app to DOM
ReactDOM.render(
  <Provider store={store}>
    <MenuContainer />
  </Provider>,
  document.getElementById('app')
);

// higher-order component to provide translation and state
function translate(name, mapStateToProps, mapDispatchToProps) {
  return WrappedComponent => {
    // create HOC and pass through props
    const TranslatedComponent = props => {
      // find translations or use defaultProps
      props.i18n = props.i18n.hasOwnProperty(props.lang)
        ? props.i18n[props.lang][name]
        : undefined;
      return <WrappedComponent {...props} />;
    };
    // set a name for debugging
    TranslatedComponent.displayName = `Translate(${WrappedComponent.displayName ||
      WrappedComponent.name ||
      'Component'})`;
    // return HOC connected to state
    return connect(
      state => ({
        ...(mapStateToProps ? mapStateToProps(state) : {}),
        lang: state.lang,
        i18n: state.i18n
      }),
      dispatch => ({
        ...(mapDispatchToProps ? mapDispatchToProps(dispatch) : {})
      })
    )(TranslatedComponent);
  };
}
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.com/react@16.2.0/umd/react.production.min.js
  2. https://unpkg.com/react-dom@16.2.0/umd/react-dom.production.min.js
  3. https://unpkg.com/react-redux@5.0.7/dist/react-redux.min.js
  4. https://unpkg.com/redux@3.7.2/dist/redux.min.js
  5. https://unpkg.com/sprintf-js@1.1.1/dist/sprintf.min.js