Translations
Evolve uses next-intl for internationalization. Translation keys are extracted automatically from the source code, stored as JSON, and synced with Crowdin for professional translation management.
How it works
- You use
useTranslations()orgetTranslations()in your components - Run
pnpm codegen:messagesto extract all translation keys intomessages/source.json - Crowdin picks up new keys and makes them available for translators
- A daily CI/CD job downloads translations and opens a PR
Message files
Translation messages live in frontend/site/src/messages/:
messages/
├── source.json # Extracted keys (source of truth)
├── nl-NL.json # Dutch translations
├── en-GB.json # English translations
├── de-DE.json # German translations
├── fr-FR.json # French translations
└── ... # Other locales
Messages use a hierarchical structure where the top-level key matches the component namespace:
{
"AccountPage": {
"title": "Hi, {name}",
"addresses": "Your addresses"
},
"CheckoutForm": {
"email": "Email address",
"billing-address": "Billing address"
}
}
At runtime, next-intl deep-merges the locale-specific file with
source.json, so untranslated keys fall back to the source language.
Using translations in components
Server components
Use getTranslations() from next-intl/server. This is async and
runs at request time:
import { getTranslations } from "next-intl/server";
export default async function AccountPage({ params }) {
const { locale } = await params;
const t = await getTranslations({
namespace: "AccountPage",
locale,
});
return <h1>{t("title", { name: "John" })}</h1>;
}
Client components
Use the useTranslations() hook:
"use client";
import { useTranslations } from "next-intl";
export function CheckoutForm() {
const t = useTranslations("CheckoutForm");
return (
<section>
<h2>{t("email")}</h2>
<h2>{t("billing-address")}</h2>
</section>
);
}
The namespace (first argument) must match a top-level key in the message files.
Extracting translation keys
The intl-extractor tool scans the source code for useTranslations()
and getTranslations() calls and extracts all referenced keys into
source.json:
pnpm codegen:messages
Run this whenever you add or change translation keys. The extractor
generates the complete source.json from the source code, so you
never need to edit that file manually.
Locale routing
Locales are configured in the site-config package and used by next-intl's routing middleware. Each locale gets a URL prefix:
export const localePrefixes = {
"nl-NL": "/nl",
"en-GB": "/en",
"de-DE": "/de",
"fr-FR": "/fr",
"fr-BE": "/be/fr",
"nl-BE": "/be/nl",
};
Translated pathnames (e.g., /cart becoming /warenkorb in German)
are defined in the same routing configuration.
Translation management with Crowdin
Translations are managed through Crowdin. A GitHub Actions workflow runs daily to:
- Upload
source.jsonwith any new keys to Crowdin - Download completed translations from Crowdin
- Open a PR with updated locale files
The workflow can also be triggered manually. Translated locale files should not be edited directly in the repository since Crowdin is the source of truth for translations.
Storybook
Storybook has access to all translations so components render with
real text. The .storybook/next-intl.ts file loads and merges
messages for each locale, matching the runtime behavior.