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
| File | Role |
|---|---|
src/lib/i18n/routing.ts | defineRouting config + navigation helpers |
src/lib/i18n/request.ts | getRequestConfig — loads messages per request |
src/lib/i18n/loadTranslations.ts | Server-side manual translation loader with in-memory cache |
src/constants/supportedLocales.ts | Locale list and Clerk locale mapping |
messages/{locale}.json | Translation files (one per locale) |
src/app/[locale]/layout.tsx | Root 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):
| Export | Use for |
|---|---|
Link | Locale-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
- Open
messages/en.json(the reference locale). - Add the key under the appropriate namespace:
{ "myNamespace": { "newKey": "New English text" } }
- Add the translation to all other 9 locale files (
ca,es,fr,de,it,gl,nl,eu,pt). - Use in code:
- Client:
const t = useTranslations("myNamespace"); t("newKey") - Server:
const t = await getTranslations({ locale, namespace: "myNamespace" }); t("newKey")
- Client:
Missing keys in non-English locales will fall back to the key name, not to en.json. Keep all locale files in sync.