Skip to content

Add a language

A language in Evolve is a locale: a language plus a country, such as it-IT. The locale is in every URL, picks the message file, and goes to the backend with every request. So adding one touches four places.

What Where, in the reference sites
URLs and their prefixes packages/site-config/src/i18n/config.ts
The locales a store offers, its store key and currency frontend/site/src/lib/store-config/
The interface text frontend/site/src/messages/<locale>.json
Product data, prices, and CMS content your commerce engine and CMS
  1. src/proxy.ts runs the next-intl middleware, made from the routing in site-config. It reads the prefix (/be/fr) and rewrites the request to app/[locale]/… with the full locale (fr-BE).

  2. app/[locale]/layout.tsx checks the locale, loads its messages, and builds the store context:

    const storeConfig = await getStoreConfig();
    const locale = getLocaleFromParams(params.locale, storeConfig.defaultLocale);
    const messages = await getMessages({ locale });
    const storeContext = {
    storeKey: storeConfig.storeKey,
    currency: storeConfig.currency,
    locale,
    };
  3. The GraphQL fetchers send the store context as the X-StoreContext-StoreKey, X-StoreContext-Locale and X-StoreContext-Currency headers on every request. The backend uses them to answer in that language, currency, and store. See Authentication and context.

Because everything starts from routing, you add a locale there first.

