####################
React App i18n HOWTO
####################

************
Introduction
************

This is a step by step guide to making your React app ready to accept translations. The instructions here are very specific to the edX setup.

.. contents:: Table of Contents


*************************************************
Internationalize your application with react-intl
*************************************************

These steps will allow your application to accept translation strings. See `frontend-app-account <https://github.com/openedx/frontend-app-account/>`_ for an example app to follow.

#. Add ``@edx/frontend-platform`` as a dependency to your ``package.json`` . (If you are actually writing a consumable component, add ``@edx/frontend-platform`` as both a dev dependency and peer dependency instead.)  ``@edx/frontend-platform/i18n`` is a wrapper around ``react-intl`` that adds some shims. You should only access the ``react-intl`` functions and elements exposed by ``@edx/frontend-platform/i18n``. (They have the same names as in ``react-intl``.)

#. In ``App.js``, wrap your entire app in an ``IntlProvider`` element. See `Load up your translation files`_ for details. (Consumable components: Don't do this step, except possibly in tests. Your consuming application will do it for you. Instead, update your `README like this example <https://github.com/openedx/frontend-component-footer/blame/master/README.rst#L23-L27>`__.)

#. For places in your code where you need a display string, and it's okay if it's a React element (generally, most messages): use a ``FormattedMessage``.

   * The ``id`` is required and must be a unique dot-separated string. The first part of it should be your app name. The rest can be whatever you want, though a hierarchical namespacing part followed by some descriptive words is most common.

   * The ``defaultMessage`` is required, and should be the English display string. Otherwise translators won't know what they're translating.

     .. note::
        English strings should not include the HTML brackets < or > since those characters will break Transifex.

   * The ``description`` is a note to the translators that can help them figure out how to translate your text. It is optional, but recommended.

     Example::

       <FormattedMessage
        id="myapp.cart.shipping.address"
        defaultMessage="Shipping address"
        description="header above the shipping address form"
       />

     For additional help, including adding interprolated variables, see the `FormattedMessage documentation <https://formatjs.io/docs/react-intl/components#formattedmessage>`__. It can also handle plurals.

#. For places in your code where you need a display string, and it has to be a plain JavaScript string (e.g., a button label), you will need to do the following:

   #. Inject the ``intl`` object into your component:

      #. ``import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';``;

      #. add ``intl: intlShape.isRequired`` to your component's ``propTypes``.

      #. instead of ``export Foo``, ``export injectIntl(Foo)`` .

   #. Define your messages using ``defineMessages``. This function doesn't actually do anything; it's just a hook for the translation pipeline to be able to find your translation strings. You can call ``defineMessages`` wherever you want, but if you have a lot of them you might want to move them to a separate file. Either ``MyAppName.messages.js`` (if your entire app has only a few strings) or ``SomeComponent.messages.js`` will work. Your file should look like the example below. For your own sanity, using a short camel-case string for the property name is fine as long as ``id`` is globally unique in the MFE. Example::

       import { defineMessages } from '@edx/frontend-platform/i18n';

       const messages = defineMessages({
         'cartPayNow': {
           id: 'myapp.cart.pay.now',
           defaultMessage: 'Pay Now',
           description: 'a button label',
         },
       });

       export default messages;

   #. Use the ``intl.formatMessage`` function to get your translated string::

       import messages from './SomeComponent.messages';
       // ...
       intl.formatMessage(messages.cartPayNow)

#. If you want to use ``FormattedMessage`` but your display string is repeated several times, it's probably better to pull it out into a messages file. In this case the messages file will have the ``defaultMessage`` and the ``description``, and you can just give ``FormattedMessage`` the ``id``.

#. You should now be able to run your app and see everything behaving normally, with English strings.

*************************************************
Add your application to the translations workflow
*************************************************

Follow the `instructions for including React repositories in the translations workflow <https://docs.openedx.org/en/latest/developers/how-tos/enable-translations-new-repo.html#react-repos>`_ on `docs.openedx.org <https://docs.openedx.org/>`_.

