diff --git a/next.config.ts b/next.config.ts index 0fdc31e..66e7bc6 100644 --- a/next.config.ts +++ b/next.config.ts @@ -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: { diff --git a/src/app/[locale]/blog/[slug]/page.tsx b/src/app/[locale]/blog/[slug]/page.tsx index a570b3b..e1593c7 100644 --- a/src/app/[locale]/blog/[slug]/page.tsx +++ b/src/app/[locale]/blog/[slug]/page.tsx @@ -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 }> } diff --git a/src/app/[locale]/event/[slug]/page.tsx b/src/app/[locale]/event/[slug]/page.tsx index dbf3f44..03de1fd 100644 --- a/src/app/[locale]/event/[slug]/page.tsx +++ b/src/app/[locale]/event/[slug]/page.tsx @@ -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 }> } diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index d514566..cabe627 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -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 diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx index 8c89850..7acbd22 100644 --- a/src/app/[locale]/page.tsx +++ b/src/app/[locale]/page.tsx @@ -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 }> } diff --git a/src/components/page-sections/boardMembers.tsx b/src/components/page-sections/boardMembers.tsx index 3ea5fdf..07c767b 100644 --- a/src/components/page-sections/boardMembers.tsx +++ b/src/components/page-sections/boardMembers.tsx @@ -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() diff --git a/src/components/page-sections/notFound.tsx b/src/components/page-sections/notFound.tsx index 7359f80..fb42e9c 100644 --- a/src/components/page-sections/notFound.tsx +++ b/src/components/page-sections/notFound.tsx @@ -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 ( diff --git a/src/components/seo.tsx b/src/components/seo.tsx index c865384..2f30c75 100644 --- a/src/components/seo.tsx +++ b/src/components/seo.tsx @@ -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 diff --git a/src/components/ui/languageSelector.tsx b/src/components/ui/languageSelector.tsx index 6212f27..c9634e6 100644 --- a/src/components/ui/languageSelector.tsx +++ b/src/components/ui/languageSelector.tsx @@ -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 ( diff --git a/src/i18n/navigation.ts b/src/i18n/navigation.ts new file mode 100644 index 0000000..c6642ca --- /dev/null +++ b/src/i18n/navigation.ts @@ -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) diff --git a/src/i18n.ts b/src/i18n/request.ts similarity index 69% rename from src/i18n.ts rename to src/i18n/request.ts index 50b056c..79aa093 100644 --- a/src/i18n.ts +++ b/src/i18n/request.ts @@ -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, } }) diff --git a/src/i18n/routing.ts b/src/i18n/routing.ts new file mode 100644 index 0000000..9cb047d --- /dev/null +++ b/src/i18n/routing.ts @@ -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] + } +} diff --git a/src/middleware.ts b/src/middleware.ts index 2ce9fdc..cd0ca9e 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -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