This example adds Italian for Italy, it-IT.

  1. Add the locale to routing.

    Add it to locales, give it a URL prefix, and give it a path in each localized entry of pathnames:

    export const locales = [
    "nl-NL",
    "nl-BE",
    "fr-BE",
    "en-GB",
    "de-DE",
    "es-ES",
    "fr-FR",
    "it-IT",
    ] as const;
    export const localePrefixes: LocalePrefixes = {
    "de-DE": "/de",
    "en-GB": "/en",
    "es-ES": "/es",
    "fr-BE": "/be/fr",
    "fr-FR": "/fr",
    "it-IT": "/it",
    "nl-BE": "/be/nl",
    "nl-NL": "/nl",
    };
    export const pathnames = {
    "/cart": {
    "it-IT": "/carrello",
    // …the other locales
    },
    // …and the same for /search, /product/[slug], /category/[...slug],
    // and the /checkout paths in the B2B site
    };

    The middleware, the rewrites in next-rewrites.ts, the sitemaps, the CMS preview route, and evolve.config.ts all import from this file. The backend imports it too, for sitemap and email links. That is why it lives in a package of its own: one list, so nothing drifts.

  2. Offer it in your store.

    The store config decides which locales the language switch shows, and which store key and currency go to the backend. The site picks one at startup with the STORE_KEY environment variable.

    currency: "EUR",
    distributionChannelKey: "evolve-store",
    defaultLocale: "nl-NL",
    locales: ["nl-NL", "nl-BE", "fr-BE", "en-GB", "de-DE", "es-ES", "fr-FR", "it-IT"],
    storeKey: "evolve",

    A store has one currency and one store key. A market that needs another currency or another commerce store is a store config of its own: add a file next to example-nl.ts, add it to storeConfigs in index.ts, and extend the StoreKey and Currency types in context/types.ts.

  3. Add the market for translated CMS paths.

    With Storyblok, site-config also lists the languages of each country. The discover service passes staticSiteConfigs to the StoryblokModule as siteConfigs, which computes each page’s path in the other locales.

    import type { StaticSiteConfig } from "./types.ts";
    export default {
    country: "IT",
    currency: "EUR",
    languages: ["it"],
    storeKey: "evolve",
    } satisfies StaticSiteConfig;

    Then add it to staticSiteConfigs in configs/index.ts. A new language or country must also be in languages or countries in src/types.ts.

  4. Generate the message file.

    pnpm evolve messages build --locale it-IT

    This writes src/messages/it-IT.json, with every key that the @evolve-storefront/* packages in your package.json ship, plus every key your own code uses. --locale only filters the locales that evolve.config.ts reads from routing, so step 1 comes first. See evolve messages.

  5. Translate the placeholders.

    An untranslated value is wrapped in brackets, such as "[title]". The page shows it as is, so you see what is missing.

    Feature packages ship en, nl, de, fr and es. A locale with one of those languages, such as de-AT, arrives filled in from the package’s de. Italian is not one of them, so every key of it-IT arrives as a placeholder. Plan it as one batch of translation work.

  6. Add the locale to your backend data. See What the backend needs.

  7. Add a flag. The language switch shows public/static/icons/flags/<country>.svg, named after the locale’s country in lowercase. The reference sites already have it.svg.

evolve.config.ts tells evolve messages build which locales the project has and where the files are. It points at routing, so the list exists once.

import { routing } from "@evolve-packages/site-config";
import { defineEvolveConfig } from "@evolve-storefront/commands/config";
export default defineEvolveConfig({
locales: routing.locales,
sourceLocale: "en-GB",
messagesDir: "./src/messages",
srcDir: "./src",
});

All fields are on the configuration reference. The command comes with @evolve-storefront/commands; the reference sites run it as pnpm codegen:messages. It needs Node 24.8 or later.

The command fills holes and never overwrites:

The file has The command
a real value keeps it
nothing, or a placeholder, and a package ships a value fills in the package’s value
nothing, or a placeholder, and no package ships a value writes a placeholder

So your translations and corrections are safe to keep in the file. When a package later changes a default you already have, the command prints both versions and leaves yours. It removes keys that no installed package and no code uses, and lists each one.

A locale such as en-GB falls back to the package’s en. That English is American, so the command tells you which locale it used: review words such as “Canceled”. It never falls back to another language. pt-BR without a pt file gets placeholders, not English.

The site loads the file in src/lib/i18n/next-intl.ts and merges it over en-GB.json. Until the file exists, that locale shows English and logs Failed to load messages for locale.

Your own components use next-intl with a namespace of their own:

const locale = await getLocale();
const t = await getTranslations({ locale, namespace: "TopBar" });
  1. Use the new key in your code, such as t("rating").
  2. Run pnpm evolve messages build. The command finds the useTranslations and getTranslations calls in src/, and adds the key to every locale file as a placeholder ("[rating]").
  3. Write the English text in en-GB.json, the source locale.
  4. Translate the other locales, or send them to your translators.

In CI, run pnpm evolve messages build --check. It writes nothing, and fails when a file would change or when the source locale still has placeholders. Placeholders in other locales are counted, not fatal, because a locale that is partly translated is normal while translation is in progress.

The backend gets the locale only through the store context. Each engine uses it in its own way, so give each one the new locale.

Commercetools. The project and the store need the language and the country. In the reference sites, that is languages and countries in configuration/commercetools/main.tf; the store in stores.tf takes both from there. The engine’s StoreContext asks for product data in it-IT with it as fallback, and selects prices for the locale’s country (IT) and the store’s currency.

Contentful. The locale goes to Contentful as is, so the space needs a locale with exactly the code it-IT. Then add the Italian content.

Storyblok. The module asks for the locale as the Storyblok language, with the language part (it) as fallback. Add the language to the space, and add the market to staticSiteConfigs (step 3) so pages get their Italian paths.

The backend imports site-config too, so deploy the discover and notify services together with the storefront.

  1. Run pnpm evolve messages build --check. It passes when the files are in sync and English has no placeholders.

  2. Start the storefront and open /it. The <html> element has lang="it-IT".

  3. Open the language switch in the top bar. It lists the locales of your store config, each with its flag and its own name for the language (“italiano”).

  4. On a product or CMS page, pick another language. The switch links to the same page in that locale, through the page’s hreflang alternates. A page without an alternate for that locale links to the locale’s home page.

  5. Check that product names, prices, and CMS content are in Italian. If they show in another language or are empty, the backend data is missing the locale.