Skip to main content

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

  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

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:

  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

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.