Webion

Internazionalizzazione con Next.js e next-intl

Guida pratica all’internazionalizzazione con Next.js e next-intl: architettura, middleware e traduzioni scalabili.

S
Saad Medhaton October 1, 2025
G15-compressed

Internazionalizzazione con Next.js e next-intl: come lo implementiamo in Webion


L'internazionalizzazione è ormai un requisito di base nei nostri progetti. All'interno dei nostri applicativi utilizziamo next-intl per orchestrare lingua, caricamento dei messaggi e preferenze dell'utente. Di seguito racconto l'architettura completa, dal boot alla traduzione dei componenti riutilizzabili.

Architettura in breve

  • Next.js 15 con App Router e rendering ibrido server/client.
  • next-intl per localizzazione, middleware e API server-side.
  • Cataloghi JSON versione per lingua in apps/app/messages.
  • Middleware (apps/app/middleware.ts) che risolve la lingua partendo dalla richiesta.
  • Provider globale nel layout per servire traduzioni ovunque.

Configurazione centrale di next-intl


TYPESCRIPT
1// apps/app/i18n.ts
2import { getRequestConfig } from "next-intl/server";
3
4export const locales = ["en"] as const;
5export const defaultLocale = "en";
6export const localePrefix = "never";
7
8export default getRequestConfig(async ({ requestLocale }) => {
9 let locale = await requestLocale;
10
11 if (!locales.includes(locale)) {
12 locale = defaultLocale;
13 }
14
15 return {
16 locale,
17 messages: (await import(`./messages/${locale}.json`)).default,
18 };
19});


Punti chiave:

  • getRequestConfig centralizza il caricamento dei messaggi per qualsiasi richiesta.
  • Le traduzioni sono importate dinamicamente, così Next crea bundle separati per ciascuna lingua.
  • localePrefix = "never" mantiene URL puliti (/dashboard anziché /en/dashboard) e demanda la selezione della lingua al middleware.

Collegare il plugin al build di Next.js


TYPESCRIPT
1// apps/app/next.config.js
2import createIntlPlugin from "next-intl/plugin";
3
4const withNextIntl = createIntlPlugin("./i18n.ts");
5
6export default withNextIntl({
7 output: "standalone",
8});


Il plugin registra i parametri di next-intl lato build, abilita l'estrazione dei messaggi e rende trasparente l'uso di getLocale/getMessages nei server component.

Middleware


TYPESCRIPT
1// apps/app/middleware.ts
2import { type NextRequest } from "next/server";
3import createMiddleware from "next-intl/middleware";
4import { locales, defaultLocale, localePrefix } from "./i18n";
5
6const intlMiddleware = createMiddleware({
7 locales,
8 defaultLocale,
9 localePrefix,
10});
11
12export default function middleware(req: NextRequest) {
13 return intlMiddleware(req);
14}


Il middleware:

  • Determina la lingua in ingresso (header Accept-Language, cookie, ecc.).
  • Applica il locale di default se la richiesta chiede una lingua non supportata.

Un provider globale nel layout


TYPESCRIPT
1// apps/app/app/layout.tsx
2import { NextIntlClientProvider } from "next-intl";
3import { getLocale, getMessages } from "next-intl/server";
4
5export default async function RootLayout({ children }: { children: React.ReactNode }) {
6 const locale = await getLocale();
7 const messages = await getMessages();
8
9 return (
10 <html lang={locale} suppressHydrationWarning>
11 <ThemeLayout isMobile={await isMobileDevice()}>
12 <body>
13 <NextIntlClientProvider
14 locale={locale}
15 messages={messages}
16 >
17 <RootLayoutClient>
18 {children}
19 </RootLayoutClient>
20 </NextIntlClientProvider>
21 </body>
22 </ThemeLayout>
23 </html>
24 );
25}


Recupero le informazioni di lingua direttamente lato server e le inietto nel provider. Qualsiasi componente client (anche in pacchetti riutilizzabili) può così chiamare useTranslations senza boilerplate aggiuntivo.

Strutturare i cataloghi di messaggi


JSON
1// apps/app/messages/en.json
2{
3 "dashboard": {
4 "machinesCount": "{count} machines",
5 "dateRange": {
6 "last-hour": "Last hour",
7 "week-to-date": "Week to date"
8 }
9 },
10 "agentCard": {
11 "selectedVariable_one": "1 selected variable",
12 "selectedVariable_other": "{count} selected variables"
13 }
14}


Strategie adottate:

  • Nome delle chiavi gerarchico per riflettere il dominio funzionale.
  • Placeholder con sintassi ICU ({count}) per numeri, stringhe e valori dinamici.
  • I cataloghi vengono replicati per ciascuna lingua (en.json, it.json), mantenendo la stessa struttura.

Traduzioni nei componenti client


TYPESCRIPT
1// apps/app/app/[locale]/(auth)/login/page.tsx
2"use client";
3import { useTranslations } from "next-intl";
4
5export default function LoginPage() {
6 const t = useTranslations("login");
7
8 return (
9 <form>
10 <label>{t("emailLabel")}</label>
11 <input placeholder="name@example.com" />
12 <button type="submit">{t("loginButton")}</button>
13 </form>
14 );
15}


useTranslations accetta il namespace (qui login) e restituisce un resolver tipizzato (grazie al literal type dell'oggetto JSON). Per messaggi parametrizzati basta passare un oggetto, es. t("machinesCount", { count }).

Traduzioni nei server component e negli error boundary


TYPESCRIPT
1// apps/app/app/not-found.tsx
2import { getTranslations } from "next-intl/server";
3
4export default async function NotFound() {
5 const t = await getTranslations("notFound");
6 return (
7 <section>
8 <h2>{t("title")}</h2>
9 <p>{t("description")}</p>
10 </section>
11 );
12}


getTranslations funziona in qualsiasi server component o azione, così posso localizzare pagine d'errore, metadata o risposte API senza accedere a hook client-side.

Componenti condivisi e pacchetti UI

Anche un pacchetto UI condiviso (packages/ui/src/user/Component.tsx) può usare useTranslations("user"). Il provider nel layout principale garantisce che i testi siano disponibili indipendentemente da dove venga montato il componente: è essenziale in un monorepo con design system riutilizzabile.

Aggiungere una nuova lingua

  1. Creare il file dei messaggi apps/app/messages/<locale>.json replicando la struttura esistente.
  2. Aggiornare l'array locales in apps/app/i18n.ts (es. ["en", "it"] as const).
  3. (Opzionale) Configurare la lingua preferita nel middleware via cookie o header personalizzati.

Buone pratiche che applichiamo

  • Mantengo localePrefix = "never" per preservare URL corti; se servono percorsi localizzati basta cambiare la strategia senza toccare i componenti.
  • Evito concatenazioni manuali: ogni frase variabile vive nel catalogo JSON.

Conclusione

La combinazione Next.js + next-intl consente di controllare il locale a livello centrale, distribuire le traduzioni dinamicamente e riutilizzarle in tutto il monorepo. La struttura a cataloghi JSON e il provider globale semplificano l'aggiunta di altre lingue in futuro.

Saad Medhat

La vecchia porta la sbarra