The client runs a multilingual Next.js site with three locales — Swedish, English, and Danish — built on next-intl. Swedish is the primary language. The default locale was hardcoded to "sv" in two separate files, and changing it required a code change and a redeploy. The ask: make it configurable through an environment variable so the default can shift between environments without touching source code.
Straightforward on paper. One gotcha after deployment.
The Starting Point
The i18n setup followed the standard next-intl pattern for App Router. Two files owned the locale configuration. First, i18n.ts — the core config that next-intl's plugin loads at build time:
export const locales = ["sv", "en", "da"] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = "sv";Second, navigation.ts — the routing definition consumed by the middleware:
export const routing = defineRouting({
locales,
defaultLocale: "sv",
localePrefix: "always",
alternateLinks: false,
});Two files, two hardcoded "sv" strings. The defaultLocale in i18n.ts was exported and used as a fallback when the request's locale parameter is missing or invalid. The one in navigation.ts told the middleware where to send visitors who arrive without a locale prefix. Both needed to agree.
The duplication was the first problem. If someone updated i18n.ts but forgot navigation.ts, the middleware would route to one locale while the server-side config fell back to another.
Wiring the Environment Variable
The fix was to make i18n.ts the single source of truth and have navigation.ts import from it. The defaultLocale reads from process.env with a fallback:
// i18n.ts
export const defaultLocale: Locale =
(process.env.NEXT_PUBLIC_DEFAULT_LOCALE as Locale) || "sv";And navigation.ts stopped owning its own value:
// navigation.ts
import { locales, defaultLocale } from "./i18n";
export const routing = defineRouting({
locales,
defaultLocale,
localePrefix: "always",
alternateLinks: false,
});The NEXT_PUBLIC_ prefix is deliberate. next-intl's routing runs both server-side in the middleware and client-side through createNavigation. Without the prefix, Next.js strips the variable from the client bundle and the navigation helpers fall back to "sv" regardless of what the server resolved.
The .env file got one new line:
NEXT_PUBLIC_DEFAULT_LOCALE=sv
Deployed. Opened the site. Landed on /en.
The Browser Override
next-intl's middleware does more than just check the URL for a locale prefix. When a visitor arrives at the root path without a locale, it reads the Accept-Language header from the browser, matches it against the supported locales, and redirects accordingly. My browser is set to English. The middleware saw en in the header, matched it against the locales array, and sent me to /en — completely ignoring the configured defaultLocale.
This is localeDetection at work. It's enabled by default in next-intl and it's usually the right behavior for multilingual sites that want to greet visitors in their preferred language. But for this site, Swedish is the canonical locale. The site's content, brand, and primary audience are Swedish. Visitors should land on /sv and switch manually if needed.
One line in the routing config:
export const routing = defineRouting({
locales,
defaultLocale,
localePrefix: "always",
localeDetection: false,
alternateLinks: false,
});With localeDetection: false, the middleware stops reading Accept-Language. First-time visitors always land on whatever defaultLocale resolves to. The language switcher still works — users can navigate to /en or /da manually, and next-intl respects the locale prefix in the URL for all subsequent navigation. It just stops guessing on the initial visit.
Files Changed
Three files total. i18n.ts gained the process.env read with the "sv" fallback. navigation.ts dropped its hardcoded "sv" in favor of importing defaultLocale from i18n.ts and added localeDetection: false. The .env file got NEXT_PUBLIC_DEFAULT_LOCALE=sv.
The change itself was small. The lesson was that next-intl locale resolution is a two-step process — first the middleware decides where to route, then the server config decides what to render. The defaultLocale setting only controls the second step. The first step has its own opinion, and unless you explicitly silence it, browser preferences win. A correctly configured default locale that nobody ever sees because the middleware overrides it before the page loads — that's the kind of thing you only discover in production with a browser that isn't set to the default language.