Broadleaf Microservices
  • v1.0.0-latest-prod

Component Localization

Updating the Allowed Locales and Currencies

The locales and currencies allowed for the commerce app should be defined on the TenantServices' Application domain via the Admin. These drive the values in the locale and currency selector components in the header. To change the icons associated with the locales (e.g., 🇺🇸) or to add symbols for currencies, modify those components (locale-selector.tsx, currency-selector.tsx).

Internationalizing Messages

It is important that we internationalize our static text within our app. Static text is text that is a hard-coded part of the bundle and is not provided by one of the various services. Backend-driven text (e.g, product name and description) can be assumed to be always internationalized. However, text for the "Sign In" menu item, add-to-cart button text, footer copyright, etc., must be properly internationalized in the frontend application.

The following example is going to be the starting point for the next few steps. We are going to take this example from being static hard-coded text to being internationalized with the proper patterns.

Component to be defined in app/layout/components/sign-in.tsx
const SignIn = () => (
  <a href="/sign-in">
    Sign In
  </a>
)

1. Create a MessageDescriptor

The first step in internationalizing your text is to create a MessageDescriptor. A MessageDescriptor is comprised of an id, which is used as the key for translations of this message, and the defaultMessage, which is used if no message is found for the id. You can optionally provide a description that has no effect functionally but can be useful when debugging messages.

Example MessageDescriptor
const signInLabel = {
  // this is the ID that is used as a target for translations in different locales
  id: 'layout.sign-in.label',
  // it is a important to always give a defaultMessage
  defaultMessage: 'Sign In',
  // optionally provide a description as helpful hint or documentation of the descriptor
  description: 'This is used for representing the text within the sign-in button.'
}
Important
Additionally, we should add MessageDescriptors for a component into the parent module’s messages.ts file to reduce the clutter, e.g., since this is a header component we add the MessageDescriptor in app/layout/messages.ts.

Now, in order for react-intl to find these descriptors we’ll need to import defineMessages from react-intl and wrap out descriptors within it. This will make it so that our build process can properly generate the default messages json (messages/index.json and <default_lang>.json), while also providing a better encapsulation of our messages.

Final contents of app/layout/messages.ts
import { defineMessages } from 'react-intl';

export default defineMessages({
  signInLabel: {
    // this is the ID that is used as a target for translations in different locales
    id: 'layout.sign-in.label',
    // it is a important to always give a defaultMessage
    defaultMessage: 'Sign In',
    // optionally provide a description as helpful hint or documentation of the descriptor
    description: 'This is used for representing the text within the sign-in button.'
  }
});

2. Referencing the descriptor in our component

The next step is using the MessageDescriptors inside our component.

We’ve set up useFormatMessage hook to let us import react-intl’s formatMessage API as a hook.

Contents of app/layout/components/sign-in.tsx
import { useFormatMessage } from 'common/hooks';

import messages from '../messages';

const SignIn = () => {
  const formatMessage = useFormatMessage();
  return (
    <a href="/sign-in">
      {formatMessage(messages.label)}
    </a>
  );
};

3. Adding Translations Besides the Default Message

Now we are mostly finished! The next step is to go ahead and add translations for our text into different languages.

This is the most time-consuming part because it involves providing translations for all of the existing messages that you care about. If you do not include a translation for a given message ID, then it will fallback on the default English translation.

Let’s take an earlier example of the Nav.Login.label message for our login button, and let’s add a Spanish translation for it. All of the translations are located under src/messages/<locale>.json, so we would open up or create src/messages/es.json and add the following:

{
  "layout.sign-in.label": "Inicia Sesión"
}

Now when we are using a locale with the Spanish language tag, we will see our message replaced with the translated value.

Caution
If there is not already a json file for your locale, you will need to be sure to add one. Keep in mind that the locale is expected to be in IETF format, so if you wanted to define messages for es-MX, the name of the file would be es-MX.json.

We’ve added some automation of this process. After defining a new MessageDescriptor, run yarn build:langs. This will create the default en.json with the default messages and additional <lang>.json files as defined in scripts/build-langs.js, so es.json and fr.json by default. In the additional files, all of the keys will be present but mapped to an empty string.

{
  "layout.sign-in.label": ""
}
Note
To do the actual translating, you could copy the JSON and convert it into a spreadsheet or paste into one and parse it into key-value pairs. Then, use a Google Translate plugin to translate them.

Locale Code Format (IETF is the way to go!)

You might notice in the frontend that we are always dealing with locales formatted like en-US (i.e. with a dash, not an underscore). However, a lot of us come from a Java background where the locale code is generally en_US, so that might seem wrong at first.

The gist of it is this: the IETF standard is to use a dash (https://tools.ietf.org/rfc/bcp/bcp47.txt), and that is the standard and semantically correct way to format a locale code. However, some time ago POSIX systems and Java decided, for I assume a good reason at the time, to use underscores instead.

In our React app, we will be using the IETF standard, as that is what most libraries and the browser expects. This is the format that we will be sending in the X-Locale-Override header and is also the format the browser already automatically sends in the Accept-Language header.

If the React app receives a locale code from the catalog or site (via their default/allowed locales), it will auto-format them from the Java version to the browser version if in that format.