# i18n in the App Router: URLs, dictionaries and metadata

One [lang] segment, server-side dictionaries and metadata alternates: how to set up i18n in the App Router with no extra package and no duplicated routes.

> Page: https://ui.eneawork.it/blog/nextjs-i18n-app-router
> Markdown: https://ui.eneawork.it/blog/nextjs-i18n-app-router.md

*2026-11-01*

L’i18n nell’App Router sta in un segmento dinamico, non in una libreria: app/[lang] più un dizionario caricato sul server. Se il locale vive solo in un cookie, quella pagina non è indicizzabile.

## Un segmento, non un pacchetto

Il locale è parte dell’URL. app/[lang]/page.tsx serve tutte le lingue, e generateStaticParams dice a Next quali valori generare in build. Nessun wrapper, nessun provider globale, nessun file di configurazione.

app/ [lang]/ layout.tsx page.tsx dizionari/ it.json en.json layout.tsx middleware.ts

## Il dizionario si carica sul server

Un import statico di JSON dentro il server component è la forma più semplice e la più veloce: il file entra nel bundle del server, non in quello del client. Passa al client solo le stringhe che servono a un componente interattivo.

// app/[lang]/page.tsx import { notFound } from "next/navigation"; const lingue = ["it", "en"] as const; type Lingua = (typeof lingue)[number]; const dizionari: Record Promise }>> = { it: () => import("../dizionari/it.json"), en: () => import("../dizionari/en.json"), }; export function generateStaticParams() { return lingue.map((lang) => ({ lang })); } export default async function Page({ params }: { params: Promise }) { const { lang } = await params; if (!lingue.includes(lang as Lingua)) notFound(); const dizionario = (await dizionari[lang as Lingua]()).default; return {dizionario.titolo}; }

## Dove finisce la traduzione e dove il codice

## Metadata: la parte che si sbaglia

generateMetadata deve dichiarare alternates.canonical e alternates.languages per ogni lingua pubblicata, più openGraph.locale. Un canonical che punta sempre all’italiano annulla il lavoro fatto sul resto della pagina. Gli errori ricorrenti sono raccolti in hreflang: gli errori che costano.

EneUI indicizza 111.985 componenti da 1.472 librerie e 1.556 blocchi: il numero è misurato il 1 novembre 2026 su ui.eneawork.it, non stimato. Un selettore di lingua cambia con il design system, non con il locale.

## Componenti del catalogo utili qui

Tre record da cui partire: il selettore language selector dropdown, la famiglia blocchi navbar con spazio per lo switch, e i blocchi header per una testata bilingue.

Il pattern di redirect nel middleware è lo stesso che uso per l’autenticazione, descritto in middleware Next.js per l’autenticazione. Per il confronto tra framework resta valido Astro vs Next.js sulla documentazione. La struttura delle cartelle è in struttura di un progetto App Router.

## Il limite da mettere in conto

generateStaticParams moltiplica le pagine per lingua. Con 400 articoli e due lingue sono 800 file HTML per build, e ogni lingua nuova aggiunge un fattore. Misura il costo prima di aprire il terzo mercato, non dopo.

Il locale è un dato dell’URL. Tutto ciò che non sta nell’URL, cookie, header, stato del client, è un’eccezione che va documentata con un motivo scritto.

## FAQ

### Serve una libreria come next-intl?

No. Un segmento [lang] e un dizionario JSON coprono il caso normale. Una libreria aggiunge plurali, interpolazione e controllo delle chiavi mancanti: adottala quando ti servono davvero.

### Meglio il locale nel cookie o nell’URL?

Nell’URL. Il cookie produce una sola versione indicizzabile e un redirect che i crawler non seguono. Il cookie resta utile solo come preferenza per il primo accesso.

### Come traduco i metadata?

Con generateMetadata che legge lo stesso dizionario e restituisce title, description e alternates per la lingua corrente. Nessun testo tradotto nel componente.

### Posso tradurre solo una parte del sito?

Sì. Dichiara in alternates.languages solo le lingue che esistono davvero. Un hreflang verso una pagina non pubblicata è un errore, non un segnale.

### Come gestisco i plurali?

Con Intl.PluralRules sulla lingua corrente, oppure con una libreria che lo faccia. Concatenare suffissi produce testo sbagliato in quasi tutte le lingue.
