feat(i18n): add english locale and middleware for browser language detection

This commit is contained in:
MichalC
2026-09-18 00:14:11 +02:00
parent 1da3850759
commit f258dfad43
4 changed files with 78 additions and 3 deletions
@@ -28,7 +28,7 @@ export async function generateMetadata(
const doc = await payload.findByID({ const doc = await payload.findByID({
collection: ('collection' in route ? route.collection : 'pages') as 'pages', collection: ('collection' in route ? route.collection : 'pages') as 'pages',
id: route.doc.id as string, 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 // Retrieve meta object injected by @payloadcms/plugin-seo
+1
View File
@@ -3,6 +3,7 @@ export const i18n = {
locales: [ locales: [
{ code: 'pl', label: 'Polski' }, { code: 'pl', label: 'Polski' },
{ code: 'de', label: 'Deutsch' }, { code: 'de', label: 'Deutsch' },
{ code: 'en', label: 'English' },
], ],
} as const; } as const;
+74
View File
@@ -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).*)'],
}
+2 -2
View File
@@ -95,7 +95,7 @@ export interface Config {
db: { db: {
defaultIDType: number; 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: { globals: {
company: Company; company: Company;
header: Header; header: Header;
@@ -114,7 +114,7 @@ export interface Config {
'cookie-settings': CookieSettingsSelect<false> | CookieSettingsSelect<true>; 'cookie-settings': CookieSettingsSelect<false> | CookieSettingsSelect<true>;
notifications: NotificationsSelect<false> | NotificationsSelect<true>; notifications: NotificationsSelect<false> | NotificationsSelect<true>;
}; };
locale: 'pl' | 'de'; locale: 'pl' | 'de' | 'en';
widgets: { widgets: {
collections: CollectionsWidget; collections: CollectionsWidget;
}; };