A genuinely multilingual Next.js site with next-intl
Localized routing, static rendering, hreflang and a language switcher: how I structure a Next.js site in ten languages without sacrificing performance or SEO.
Why multilingual is a real architecture question
Translating an interface is easy. Building a proper multilingual site — indexable, fast, maintainable — is an architecture decision you make at the start, not the end. This portfolio exists in ten languages, and here are the choices that hold up at scale.
Routing by locale segment
The most robust approach with the App Router: a dynamic [locale] segment at the root. Every page lives under app/[locale]/…, and the language becomes a first-class piece of URL data (/fr/projects, /en/projects).
With next-intl, that segment is validated at render time:
- The supported locales are listed in a central
routingconfig. - An unknown locale triggers a clean
notFound()rather than a broken page. - Navigation goes through a localized
Linkthat automatically prefixes the right language.
The benefit: components never handle the locale "by hand". They ask for a translation, and the system takes care of the rest.
The static-rendering trap
This is the mistake that costs the most in production. If you call the translations without fixing the locale first, next-intl reads the request headers. The result: the page flips to dynamic rendering, and you lose static prerendering — sometimes with a DYNAMIC_SERVER_USAGE error in production.
The fix is a single line, right at the top of each page:
setRequestLocale(locale);
Called before any getTranslations, it guarantees fully static rendering. Combined with a generateStaticParams that crosses locale × slug, it prerenders every page in every language at build time.
Static multilingual isn't slower than monolingual. It's just more demanding about the order of calls.
hreflang, or how to avoid competing with yourself
Without any hint, Google treats your ten language versions as ten rival pages on the same keywords. The solution is hreflang: each page declares the full set of its variants.
Two places must stay in sync:
- Each page's metadata — via a
generateAlternateshelper that produces thealternatetags. - The sitemap — every URL there lists its languages and an
x-default.
The consistency between these two sources is what makes a Spanish search land on /es and not on /fr.
The language switcher, from the user's side
A language selector must change the locale without losing the current page. The naive reflex — sending the user back to the home page — is frustrating. With next-intl's localized usePathname, you get the path without its prefix, and re-emit it in the chosen language. The user stays exactly where they were.
What I take away
- The root
[locale]segment is the foundation; everything flows from it. setRequestLocalebefore the translations: non-negotiable to keep things static.- The
hreflangtags are handled in two places (metadata and sitemap), always in sync. - Text direction (RTL for Arabic) is driven at the
<html dir>level, not page by page.
Done well, multilingual becomes invisible: every visitor feels the site was written for them, and Google knows exactly which door to open.