Flerspråk i App Router
i18n i Next.js App Router med next-intl: URL-routing per språk, server- och klientöversättningar, generateStaticParams och vanliga fallgropar.
Flerspråkiga sajter är ett av de vanligaste kraven i kommersiella Next.js-projekt, men App Router levereras utan inbyggd i18n-infrastruktur. Den här guiden visar hur du bygger upp en komplett lösning med next-intl: URL-routing per språk, översättningar i både Server Components och Client Components, Middleware för locale-detection och statisk export med generateStaticParams.
Varför App Router saknar inbyggd i18n
Pages Router hade ett i18n-block i next.config.js som skötte locale-detection, omdirigering och domänbaserad routing automatiskt. App Router tog bort det blocket helt.
Det är inte ett misstag utan ett designval. App Routers segmentbaserade filsystem gör att locale-prefixade URL:er som /sv/ och /en/ passar naturligt som ett [locale]-segment i mappstrukturen. Vad Next.js inte tillhandahåller är rörmokeriarbetet: att läsa Accept-Language-headern, omdirigera från / till /sv/, och ladda in rätt meddelandefil för varje request.
Det är här bibliotek som next-intl kliver in. Det fyller luckan med Middleware för locale-detection, serverhjälpfunktioner för Server Components och hooks för Client Components, allt designat för App Router.
URL-struktur med [locale]
Grundprincipen är enkel: alla routes lägger sig inuti ett [locale]-segment. Filsystemet ser ut så här:
app/
[locale]/
layout.jsx
page.jsx
om-oss/
page.jsx
blogg/
[slug]/
page.jsx
Det innebär att /sv/om-oss och /en/om-oss är separata routes med var sin params.locale. Alla interna länkar måste inkludera locale-prefixet, annars hamnar användaren utanför [locale]-trädet.
next-intl exporterar en Link-komponent och en useRouter som automatiskt sätter in rätt locale-prefix baserat på den aktiva localen. I stället för att skriva:
<Link href={`/${locale}/om-oss`}>Om oss</Link>
kan du skriva:
import { Link } from '@/i18n/navigation'
<Link href="/om-oss">Om oss</Link>
Biblioteket sköter prefix-logiken internt. Detta minskar risken för att locale-prefixet glöms bort eller stavas fel på ett enskilt ställe.
next-intl: installation och setup
Installera paketet:
npm install next-intl
Skapa meddelandefiler för varje språk under messages/:
// messages/sv.json
{
"Hem": {
"rubrik": "Välkommen till vår sajt",
"beskrivning": "Här hittar du allt du behöver."
},
"Navigation": {
"hem": "Hem",
"om": "Om oss"
}
}
// messages/en.json
{
"Hem": {
"rubrik": "Welcome to our site",
"beskrivning": "Find everything you need here."
},
"Navigation": {
"hem": "Home",
"om": "About us"
}
}
Skapa en i18n.js i projektroten som talar om för next-intl hur den ska ladda meddelandefiler:
// i18n.js
import { getRequestConfig } from 'next-intl/server'
export default getRequestConfig(async ({ locale }) => ({
messages: (await import(`./messages/${locale}.json`)).default
}))
Aktivera sedan pluginet i next.config.js:
// next.config.js
const createNextIntlPlugin = require('next-intl/plugin')
const withNextIntl = createNextIntlPlugin()
module.exports = withNextIntl({})
Pluginet pekar automatiskt på i18n.js i projektroten. Om du vill ha filen på en annan plats skickar du sökvägen som argument till createNextIntlPlugin('./src/i18n.js').
Översättningar i Server Components
I Server Components använder du getTranslations från next-intl/server. Funktionen är asynkron och kan anropas direkt i en async-komponent:
// app/[locale]/page.jsx
import { getTranslations } from 'next-intl/server'
export default async function Hem() {
const t = await getTranslations('Hem')
return (
<main>
<h1>{t('rubrik')}</h1>
<p>{t('beskrivning')}</p>
</main>
)
}
Samma funktion fungerar i generateMetadata. Där måste du skicka locale-parametern explicit eftersom funktionen inte längre körs i request-kontexten på samma sätt:
export async function generateMetadata({ params }) {
const { locale } = await params
const t = await getTranslations({ locale, namespace: 'Hem' })
return {
title: t('rubrik'),
}
}
Att glömma locale-parametern i generateMetadata är ett vanligt misstag som gör att metadata alltid renderas med standardlocalen, oavsett vilken URL som besöks. Mer om det under fallgropar.
Översättningar i Client Components
I Client Components används hooken useTranslations. Den är synkron, inget await:
'use client'
import { useTranslations } from 'next-intl'
import Link from 'next/link'
export function Navigering() {
const t = useTranslations('Navigation')
return (
<nav>
<Link href="/">{t('hem')}</Link>
<Link href="/om-oss">{t('om')}</Link>
</nav>
)
}
Link från next/link används här istället för <a>-taggar. Locale-prefixet hanteras av Middleware som automatiskt skriver om /om-oss till /sv/om-oss (eller rätt locale) för inkommande requests, så plain href-värden fungerar korrekt.
För att hooken ska fungera behöver meddelandena skickas ned till klienten. Det sköts via NextIntlClientProvider i locale-layouten. Sätt upp layouten så här:
// app/[locale]/layout.jsx
import { NextIntlClientProvider } from 'next-intl'
import { getMessages } from 'next-intl/server'
export default async function LocaleLayout({ children, params }) {
const { locale } = await params
const messages = await getMessages()
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
)
}
getMessages() hämtar alla meddelanden för den aktiva localen och gör dem tillgängliga för klientkomponenter via React Context. Det innebär att hela meddelandefilen skickas till webbläsaren, vilket är rimligt för de flesta sajter. Om du har mycket stora meddelandefiler kan du skicka enbart ett namespace: <NextIntlClientProvider messages={messages['Navigation']}>.
Middleware för locale-detection
Middleware sköter omdirigering av inkommande requests till rätt locale-prefix. Skapa filen middleware.js i projektroten:
// middleware.js
import createMiddleware from 'next-intl/middleware'
export default createMiddleware({
locales: ['sv', 'en'],
defaultLocale: 'sv',
})
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
}
Hur det fungerar i praktiken: en användare med svenska webbläsarinställningar som besöker /om-oss skickas vidare till /sv/om-oss. En användare med engelska inställningar skickas till /en/om-oss. Om ingen matchande locale hittas i Accept-Language-headern används defaultLocale som reserv.
matcher-mönstret utesluter Next.js-interna paths (_next) och statiska filer (allt med ett filsuffix), så att bilder, fonts och manifests inte passerar genom Middleware i onödan.
Du kan också konfigurera localePrefix: 'always' (standard) eller localePrefix: 'as-needed' om du vill att standardlocalen ska hanteras utan prefix, det vill säga att /om-oss fungerar för sv utan omdirigering till /sv/om-oss.
generateStaticParams för statisk export
Om sajten byggs som statisk export med output: 'export' i next.config.js måste Next.js veta vid byggtid vilka locales som ska genereras. Det görs med generateStaticParams i [locale]-layouten:
// app/[locale]/layout.jsx
export function generateStaticParams() {
return [{ locale: 'sv' }, { locale: 'en' }]
}
Next.js bygger nu /sv/ och /en/ med alla sina undersidor vid varje build. Utan den här funktionen vet inte den statiska exporten om att [locale]-segmentet ska materialiseras med specifika värden, och inga locale-specifika sidor genereras.
För dynamiska routes som [locale]/blogg/[slug]/page.jsx kombineras locale-parametern med slug-parametern:
export async function generateStaticParams() {
const inlagg = await hamtaAllaInlagg()
const locales = ['sv', 'en']
return locales.flatMap((locale) =>
inlagg.map((inlagg) => ({ locale, slug: inlagg.slug }))
)
}
Vanliga fallgropar
1. Hydration mismatch från locale i cookie
Om locale lagras i en cookie i stället för i URL:en kan servern och klienten läsa olika värden vid hydration. Servern renderar /om-oss med sv-meddelanden, klienten läser cookien och försöker byta till en, och React kastar ett hydration-fel. Lösningen är att alltid använda URL-baserad locale-routing och aldrig förlita sig på cookies eller localStorage som enda sanningskälla för locale.
2. Saknade översättningar
En nyckel som finns i sv.json men saknas i en.json ger en konsolvarning i utvecklingsläget och faller tillbaka på nyckelsträngen i produktion, till exempel Hem.rubrik i stället för "Welcome to our site". Fånga detta tidigt med next-intl's TypeScript-integration eller genom att köra en diff på nycklar mellan meddelandefilerna som ett steg i CI-pipelinen.
3. Glömda generateStaticParams
Om du lägger till en ny route under [locale]/ utan att antingen ärva generateStaticParams från [locale]/layout.jsx eller definiera det lokalt, bygger Next.js sidan för standardlocalen men inte för de övriga. Felet syns inte under bygget utan märks när en sida saknas efter deploy. Sätt ett lint-verktyg eller en custom check på att alla [locale]-routes har tillgång till generateStaticParams.
4. Fel locale i metadata
generateMetadata körs på servern men utanför den normala request-livscykeln för rendering. Om du anropar getTranslations('Hem') utan att skicka med locale från params används standardlocalen för alla sidor, oavsett vilken locale-URL som besöks. Resultatet är att <title> och <meta description> alltid är på svenska, även på /en/-sidorna. Lösningen är att alltid destrukturera locale från params och skicka det explicit: getTranslations({ locale, namespace: 'Hem' }).