******************************
Load up your translation files
******************************

.. note:: This step is for applications only. You can skip this for consumable components.

   You can actually do this step even before you have Transifex and Jenkins set up, by providing your own translation files in ``src/i18n/messages/LANG_CODE.json``.

#. Your pipeline job should have updated several translation files in ``src/i18n/messages/LANG_CODE.json`` .

#. Create ``src/i18n/index.js`` using `frontend-app-account's index.js <https://github.com/openedx/frontend-app-account/blob/master/src/i18n/index.js>`_ as a model.

#. In ``App.jsx``, make the following changes::

     import { IntlProvider, getMessages, configure } from '@edx/frontend-platform/i18n';
     import messages from './i18n/index'; // A map of all messages by locale

      configure({
        messages,
        config: getConfig(), // ENVIRONMENT and LANGUAGE_PREFERENCE_COOKIE_NAME are required
        loggingService: getLoggingService(), // An object with logError and logInfo methods
      });

      // ...inside ReactDOM.render...
      <IntlProvider locale={this.props.locale} messages={}>

#. As of this writing, ``frontend-platform/i18n`` reads the locale from the user language preference cookie, or, if none is found, from the browser's language setting. You can verify everything is working by changing your language preference in your account settings. If you are not logged in, you can change your browser language to one of the languages you have translations for.


********************
Migrating to react-intl@5
********************

Initially ``frontend-platform`` used ``react-intl@2`` but as a part of its ``2.0.0`` release the version has been upgraded to ``react-intl@5``. If your application
used ``frontend-platform`` < ``2.0.0`` and you decided to upgrade, here's a list of breaking changes that you will need to consider during the upgrade:

* ``FormattedRelative`` component has been renamed to ``FormattedRelativeTime`` (and ``formatRelative`` to ``formatRelativeTime``) with changes to its API, read more `here <https://formatjs.io/docs/react-intl/upgrade-guide-3x/#formattedrelativetime>`_

* Message Format syntax has been changed. See `Message Format Syntax Changes <https://formatjs.io/docs/react-intl/upgrade-guide-3x#message-format-syntax-changes>`_ for more details.

* All tags specified must have corresponding values and will throw error if it's missing, e.g::

    new IntlMessageFormat('a<b>strong</b>').format({
      b: (...chunks) => <strong>{chunks}</strong>,
    })

* Formatting self-closing tags is not supported anymore.

* XML/HTML tags are escaped using apostrophe just like other ICU constructs.

* Rich text formatting callback function is no longer variadic

  * Before::

      new IntlMessageFormat('a<b>strong</b>').format({
        b: (...chunks) => <strong>{chunks}</strong>,
      })

  * After::

      new IntlMessageFormat('a<b>strong</b>').format({
        b: chunks => <strong>{chunks}</strong>,
      })

* ``FormattedMessage`` render prop is no longer variadic

  * Before::

      <FormattedMessage defaultMessage="a<b>strong</b>">
        {(...chunks) => <b>{chunks}</b>}
      </FormattedMessage>

  * After::

      <FormattedMessage defaultMessage="a<b>strong</b>">
        {chunks => <b>{chunks}</b>}
      </FormattedMessage>

* ``IntlProvider.getChildContext`` has been removed which was used for testing purposes. Use ``createIntl`` instead to create a standalone ``intl`` object outside of React. See `Testing with React Intl <https://formatjs.io/docs/guides/testing>`_ for more details.

********************
For more information
********************

* `ADR for react-intl <https://github.com/openedx/paragon/blob/master/docs/decisions/0002-react-i18n.rst>`_

* `Working i18n notes <https://openedx.atlassian.net/wiki/spaces/LOC/pages/946503833/i18n+for+React>`_

* `studio-frontend i18n guide <https://github.com/openedx/studio-frontend/tree/master/src/data/i18n>`_
