Define locales once with next-intl's routing config

Locales were listed separately in the middleware, the request config
and several type casts. Use defineRouting and createNavigation, type
the Locale globally, and let the language selector use the locale-aware
router so it keeps the query string and hash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
leanderandClaude Opus 5.5 committed 2026-10-30 22:13:57 +01:00
1 parent b84781ebe5
commit 5b135ce712
13 files changed
+53 -25

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
import type { NextConfig } from "next"
import createNextIntlPlugin from "next-intl/plugin"
const withNextIntl = createNextIntlPlugin("./src/i18n.ts")
const withNextIntl = createNextIntlPlugin()
const nextConfig: NextConfig = {
images: {
+2 -1
View File
@@ -7,6 +7,7 @@ import { getBlogPost } from "@/lib/sanity"
import { PortableText, PortableTextComponents } from "@portabletext/react"
import { Metadata, Viewport } from "next"
import type { Locale } from "next-intl"
import { notFound } from "next/navigation"
import Image from "next/image"
@@ -15,7 +16,7 @@ import * as styles from "./page.module.css"
interface BlogPageParams {
params: Promise<{
slug: string
locale: string
locale: Locale
}>
}
+2 -1
View File
@@ -4,6 +4,7 @@ import Back from "@/components/ui/back"
import { getEvent } from "@/lib/sanity"
import { Metadata, Viewport } from "next"
import type { Locale } from "next-intl"
import { notFound } from "next/navigation"
import { getTranslations } from "next-intl/server"
import { FaCalendarAlt, FaMapMarkerAlt } from "react-icons/fa"
@@ -13,7 +14,7 @@ import * as styles from "./page.module.css"
interface EventPageParams {
params: Promise<{
slug: string
locale: string
locale: Locale
}>
}
+3 -3
View File
@@ -6,7 +6,7 @@ import { notFound } from "next/navigation"
import LanguageSelector from "@components/ui/languageSelector"
import UmamiAnalytics from "@/components/umamiAnalytics"
import { locales } from "@/i18n"
import { routing } from "@/i18n/routing"
import { getAnalyticsConfig } from "@/lib/analyticsConfig"
export const metadata: Metadata = {
@@ -21,7 +21,7 @@ export const metadata: Metadata = {
}
export function generateStaticParams() {
return locales.map(locale => ({ locale }))
return routing.locales.map(locale => ({ locale }))
}
export default async function RootLayout({
@@ -34,7 +34,7 @@ export default async function RootLayout({
const { locale } = await params
// [locale] matches any first path segment, e.g. /favicon.txt or /de
if (!hasLocale(locales, locale)) notFound()
if (!hasLocale(routing.locales, locale)) notFound()
const messages = (await import(`../../../messages/${locale}.json`)).default
+2 -1
View File
@@ -8,12 +8,13 @@ import Office from "@/components/page-sections/office"
import { generateSeoMetadata, generateSeoViewport } from "@/components/seo"
import { Metadata, Viewport } from "next"
import type { Locale } from "next-intl"
export const revalidate = 3600 // 1 hour
interface HomePageParams {
params: Promise<{
locale: string
locale: Locale
}>
}
@@ -29,7 +29,7 @@ interface BoardMember {
const BoardMembers = async () => {
const t = await getTranslations("boardMembers")
const locale = (await getLocale()) as "no" | "en"
const locale = await getLocale()
const data: BoardMember[] = await getBoardMembers()
+2 -1
View File
@@ -1,9 +1,10 @@
import type { Locale } from "next-intl"
import { getTranslations } from "next-intl/server"
import Link from "next/link"
import * as styles from "./notFound.module.css"
const NotFound = async ({ locale }: { locale: string }) => {
const NotFound = async ({ locale }: { locale: Locale }) => {
const t = await getTranslations({ locale, namespace: "404" })
return (
+2 -1
View File
@@ -1,8 +1,9 @@
import { Metadata, Viewport } from "next"
import type { Locale } from "next-intl"
import { getTranslations } from "next-intl/server"
type SeoProps = {
locale: string
locale: Locale
title?: string
description?: string
imageURL?: string
+9 -6
View File
@@ -1,20 +1,23 @@
"use client"
import { GB, NO } from "country-flag-icons/react/3x2"
import { useLocale } from "next-intl"
import { useRouter, usePathname } from "next/navigation"
import { type Locale, useLocale } from "next-intl"
import { usePathname, useRouter } from "@/i18n/navigation"
import * as styles from "./languageSelector.module.css"
const LanguageSelector = () => {
const locale = useLocale()
const router = useRouter()
// Pathname without the locale prefix
const pathname = usePathname()
const changeLocale = (newLocale: "no" | "en") => {
// Remove the current locale from the pathname
const pathWithoutLocale = pathname.replace(/^\/[a-z]{2}/, "")
router.push(`/${newLocale}${pathWithoutLocale}`)
const changeLocale = (newLocale: Locale) => {
// Read at click time, since useSearchParams() would opt pages out of
// static rendering
const { search, hash } = window.location
router.push(`${pathname}${search}${hash}`, { locale: newLocale })
}
return (
+7
View File
@@ -0,0 +1,7 @@
import { createNavigation } from "next-intl/navigation"
import { routing } from "./routing"
// Locale-aware wrappers around Next's navigation APIs
export const { Link, redirect, usePathname, useRouter, getPathname } =
createNavigation(routing)
+5 -4
View File
@@ -2,18 +2,19 @@ import { hasLocale } from "next-intl"
import { getRequestConfig } from "next-intl/server"
import { locale as rootLocale } from "next/root-params"
export const locales = ["no", "en"] as const
const defaultLocale = "no"
import { routing } from "./routing"
export default getRequestConfig(async ({ locale }) => {
// An explicit locale (e.g. getTranslations({ locale })) takes precedence.
// The root param can be any first path segment until the layout rejects it,
// so fall back to the default instead of throwing
const requested = locale ?? (await rootLocale())
const resolved = hasLocale(locales, requested) ? requested : defaultLocale
const resolved = hasLocale(routing.locales, requested)
? requested
: routing.defaultLocale
return {
locale: resolved,
messages: (await import(`../messages/${resolved}.json`)).default,
messages: (await import(`../../messages/${resolved}.json`)).default,
}
})
+14
View File
@@ -0,0 +1,14 @@
import { defineRouting } from "next-intl/routing"
export const routing = defineRouting({
locales: ["no", "en"],
defaultLocale: "no",
localePrefix: "always",
})
// Types getLocale(), useLocale() etc. as "no" | "en" instead of string
declare module "next-intl" {
interface AppConfig {
Locale: (typeof routing.locales)[number]
}
}
+3 -5
View File
@@ -1,10 +1,8 @@
import createMiddleware from "next-intl/middleware"
export default createMiddleware({
locales: ["no", "en"],
defaultLocale: "no",
localePrefix: "always",
})
import { routing } from "./i18n/routing"
export default createMiddleware(routing)
export const config = {
// Match everything except API routes, Next internals and files with an