From f258dfad435951ac2a1d86dc8a0997facf97a66d Mon Sep 17 00:00:00 2001 From: MichalC <103170279+wSBMichalee@users.noreply.github.com> Date: Fri, 18 Sep 2026 00:14:11 +0200 Subject: [PATCH] feat(i18n): add english locale and middleware for browser language detection --- .../(frontend)/[locale]/[[...slug]]/page.tsx | 2 +- src/i18n.config.ts | 1 + src/middleware.ts | 74 +++++++++++++++++++ src/payload-types.ts | 4 +- 4 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 src/middleware.ts diff --git a/src/app/(frontend)/[locale]/[[...slug]]/page.tsx b/src/app/(frontend)/[locale]/[[...slug]]/page.tsx index 3aa9f11..ebec83b 100644 --- a/src/app/(frontend)/[locale]/[[...slug]]/page.tsx +++ b/src/app/(frontend)/[locale]/[[...slug]]/page.tsx @@ -28,7 +28,7 @@ export async function generateMetadata( const doc = await payload.findByID({ collection: ('collection' in route ? route.collection : 'pages') as 'pages', id: route.doc.id as string, - locale: locale as 'all' | 'pl' | 'de', + locale: locale as 'all' | 'pl' | 'de' | 'en', }) // Retrieve meta object injected by @payloadcms/plugin-seo diff --git a/src/i18n.config.ts b/src/i18n.config.ts index 794fa45..2af839f 100644 --- a/src/i18n.config.ts +++ b/src/i18n.config.ts @@ -3,6 +3,7 @@ export const i18n = { locales: [ { code: 'pl', label: 'Polski' }, { code: 'de', label: 'Deutsch' }, + { code: 'en', label: 'English' }, ], } as const; diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..f95b9b6 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,74 @@ +import { NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' +import { i18n } from './i18n.config' + +function getLocale(request: NextRequest): string { + const acceptLanguage = request.headers.get('accept-language') + const defaultLocale = i18n.defaultLocale + const supportedLocales = i18n.locales.map((l) => l.code) + + if (!acceptLanguage) { + return defaultLocale + } + + // Very basic parser for Accept-Language header + // Example header: fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5 + const languages = acceptLanguage + .split(',') + .map((lang) => { + const [code, qValue] = lang.split(';q=') + const quality = qValue ? parseFloat(qValue) : 1.0 + return { code: code.trim().split('-')[0].toLowerCase(), quality } // Extract primary subtag (e.g., 'en-US' -> 'en') + }) + .sort((a, b) => b.quality - a.quality) + + // Find the first matching supported locale + for (const { code } of languages) { + if (supportedLocales.includes(code as any)) { + return code + } + } + + return defaultLocale +} + +export function middleware(request: NextRequest) { + const { pathname } = request.nextUrl + const locales = i18n.locales.map((l) => l.code) + + // Bypass paths that should not be localized + const bypassPaths = [ + '/api', + '/_next', + '/admin', + '/favicon.ico', + '/robots.txt', + '/sitemap.xml', + '/site.webmanifest', + '/icon', // standard icons + ] + + if (bypassPaths.some((path) => pathname.startsWith(path))) { + return NextResponse.next() + } + + // Check if there is any supported locale in the pathname + const pathnameHasLocale = locales.some( + (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}` + ) + + if (pathnameHasLocale) { + return NextResponse.next() + } + + // Redirect if there is no locale + const locale = getLocale(request) + request.nextUrl.pathname = `/${locale}${pathname}` + + return NextResponse.redirect(request.nextUrl) +} + +export const config = { + // Matcher ignoring `/_next/` and `/api/` + matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], +} diff --git a/src/payload-types.ts b/src/payload-types.ts index 1748c14..7aec474 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -95,7 +95,7 @@ export interface Config { db: { defaultIDType: number; }; - fallbackLocale: ('false' | 'none' | 'null') | false | null | ('pl' | 'de') | ('pl' | 'de')[]; + fallbackLocale: ('false' | 'none' | 'null') | false | null | ('pl' | 'de' | 'en') | ('pl' | 'de' | 'en')[]; globals: { company: Company; header: Header; @@ -114,7 +114,7 @@ export interface Config { 'cookie-settings': CookieSettingsSelect | CookieSettingsSelect; notifications: NotificationsSelect | NotificationsSelect; }; - locale: 'pl' | 'de'; + locale: 'pl' | 'de' | 'en'; widgets: { collections: CollectionsWidget; };