Skip to main content

App-level i18n

The app uses next-intl for internationalization. This page covers the Next.js app side. For Docusaurus i18n (the dev-docs site), see Localization.

Source files

FileRole
src/lib/i18n/routing.tsdefineRouting config + navigation helpers
src/lib/i18n/request.tsgetRequestConfig — loads messages per request
src/lib/i18n/loadTranslations.tsServer-side manual translation loader with in-memory cache
src/constants/supportedLocales.tsLocale list and Clerk locale mapping
messages/{locale}.jsonTranslation files (one per locale)
src/app/[locale]/layout.tsxRoot locale layout; wraps children in the next-intl provider

Supported locales

// src/constants/supportedLocales.ts
const supportedLocales = ["ca", "es", "en", "fr", "de", "it", "gl", "nl", "eu", "pt"] as const;

Default locale: "en".

URL structure: /{locale}/... (e.g. /es/, /en/e/activities).

localeDetection: false — the locale is taken exclusively from the URL segment; Accept-Language headers are never used.


Routing configuration

// src/lib/i18n/routing.ts
export const routing = defineRouting({
locales: supportedLocales,
defaultLocale: "en",
localeDetection: false,
});

export const { Link, redirect, usePathname, useRouter, getPathname } =
createNavigation(routing);

Navigation helpers (use these instead of the plain Next.js equivalents):

ExportUse for
LinkLocale-aware <a> — auto-prefixes the current locale
redirect()Locale-aware programmatic redirect
usePathname()Current pathname without the locale prefix
useRouter()Locale-aware router
getPathname()Build a locale-aware path string

Request configuration

// src/lib/i18n/request.ts
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
if (!locale || !routing.locales.includes(locale as Locale)) {
locale = routing.defaultLocale; // fallback to "en"
}
return {
locale,
messages: (await import(`../../../messages/${locale}.json`)).default,
};
});

This runs once per server request. It validates the locale and dynamically imports the matching JSON file.


Message files

Directory: messages/ (project root)

One file per locale: en.json, es.json, fr.json, etc.

Top-level keys are namespaces. Example structure from en.json:

{
"common": { "organization": "Organization", ... },
"activity-declaration": { "directLink": "...", "downloadAsPdf": "...", ... },
"activity-information": { ... },
"activitydetails": { "Activate": "...", ... },
"attribute": {
"legalbasis": { "a": "Article 6(1)(a) - Consent", "b": "...", ... },
"true": "Yes",
"false": "No"
},
"gforms": { ... },
...
}

The attribute namespace

The attribute namespace translates GDPR enum values. Enum keys (e.g. "a", "b") are stored in English in MongoDB; at render time they are looked up in this namespace:

const legalbasis = (activity.legalbasis ?? []).map((key) => t(key));
// "a" → "Article 6(1)(a) - Consent" (in the request locale)

attribute.true / attribute.false translate boolean fields like profiling and transfers.


Using translations in components

Client components

"use client";
import { useTranslations, useLocale } from "next-intl";

export function MyComponent() {
const t = useTranslations("common");
const locale = useLocale(); // "en", "es", etc.
return <p>{t("organization")}</p>;
}

Server components and actions

import { getTranslations } from "next-intl/server";

export async function myServerAction(locale: string) {
const t = await getTranslations({ locale, namespace: "attribute" });
return t("true"); // "Yes" in the target locale
}

locale must be passed explicitly in server contexts.

Outside the React tree (manual loading)

For contexts that have no next-intl provider (e.g. the GForms locale API route):

import { loadTranslations, t } from "@/lib/i18n/loadTranslations";

const translations = loadTranslations("en"); // cached in-memory per process
const label = t(translations, "common.organization", "Organization");

loadTranslations reads from disk on first call per locale and caches the result for the process lifetime. Changes to message files require a server restart to take effect.


Adding a new string

  1. Open messages/en.json (the reference locale).
  2. Add the key under the appropriate namespace:
    { "myNamespace": { "newKey": "New English text" } }
  3. Add the translation to all other 9 locale files (ca, es, fr, de, it, gl, nl, eu, pt).
  4. Use in code:
    • Client: const t = useTranslations("myNamespace"); t("newKey")
    • Server: const t = await getTranslations({ locale, namespace: "myNamespace" }); t("newKey")

Missing keys in non-English locales will fall back to the key name, not to en.json. Keep all locale files in sync.