Every phase of this rebuild before internationalization shipped with zero new dependencies. A scroll listener, a substring search, a CSS keyframe — small, simple problems that a library would have been overkill for. So when Phase 4 (EN → FR → AR locale routing) came up, the default instinct was the same: can this be hand-rolled too?
The problem & constraints
Locale routing for a Next.js App Router site needs three things working together: URL-prefix routing that coexists with every existing English URL unchanged (real backlinks — my CV, my LinkedIn profile, application emails — already point at today's unprefixed URLs), locale negotiation, and static rendering that stays correct across every route once a second and third locale exist. None of that is exposed as a built-in App Router primitive.
Note
This is the detail that changed my answer. Every prior "no dependency" decision in this project was about avoiding unnecessary abstraction over something genuinely simple. Locale routing isn't simple in that same way — the realistic alternative to a library isn't "a few more lines," it's reimplementing a smaller, less-tested subset of what one already provides.
The decision
I used next-intl for locale routing, message dictionaries, and
locale-aware navigation, with localePrefix: "as-needed" — English stays
unprefixed at /, /about, etc.; French and Arabic get /fr and /ar
prefixes. That last part was the deciding factor over localePrefix: "always", which would have moved the English homepage to /en and broken
every existing link. Full reasoning is in ADR
0003.
What it actually cost
Adding a real dependency wasn't free. The install hit a peer-dependency
conflict — the version I initially pinned declared support only up to
Next.js 15, one major version behind this project's Next.js 16.2.10. A stale
.next/ typed-routes cache from before the route tree moved under
app/[locale]/... produced a confusing type error on the next attempt. Then
a version-specific API gap: the helper I'd written i18n/request.ts against
wasn't actually exported by the pinned version.
Decision
None of these were next-intl bugs — they were consequences of pinning a version from general familiarity with the library instead of checking the npm registry against the actual installed Next.js version first. Once I checked the registry directly and bumped to the release that explicitly declares Next 16 support, the peer conflict and the Turbopack prerender error it was quietly causing both disappeared in the same fix.
What I'd do differently
Check the peer range on the registry before pinning a version, not after the install fails — especially for a dependency that touches the framework's build pipeline directly (a Next.js plugin) rather than just running at runtime. The full incident account, with exact error messages and fixes, is in this project's own maintenance log.