Skip to content

New: AI agent integration via Model Context Protocol (MCP).Learn more

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.

  1. You use useTranslations() or getTranslations() in your components
  2. Run pnpm codegen:messages to extract all translation keys into messages/source.json
  3. Crowdin picks up new keys and makes them available for translators
  4. A daily CI/CD job downloads translations and opens a PR

Translation messages live in frontend/site/src/messages/:

  • Directorymessages/
    • 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.

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>;
}

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.

The intl-extractor tool scans the source code for useTranslations() and getTranslations() calls and extracts all referenced keys into source.json:

Terminal window
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.

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.

Translations are managed through Crowdin. A GitHub Actions workflow runs daily to:

  1. Upload source.json with any new keys to Crowdin
  2. Download completed translations from Crowdin
  3. 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 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.