Internationalization (i18n) (DRAFT)
Note: Spartacus 1.x is no longer maintained. Please upgrade to the latest version.
The storefront is driven by different content streams. Most of the content is driven by either the CMS or by the product content. There’s however a portion of content that is provided by so-called site labels (i.e. texts in buttons or form placeholders).
Site labels are stored in separated files and can be translated into various languages.
Dependencies
Under the hood Spartacus uses i18next
library for translation mechanism and i18next-http-backend
for lazy loading of translation chunks. Those libraries have rich API, but we support only parts of it and treat as an implementation detail. In other words, Spartacus doesn’t support a custom usage of i18next
in your application.
Getting started
For a quick start, just import predefined Spartacus translations (currently only in English) from @spartacus/assets
and register them in the config of the B2cStorefrontModule
:
import { translations, translationChunksConfig } from '@spartacus/assets';
// ...
imports: [
B2cStorefrontModule.withConfig({
i18n: {
resources: translations,
chunks: translationChunksConfig
}
})
];
How to add translations for other languages
You can also provide your own English translations and add for other languages, for 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 will compile the translations in to your application JS bundle. While it’s a good way for a quick start, in production you would like to take advantage from lazy loading of translation chunks.
How to overwrite individual translations
To overwrite individual translations, the object with overwrites needs to be provided after the default translations. For example:
// app.module
import { translations } from '@spartacus/assets';
// ...
export const translationOverwrites = {
en: { // lang
cart: { // chunk
cartDetails: { // keys (nested)
proceedToCheckout: "Let's checkout!",
},
},
},
};
// ...
imports: [
B2cStorefrontModule.withConfig({
i18n: { resources: translations }
}),
ConfigModule.withConfig({
i18n: { resources: translationOverwrites }
})
]
Fallback language
In case of translation missing for a particular key, storefront in production mode displays non-breaking space character. To make it easier to catch those missing keys in development mode you will see the translation key preceded with the chunk’s name and colon (eg. [common:form.confirm]
).
To provide better UX in case of missing translation you can specify a fallback language.
Setting fallbackLang
option ensures that for every missing translation equivalent from fallback language will be used.
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
It makes a lot of sense to load translation resources only for the current language and only for the current page. That is why translations are structured by language and named chunks:
Translation resources have a structure:
interface TranslationResources {
[lang: string]: {
[chunkName: string]: {
[key: string]: any; // value or nested object with keys
};
};
}
To take advantage from lazy loading, you need to serve different JSON files, each for a specific language and chunk, and configure the URL to them using placeholders: {{lng}}
for language and {{ns}}
for chunk. For example:
imports: [
B2cStorefrontModule.withConfig({
i18n: {
backend: {
loadPath: 'assets/i18n-assets/{{lng}}/{{ns}}.json'
// crossOrigin: true, - use this option when i18n assets come from a different domain
},
chunks: translationChunksConfig
}
})
];
You can find the predefined Spartacus’ JSON files with translations in the folder /i18n-assets
of @spartacus/storefront
. But you need to serve them: either from your custom endpoint, or simply by copying them into /assets
folder of your Angular application. For example:
$ cp ./node_modules/@spartacus/assets/i18n-assets ./src/assets -r
Note: ./src/assets
is a standard path for apps created by Angular CLI. Your path to assets may be different.
When to create a new chunk
One chunk should group texts used for one functionality in the user journey (i.e. product list/details, cart, checkout or my account section). When a new functionality is added, one should consider whether to add it’s texts into existing translation chunk or to create a new chunk for it. It’s all about optimization. Your decision will be subjective and related to your CMS components structure. Too fine-grained chunks may result in loading many JSONs only to display one page. On the other hand, too coarse chunks with not related parts won’t be effective.
Things to consider when adding the feature with translation keys:
- will it appear very often (i.e. header) or only in some specific pages (i.e. checkout)
- will it appear only for signed in / privileged users (i.e. my account section) or for all users, etc.
When a feature appears only under some conditions, it’s a sign it may be worth to have a separate chunk for it.
Note: for smooth upgrade path, please avoid adding custom keys to existing chunks of Spartacus. See section Upgrading.
Using translations in HTML
You can use the cxTranslate
pipe, for example:
<input placeholder="{{ 'searchBox.searchHere' | cxTranslate }}" />
Config of chunks and namespaces mapping
Every key belongs to some named space (namespace). Each namespace is encapsulated in a chunk (i18n.chunks
below). But in order to resolve which key is in which namespace and chunk, a config is needed:
imports: [
B2cStorefrontModule.withConfig({
i18n: {
backend: {
loadPath: 'assets/i18n-assets/{{lng}}/{{ns}}.json'
},
chunks: {
...
common: ['searchBox', 'sorting', ...],
cart: ['cartDetails', 'cartItems', ...]
product: ['productDetails', 'productList', ...]
...
}
}
})
];
// common.json
{
...
"searchBox": {
"searchHere": "Search here..."
},
"sorting": {
"date": "Date",
"orderNumber": "Order Number"
},
...
}
// cart.json
{
"cartItems": {
...
"itemPrice": "Item price",
...
},
}
Note: if the key namespace and chunk’s name are the same, no explicit chunk config is needed for it. So there is no need for:
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 parameters
There are two special parameters of translation pipe used for pluralization and conditional translations: count
and context
.
count parameter
You can pass the count
parameter to differ translations for the same key depending on the integer value of count
. It takes the current language into consideration. Different languages have different plural rules. Languages as simple as English have only 2 forms: singular and plural, but other languages may have more. For example:
<p>{{ 'miniCart.item' | cxTranslate : { count: cart.totalItem } }}</p>
Translation resources:
// cart.en.json (ENGLISH)
{
"miniCart": {
"item": "{{count}} item currently in your cart",
"item_plural": "{{count}} items currently in your cart"
}
}
But for languages with more than 2 forms, numeric suffixes are used _0
, _1
, … _5
. For example in Polish there are 3 rules:
// cart.pl.json (POLISH)
{
"miniCart": {
"item": "Masz {{ count }} przedmiot w koszyku",
"item_2": "Masz {{ count }} przedmioty w koszyku",
"item_5": "Masz {{ count }} przedmiotów w koszyku",
}
}
Here is a small tool which helps figuring out the correct plural suffixes for different languages.
More about the special count
parameter in the official i18next
docs: https://www.i18next.com/translation-function/plurals
context parameter
You can pass the special parameter context
to differ translations for the same key depending on the string value of context
. It’s useful to translate enum values from backend. For example:
<p>{{ 'order.status' | cxTranslate : { context: order.status } }}</p>
Translation resources:
{
"order": {
"status_processing": "In process...",
"status_completed": "Completed",
"status_cancelled": "Cancelled",
}
}
More about the special context
parameter in the official i18next docs: https://www.i18next.com/translation-function/plurals
Other special parameters (unsupported)
There are few special parameters which interfere with the i18next
translation mechanism, but Spartacus doesn’t support them and we recommend to avoid using them:
defaultValue
, replace
, lng
, lngs
, fallbackLng
, ns
, keySeparator
, nsSeparator
, returnObjects
, joinArrays
, postProcess
, interpolation
, skipInterpolation
That being said, here are official i18next
docs for those unsupported parameters: https://www.i18next.com/translation-function/essentials#overview-options
Using translations in TS code
If you need to make use of translations before the template inject the translation service as follows:
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],
};
})
);
}
It is observable so please remember to add async
pipe in the template:
<cx-card
[content]="getPaymentCardContent(order.paymentInfo) | async"
></cx-card>
Upgrading
When Spartacus comes out with new features, it publishes new JSON files with predefined translations. Then any differences need to be taken into account in the JSON files translated into other languages.
For easier upgrades, we recommend NOT to add any custom keys in the namespaces of Spartacus, but rather to create custom chunks and namespaces for your custom features. Otherwise on every new version of Spartacus shipping 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 want to add new translations, there is the risk that in future Spartacus Storefront or other library from Spartacus’ ecosystem will come out with similar feature with the same translation key.
To avoid conflicts of keys, we recommend prefixing your feature keys with your app/library abbreviation. For example a library called Super Library
that has custom feature
, can have key:
<p>{{ 'slCustomFeature.subKey' | cxTranslate }}</p>
{
"slCustomFeature": {
"subKey": "value",
}
}
Localizing and formatting dates
You can format a date for the active language using the special pipe in the HTML template, for example:
{{ order.created | cxDate }}
The cxDate
pipe is just a wrapper on the Angular’s date
pipe, so it accepts the same arguments (format and timezone). For example:
{{ order.created | cxDate: 'longDate' }}
Note: cxDate
pipe uses the Angular’s locale data - which comes only with English by default. For other locales, you need to register them explicitly in your app.module. For example:
// app.module
import localeDe from '@angular/common/locales/de';
import localeJa from '@angular/common/locales/ja';
registerLocaleData(localeDe);
registerLocaleData(localeJa);
Note: If locale is not registered for the active language, the cxDate
pipe will fallback to English.