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 |
How a page gets its locale
Section titled “How a page gets its locale”-
src/proxy.tsruns the next-intl middleware, made from theroutinginsite-config. It reads the prefix (/be/fr) and rewrites the request toapp/[locale]/…with the full locale (fr-BE). -
app/[locale]/layout.tsxchecks 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,}; -
The GraphQL fetchers send the store context as the
X-StoreContext-StoreKey,X-StoreContext-LocaleandX-StoreContext-Currencyheaders 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.
Add a locale
Section titled “Add a locale”This example adds Italian for Italy, it-IT.
-
Add the locale to routing.
Add it to
locales, give it a URL prefix, and give it a path in each localized entry ofpathnames: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, andevolve.config.tsall 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. -
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_KEYenvironment 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 tostoreConfigsinindex.ts, and extend theStoreKeyandCurrencytypes incontext/types.ts. -
Add the market for translated CMS paths.
With Storyblok,
site-configalso lists the languages of each country. The discover service passesstaticSiteConfigsto theStoryblokModuleassiteConfigs, 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
ittostaticSiteConfigsinconfigs/index.ts. A new language or country must also be inlanguagesorcountriesinsrc/types.ts. -
Generate the message file.
pnpm evolve messages build --locale it-ITThis writes
src/messages/it-IT.json, with every key that the@evolve-storefront/*packages in yourpackage.jsonship, plus every key your own code uses.--localeonly filters the locales thatevolve.config.tsreads from routing, so step 1 comes first. Seeevolve messages. -
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,frandes. A locale with one of those languages, such asde-AT, arrives filled in from the package’sde. Italian is not one of them, so every key ofit-ITarrives as a placeholder. Plan it as one batch of translation work. -
Add the locale to your backend data. See What the backend needs.
-
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 haveit.svg.
The message files
Section titled “The message files”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.
Translate your own texts
Section titled “Translate your own texts”Your own components use next-intl with a namespace of their own:
const locale = await getLocale();const t = await getTranslations({ locale, namespace: "TopBar" });- Use the new key in your code, such as
t("rating"). - Run
pnpm evolve messages build. The command finds theuseTranslationsandgetTranslationscalls insrc/, and adds the key to every locale file as a placeholder ("[rating]"). - Write the English text in
en-GB.json, the source locale. - 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.
What the backend needs
Section titled “What the backend needs”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.
Check it
Section titled “Check it”-
Run
pnpm evolve messages build --check. It passes when the files are in sync and English has no placeholders. -
Start the storefront and open
/it. The<html>element haslang="it-IT". -
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”).
-
On a product or CMS page, pick another language. The switch links to the same page in that locale, through the page’s
hreflangalternates. A page without an alternate for that locale links to the locale’s home page. -
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.

