Internationalization (i18n)

In a typical Spartacus storefront, most of the content comes either from the CMS, or from the product content. However, for the storefront site labels (such as texts in buttons), the content is stored in separate files, and these can be localized (that is, translated).


Table of Contents


Dependencies

Spartacus uses the i18next library for its translation mechanism, and uses i18next-xhr-backend for lazy loading of translation chunks. Both of these libraries have rich APIs, but Spartacus supports only parts of them, and treats them as implementation details. As a result, Spartacus does not support custom usage of i18next in your application.

Getting Started

For a quick start, import the predefined Spartacus translations (currently only in English) from @spartacus/assets, and register them in the config of the B2cStorefrontModule. The following is an example:

import { translations, translationChunksConfig } from '@spartacus/assets';

// ...

imports: [
  B2cStorefrontModule.withConfig({
    i18n: {
        resources: translations,
        chunks: translationChunksConfig
    }
  })
];

Adding Translations for Other Languages

Instead of using the predefined Spartacus translations, you can provide your own English translations, and add translations for other languages as well. The following is an example:

import { translations, translationChunksConfig } from '@spartacus/assets';

// ...

imports: [
  B2cStorefrontModule.withConfig({
    i18n: {
        resources: {
            en: translations, // or YOUR_ENGLISH_TRANSLATIONS,
            de: YOUR_GERMAN_TRANSLATIONS,
            ...
        },
        chunks: translationChunksConfig
    }
  })
];

This compiles the translations into your application JS bundle. Although this is fine for a quick start, in production you would want to take advantage of lazy loading for translation chunks.

Overwriting Individual Translations

To overwrite individual translations, the object with overwrites needs to be provided after the default translations. The following is an example:

// app.module

import { translations } from '@spartacus/assets';

// ...

export const translationOverwrites = {
  en: { // lang
    cart: { // chunk
      cartDetails: { // keys (nested)
        proceedToCheckout: 'Proceed to Checkout',
      },
    },
  },
};

// ...

imports: [
    B2cStorefrontModule.withConfig({
        i18n: { resources: translations }
    }),
    ConfigModule.withConfig({
        i18n: { resources: translationOverwrites }
    })
]

Note: Because of the underlying mechanisms that the Spartacus configuration relies on, i18n translation overwrites have to be configured in the same module as the translation that is overwritten. This is how the above example is structured. Configuring translation overwrites in a sub module, for example, will not work, even if that sub module is imported after B2cStorefrontModule.

Fallback Language

IF a translation is missing for a particular key, the storefront in production mode displays a non-breaking space character. To make it easier to catch missing keys, in development mode Spartacus displays the translation key preceded with the chunk’s name and colon (for example, [common:form.confirm]).

To provide better a better user experience if a translation is missing, you can specify a fallback language. Setting the fallbackLang option ensures that, for every missing translation, the equivalent from the fallback language is used instead.

The following is an example configuration with English as a fallback language:

import { translations, translationChunksConfig } from '@spartacus/assets';

// ...

imports: [
  B2cStorefrontModule.withConfig({
    i18n: {
        resources: translations,
        chunks: translationChunksConfig,
        fallbackLang: 'en',
    }
  })
];

Lazy Loading

Translations are structured by language and named chunks so that you can load translation resources for the current language and current page only. The following is an example of the structure for translation resources:

interface TranslationResources {
  [lang: string]: {
    [chunkName: string]: {
      [key: string]: any; // value or nested object with keys
    };
  };
}

To take advantage of lazy loading, you need to serve different JSON files for each specific language and chunk, and configure the URL to the JSON files using the {{lng}} placeholder for language and the {{ns}} placeholder for chunks. The following is an example:

imports: [
  B2cStorefrontModule.withConfig({
    i18n: {
        backend: {
            loadPath: 'assets/i18n-assets/{{lng}}/{{ns}}.json'
        },
        chunks: translationChunksConfig
    }
  })
];

For Spartacus, you can find the predefined JSON files with translations in the /i18n-assets folder of @spartacus/storefront. You need to serve these files, either from your own custom endpoint, or by copying them into the /assets folder of your Angular application. The following is an example:

cp ./node_modules/@spartacus/assets/i18n-assets ./src/assets -r

Note: The ./src/assets path shown in this example is a standard path for apps created by Angular CLI. Your path to assets may be different.

Deciding When to Create a New Chunk

One chunk should group texts used for a single functionality in the user journey (for example, the product list or product details, the cart, the checkout, the My Account section, and so on). When new functionality is added, you should consider whether to add the relevant texts into an existing translation chunk, or to create a new chunk for it. Your decision will be subjective, and related to your CMS components structure, but it all comes down to optimization. If your chunks are too fine-grained, it may result in loading many JSON files only to display a single page. On the other hand, if your chunks are too coarse chunks, with unrelated parts, they will not be effective.

When adding a feature with translation keys, consider the following:

  • Will it appear very often (for example, in the header), or only on some specific pages, such as the checkout?
  • Will it appear only for signed in or privileged users (such as the My Account section) or will it appear for all users?

When a feature appears only under certain conditions, then it may be worth having a separate chunk for it.

Note: To maintain a smooth upgrade path, avoid adding custom keys to existing chunks in Spartacus. For more information, see the Upgrading section, below.

Handling Translations in HTML

To handle translations in HTML, you can use the cxTranslate pipe. The following is an example:

<input placeholder="{{ 'searchBox.searchHere' | cxTranslate }}" />

Configuring Chunks and Namespace Mapping

Every key belongs to a namespace, and each namespace is encapsulated in a chunk (such as the i18n.chunks in the example below). A configuration is required to resolve which key belongs to which namespace and chunk. The following is an example:

imports: [
  B2cStorefrontModule.withConfig({
    i18n: {
        backend: {
            loadPath: 'assets/i18n-assets/{{lng}}/{{ns}}.json'
        },
        chunks: {
            ...
            common: ['searchBox', 'sorting', ...],
            cart: ['cartDetails', 'cartItems', ...]
            product: ['productDetails', 'productList', ...]
            ...
        }
    }
  })
];

The following is an example of the corresponding configuration in the common.ts file:

{
    ...
    searchBox: {
        placeholder: 'Search here...'
    },
    sorting: {
        date: 'Date',
        orderNumber: 'Order Number'
    },
    ...
}

The following is an example of the corresponding configuration in the cart.ts file:

{
    cartItems: {
        ...
        itemPrice: 'Item price',
        ...
    },
}

Note: If the key namespace and the chunk name are the same, no explicit chunk config is needed for it. So there is no need for something like the following example:

chunks: [
    myAddon: ['myAddon']
]

Parameters

You can also pass parameters into the translation pipe, for example:

<p>{{ 'miniLogin.hello' | cxTranslate : { name: person.name } }}</p>
// resources
{
    miniLogin: {
        hello: 'Hello, {{ name }}'
    }
}

Special Parameter: count

The count parameter is a special parameter of the translation pipe that is used for pluralization.

You can pass the count parameter to differ translations for the same key, depending on the integer value of count, and it takes the current language into consideration. Different languages have different plural rules. Some languages, such as English, have only two forms, singular and plural, but other languages may have more. The following is an example that shows how the pluralization rules and handled for different languages:

<p>{{ 'miniCart.item' | cxTranslate : { count: cart.totalItem } }}</p>

The following are the translation resources in the common.ts file for English:

{
    miniCart: {
        item: '{{count}} item currently in your cart',
        item_plural: '{{count}} items currently in your cart'
    }
}

For languages with more than two plural forms, numeric suffixes are used, such as _0, _1, … _5, and so on. The following are the translation resources in the cart.ts file for Polish, which has three rules for pluralization:

{
    miniCart: {
        item: 'Masz {{ count }} przedmiot w koszyku',
        item_2: 'Masz {{ count }} przedmioty w koszyku',
        item_5: 'Masz {{ count }} przedmiotów w koszyku',
    }
}

For more information about the special count parameter, see Plurals in the official i18next documentation.

For more information about different pluralization rules, see Localization and Plurals on the Mozilla developer site.

