Broadleaf Microservices

Admin Internationalization

Overview

The Unified Admin has built-in support for internationalization (i18n) of displayed text. Text can be supplied from:

+ * data fetched from APIs such as a Product’s name (see API Data) * messages specified in metadata such as a form field label or submit button’s text (see Metadata Text) * text controlled by the Admin App itself such as for interstitial pages, alerts, and other special components not driven by APIs or metadata.

API Data

Important
These translations are for display on the commerce-facing app rather than for the admin user’s benefit. To manage admin text translations like for admin menus and form field labels, see Setting the App&.html.

Data retrieved from APIs such as Product or Category data should supply their own translations based on the request’s Accept-Language header. This header’s value is derived from the selected locale on the entity view.

Locale Selector

The values for this selector are driven by the resolved Application’s or Tenant’s allowedLocales:

Application Allowed Locales field
Tip
See Admin Translation Features for more details on managing an entity’s translations.

Metadata Text

Things like form field labels, submit button text, hints, and tooltips are defined using metadata. This also includes translations for them. These will show translated text using a different locale-selector than with API Data since they aren’t related to customer-facing data.

Tip
See Setting the App&.html for details on that selector.

Defining Translations

The Metadata service will receive a request from the Admin that includes the desired locale and will return already-translated metadata if any translations are present. Translations should be defined in message properties files in your MicroservicesDemo project under /services/metadata/src/main/resources/messages. This take advantage of Spring’s localization support.

For example, create a product.properties for the default translations and a product_es.properties for Spanish translations:

Example product.properties
product.fields.name=Name
Example product_es.properties
product.fields.name=Nombre

Then, in your metadata, refer to the properties by key instead of using static text:

Fields.string()
    .name(ProductProps.NAME)
    .label("product.fields.name") // <-- Spring will replace this with the localized text
    .order(1000);

To make sure Spring knows about your properties files, make sure to register a MetadataMessagesBasename bean that specifies the file paths in META-INF/spring.factories.

Example MetadataMessages class
package com.broadleafdemo.metadata.i18n;

import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.NonNull;

import com.broadleafcommerce.metadata.i18n.MetadataMessagesBasename;

import java.util.Arrays;

public class DemoMetadataMessages implements MetadataMessagesBasename {

    @Override
    @NonNull
    public String getBasename() {
        return StringUtils.join(Arrays.asList(
                "messages/demo-catalog",
                "messages/demo-pricing",
                "messages/demo-offer"),
                ",");
    }

}
Example spring.factories
com.broadleafcommerce.metadata.i18n.MetadataMessagesBasename=\
  com.broadleafdemo.metadata.i18n.DemoMetadataMessages

Static App Text

Static text is text that is a hard-coded part of the code bundle and is not provided by one of the various services or metadata. This includes text for the "Logout" header-menu item and the sandbox ribbon status. Translating this text is supported using the react-intl library from FormatJS. This is an abstraction layer on top of the browser’s built in JS Intl APIs.

Setting the App’s Locale

Like with Metadata Text, the locale used to determine which translations for static text are shown is derived from the top-level selector in the header:

Admin Translation Selector

The values of this selector are determined by the following runtime property:

VITE_ALLOWED_LOCALES=en,en-US,es-MX,es-ES,fr,fr-CA

Localizing Static Text

Static text is made translatable by defining a react-intl message descriptor. This contains an id for the message, a default translation, and, optionally, a description. The id is used to identify which message is which, allowing us to override text specifically for a given locale.

Important
This should be used when defining new, custom text.
Example message descriptor definition
import { defineMessages } from 'react-intl';

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

As a convenience, we’ve also included a script in the Admin Starter (build-langs.js) that can be run that compiles all of these message descriptors and produces a JSON file that can be sent off for translation of the default messages. Just run yarn build:langs.

Important
Make sure your message descriptors are defined in files with the following naming convention: *.messages.js|ts.

This will output an en.json and index.json file in /messages.

Tip
To add new translations, just define files in /messages with a name like <locale>.json: fr.json, es.json, de.json. The script will see these and add them to the index.json.
Example en.json, auto-generated
{
  "LogoutButton.logout": "Logout"
}
Example es.json, result of a translator service
{
  "LogoutButton.logout": "Cerrar Sesión"
}
Example index.json, auto-generated
{
 "en": {
    "LogoutButton.logout": "Logout"
  },
 "es": {
    "LogoutButton.logout": "Cerrar Sesión"
  }
}

This can be merged with the bundled messages from the Broadleaf components library to override the defaults and contribute new translations. Just import the index.json file into the App.jsx:

import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import { AdminApp, AdminProvider, utils } from '@broadleaf/admin-components';

import baseMessages from '@broadleaf/admin-components/messages';
import customMessages from 'messages';

const messages = {...baseMessages, ...customMessages };

const browserSupportsHistory = 'pushState' in window.history;
const App = () => (
  <Router
    basename={utils.Environment.get('public.url')}
    forceRefresh={!browserSupportsHistory}
  >
    <AdminProvider messages={messages}>
      <AdminApp />
    </AdminProvider>
  </Router>
);

export default App;
Tip
build-langs.js is included in the Admin Starter, meaning you can customize it anyway you like: The code is yours.

Using Localized Text

Now that we have our message descriptor, we need to actually use that message descriptor. This step has a couple of options that depend on the component you are working with.

Using the FormattedMessage Component

The first option is to use the FormattedMessage component provided by react-intl:

import { FormattedMessage } from 'react-intl';

const logoutMessageDescriptor = {
  // this is the ID that is used as a target for translations in different locales
  id: 'LogoutButton.logout',
  // it is a important to always give a defaultMessage
  defaultMessage: 'Logout',
  // optionally provide a description as helpful hint or documentation of the descriptor
  description: 'This is used for representing the text within the logout button.'
};

const LogoutButton = ({ handleLogout }) => {
  return (
    <button onClick={handleLogout} type="button">
      <FormattedMessage {...logoutMessageDescriptor}/>
    </button>
  );
}

Using the useIntl hook

The second option is to use the useIntl hook:

import { useIntl } from 'react-intl';

const logoutMessageDescriptor = {
  // this is the ID that is used as a target for translations in different locales
  id: 'LogoutButton.logout',
  // it is a important to always give a defaultMessage
  defaultMessage: 'Logout',
  // optionally provide a description as helpful hint or documentation of the descriptor
  description: 'This is used for representing the text within the logout button.'
};

const LogoutButton = ({ handleLogout }) => {
  const formatMessage = useIntl().formatMessage;
  return (
    <button onClick={handleLogout} type="button">
      {formatMessage(logoutMessageDescriptor)}
    </button>
  );
}