Skip to content

v20.0.0

Compare
Choose a tag to compare
@edx-semantic-release edx-semantic-release released this 17 Jun 11:11
· 1179 commits to master since this release

20.0.0 (2022-06-17)

  • feat!: implement i18n in Paragon components and in docs site (#1100) (53e0ac6), closes #1100

BREAKING CHANGES

  • By adding i18n support to the Paragon design system, we are introducing a peer dependency on [email protected] or greater. This may be a breaking change for some consumers, if your repository:
    • Uses v1 of @edx/frontend-platform.
    • Uses older version of react-intl than v5.25.0 directly.
    • Does not use react-intl.

For Consumers

Since Paragon now relies on react-intl, your whole application needs to be wrapped in its context, e.g.:

import { IntlProvider } from 'react-intl';
import { messages as paragonMessages } from '@edx/paragon';

ReactDOM.render(
  <IntlProvider locale={usersLocale} messages={paragonMessages[usersLocale]}>
    <App />
  </IntlProvider>,
  document.getElementById('root')
);

Note that if you are using @edx/frontend-platform's AppProvider component you don't need a separate context; you would only need to add Paragon's messages during application initialization like this:

import { APP_READY, subscribe, initialize } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import { messages as paragonMessages } from '@edx/paragon';
import App from './App';
// this is your app's i18n messages
import appMessages from './i18n';

subscribe(APP_READY, () => {
  ReactDOM.render(
    <AppProvider>
      <App />
    </AppProvider>,
    document.getElementById('root')
  );
});

initialize({
  // this will add your app's messages as well as Paragon's messages to your app
  messages: [
    appMessages,
    paragonMessages,
  ],
  // any other configurations for you app...
});

Related, any tests that render Paragon components with translations will similarly need to be wrapped with an IntlProvider component so the Paragon components can read from the intl React context. For example, if you're testing MyComponent which renders an Alert component from Paragon, MyComponent must be nested under an IntlProvider with a valid locale.

const MyComponentWrapper = () => {
  return (
    <IntlProvider locale="en">
      <MyComponent />
    </IntlProvider>
  );
}

For Contributors to Paragon

When developing a new component you should generally follow three rules:

  1. The component should not have any hardcoded strings as it would be impossible for consumers to translate it

  2. Internationalize all default values of props that expect strings, i.e.

    • For places where you need to display a string, and it's okay if it is a React element use FormattedMessage, e.g. (see Alert component for a full example)

      import { FormattedMessage } from 'react-intl';
      
      <FormattedMessage 
        id="pgn.Alert.closeLabel"
        defaultMessage="Dismiss"
        description="Label of a close button on Alert component"
      />
    • For places where the display string has to be a plain JavaScript string use formatMessage, this would require access to intl object from react-intl, e.g.

      • For class components use injectIntl HOC

        import { injectIntl } from 'react-intl';
        
        class MyClassComponent extends React.Component {
          render() {
            const { altText, intl } = this.props;
            const intlAltText = altText || intl.formatMessage({
              id: 'pgn.MyComponent.altText',
              defaultMessage: 'Close',
              description: 'Close label for Toast component',
            });
            
            return (
              <IconButton
                alt={intlCloseLabel}
                onClick={() => {}}
                variant="primary"
              />
            )
          }
        }
        
        export default injectIntl(MyClassComponent);
      • For functional components use useIntl hook

        import { useIntl } from 'react-intl';
        
        const MyFunctionComponent = ({ altText }) => {
          const intls = useIntl();
          const intlAltText = altText || intl.formatMessage({
            id: 'pgn.MyComponent.altText',
            defaultMessage: 'Close',
            description: 'Close label for Toast component',
          });
        
          return (
            <IconButton
              alt={intlCloseLabel}
              onClick={() => {}}
              variant="primary"
            />
          )
        
        export default MyFunctionComponent;

    Notes on the format above:

    • id is required and must be a dot-separated string of the format pgn.<componentName>.<subcomponentName>.<propName>
    • The defaultMessage is required, and should be the English display string.
    • The description is optional, but highly recommended, this text gives context to translators about the string.
  3. If your component expects a string as a prop, allow the prop to also be an element since consumers may want to also pass instance of their own translated string, for example you might define a string prop like this:

    MyComponent.PropTypes = {
      myProp: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
    };