You can also use this small tool to help you figure out the correct plural suffixes for different languages: https://jsfiddle.net/jamuhl/3sL01fn0/#tabs=result

Special Parameter: context

The context parameter is a special parameter of the translation pipe that is used for conditional translations.

You can pass the special context parameter to differ translations for the same key depending on the string value of context. It is useful for translating enum values from the back end. The following is an example:

<p>{{ 'order.status' | cxTranslate : { context: order.status } }}</p>

The following are the relevant translation resources:

{
    order: {
        status_processing: 'In process...',
        status_completed: 'Completed',
        status_cancelled: 'Cancelled',
    }
}

For more information about the special context parameter, see Context in the official i18next documentation.

Unsupported Special Parameters

At the time of writing, there are a number of special parameters that interfere with the i18next translation mechanism, and Spartacus does not support them. These unsupported special parameters including the following: defaultValue, replace, lng, lngs, fallbackLng, ns, keySeparator, nsSeparator, returnObjects, joinArrays, postProcess, interpolation, skipInterpolation

There may be other unsupported special parameters that are not listed here. For the full list, see the official i18next documentation.

Using Translations in TypeScript Code

If you need to make use of translations before the template, inject the translation service, as shown in the follow example:

import { TranslationService } from '@spartacus/core';

constructor(
    private translation: TranslationService
) {}

getPaymentCardContent(payment: PaymentDetails): Observable<Card> {
   return combineLatest([
     this.translation.translate('paymentForm.payment'),
     this.translation.translate('paymentCard.expires', {
       month: payment.expiryMonth,
       year: payment.expiryYear,
     }),
   ]).pipe(
     map(([textTitle, textExpires]) => {
       return {
         title: textTitle,
         textBold: payment.accountHolderName,
         text: [payment.cardType.name, payment.cardNumber, textExpires],
       };
     })
   );
}

The translation is observable, so you must also add an async pipe to the template. The following is an example:

<cx-card
    [content]="getPaymentCardContent(order.paymentInfo) | async"
></cx-card>

Best Practices

In components, it is a best practice to use the cxTranslate pipe in the HTML template pipe, rather than the TranslationService in the TypeScript logic, unless there is a strong reason for using TranslationService.

The disadvantages of using TranslationService are the following:

  • it introduces an unnecessary constructor dependency to the component TypeScript class
  • it might introduce unnecessary methods and RxJs Observable logic to the component TypeScript class

Upgrading

When new features are released in Spartacus, new JSON files are published with predefined translations, and any differences need to be taken into account in the JSON files that have been translated into other languages.

For easier upgrades, we recommend to not add any custom keys in the Spartacus namespaces. A safer approach is to create custom chunks and namespaces for your custom features. Otherwise, with every new version of Spartacus that ships with new translations, you will have to add your custom keys to new Spartacus JSON files.

Extending Translations

When you add new, custom features and you want to add new translations, there is the risk that a future release of the Spartacus storefront library (or some other library from the Spartacus ecosystem) will include a similar feature with the same translation key. To avoid conflicts with keys, you can prefix your feature keys with your app or library abbreviation. For example, a library called Super Library that has a custom feature could have the following key:

<p>{{ 'slCustomFeature.subKey' | cxTranslate }}</p>
{
    slCustomFeature: {
        subKey: 'value',
    }
}

Localizing and Formatting Dates

You can format a date for the active language using a special pipe in the HTML template. The following is an example:

{{ order.created | cxDate }}

The cxDate pipe is just a wrapper of Angular’s date pipe, so it accepts the same arguments (that is, format and timezone). The following is an example:

{{ order.created | cxDate: 'longDate' }}

Note: The cxDate pipe uses Angular’s locale data, which only comes with English by default. For other locales, you need to register them explicitly in your app.module. The following is an example from the app.module:


import localeDe from '@angular/common/locales/de';
import localeJa from '@angular/common/locales/ja';

registerLocaleData(localeDe);
registerLocaleData(localeJa);

Note: If a locale is not registered for the active language, the cxDate pipe will fall back to English.

For more information about registering locales, see Format data based on locale in the official Angular documentation.

For more information about Angular’s date pipe, see DatePipe in the official Angular documentation.