Verified against Claude Code · 2026-08-01
Structure locale-aware routing in the App Router without reaching for a heavy i18n framework
Designs [locale] segment routing with middleware-based locale negotiation and generateStaticParams for pre-rendering, scoped honestly to whether a dedicated i18n library is actually warranted.
The prompt
Ready to copy — highlighted parts are example details you can swap.
You are structuring locale-aware routing for a Next.js App Router site, deciding the segment structure, the locale-detection strategy, and whether the stated needs actually justify a dedicated i18n library or are simple enough for a lighter, hand-rolled approach. LOCALES SUPPORTED en (default), es, fr CONTENT TRANSLATION APPROACH Marketing copy lives in per-locale JSON dictionaries; no pluralization or rich formatting needs beyond simple string interpolation for a name or count DEFAULT LOCALE BEHAVIOR Undecided today — some internal links point to /about, others to /en/about, inconsistently CURRENT STRUCTURE Locale is currently read from a cookie only, with no [locale] segment in the URL at all — every page is served at the same path regardless of language, which breaks sharing a Spanish-language link with someone whose browser defaults to English DESIGN RULES Nest every localized route under a [locale] dynamic segment — app/[locale]/page.tsx, app/[locale]/about/page.tsx — so the locale is a first-class part of the URL and the route structure, rather than inferred from a cookie or a header alone with no reflection in the path; a locale that isn't in the URL can't be shared via a link, can't be indexed separately by search engines for each language, and breaks the browser's own back button expectation when a visitor switches languages. Use generateStaticParams at the [locale] segment level to pre-render each supported locale's static pages at build time rather than resolving locale-specific content only at request time, for any content that doesn't need to be dynamic per-request beyond varying by locale. Do the initial locale negotiation, meaning deciding which locale an unprefixed request like the bare root path should redirect to, in middleware — reading the Accept-Language header and any existing locale preference cookie, then issuing a redirect to the matching [locale] path — rather than duplicating that negotiation logic inside every page component that might receive an unprefixed request. Decide explicitly, and state the decision, on default-locale URL behavior: whether the default locale gets its own visible segment in the URL (/en/about) or is treated as the unprefixed default (/about, with /es/about for the non-default locale) — both are legitimate choices, but leaving it undecided produces inconsistent internal links as different pages get built by different people making different implicit assumptions. Assess honestly whether the actual translation need here — a handful of static strings, or full pluralization rules, date/number formatting, and rich message interpolation across many locales — justifies a dedicated library like next-intl, versus a much simpler hand-rolled dictionary object keyed by locale; recommend the lighter approach when the stated need doesn't go beyond static string lookup, and say plainly why a heavier library would be unjustified overhead rather than defaulting to it because it's the more commonly cited solution. OUTPUT FORMAT The folder structure showing the [locale] segment and where generateStaticParams lives, the middleware's locale-negotiation logic, and one paragraph stating the default-locale URL decision and the translation-approach recommendation, each with its reasoning made explicit rather than left implied.
Customize
Optional — swap in your own details for the highlighted parts above.
Why this works
Putting the locale in the URL path, rather than resolving it silently from a cookie or header, is the decision that actually determines whether the site's internationalization is real or cosmetic: a locale that lives only in a cookie means two visitors on the same device but different browser profiles, or the same visitor after clearing cookies, land on a different language with no way to deliberately choose or share a specific one, and search engines have no way to index the Spanish and French versions of a page as the distinct, separately rankable pages they actually are, since from a crawler's perspective there's only ever been one URL. Using generateStaticParams at the locale segment level, rather than resolving translated content only at request time inside each page, is what lets locale-specific pages get the same static-generation and caching benefits as a single-locale site would — a site that treats locale as just another piece of request-time data, fetched inside every page component regardless of whether the underlying content ever changes, gives up build-time pre-rendering for every localized page even when nothing about the content is actually dynamic beyond which language it's written in. Doing locale negotiation once in middleware, rather than reimplementing an Accept-Language check inside every page that might receive an unprefixed request, matters because that logic has real edge cases — a malformed header, a locale the site doesn't actually support, a returning visitor's saved preference that should override the header — and duplicating a from-scratch implementation of those edge cases across many page files is exactly how two pages end up disagreeing about which locale an ambiguous request should resolve to, a bug that only surfaces when someone happens to test the specific unprefixed page that got the negotiation logic slightly wrong. The honest library-versus-hand-rolled assessment matters because the default answer in most i18n advice is to reach for a full framework regardless of actual need, and a site with a handful of static strings across three locales gets real, measurable complexity — a new dependency, a new set of APIs to learn, a build step to configure — for a problem a plain object literal keyed by locale solves completely, while a site that genuinely needs plural rules, date formatting per locale, and nested message interpolation across a dozen languages would be taking on real risk trying to hand-roll those correctly instead of using a library that's already solved them.
What you get back
app/[locale]/layout.tsx (generateStaticParams returns [{ locale: 'en' }, { locale: 'es' }, { locale: 'fr' }]), app/[locale]/page.tsx, app/[locale]/about/page.tsx. middleware.ts reads the Accept-Language header and a saved locale cookie for any request to '/', then redirects to the matching /[locale] path — this negotiation logic exists in exactly one place. Decision: default locale (en) gets its own visible /en segment, matching es and fr, rather than an unprefixed exception — this keeps every internal link generation function consistent instead of special-casing one locale. Translation approach: a hand-rolled dictionary object per locale is sufficient here — there's no pluralization or date formatting need stated, so next-intl would add a dependency and a learning curve for a problem three JSON files already solve.
Verified against
Claude Code Sonnet 4.6 · 2026-08-01
Cursor Cursor 2.1 · 2026-08-07
Changelog
- 2026-08-01 — Initial publish, verified against Claude Code (Sonnet 4.6) and Cursor 2.1 on a Next.js 16 marketing site adding a [locale] segment.
Need this built into your business?
If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.
EXPLORE CUSTOM SOFTWARE
