34993d417f
Introduce path-prefixed locale routing under app/[lang] with a middleware that detects locale from cookie/Accept-Language (default fa-IR) and redirects. Add fa-IR (source of truth) and en-US dictionaries, a server getDictionary, a client I18nProvider/useT, locale-aware Link + router helpers, and a language switcher. The root [lang] layout sets html lang/dir and the per-locale font (Peyda for fa, Inter for en). Landing sections and the login/register/auth shell now read all copy from the dictionaries; dashboard localization follows in a later commit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
54 lines
1.8 KiB
TypeScript
54 lines
1.8 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { locales, defaultLocale, isLocale, LOCALE_COOKIE, type Locale } from '@/i18n/config';
|
|
|
|
// Parse the Accept-Language header into an ordered list of base language codes.
|
|
function parseAcceptLanguage(header: string | null): string[] {
|
|
if (!header) return [];
|
|
return header
|
|
.split(',')
|
|
.map((part) => {
|
|
const [tag, q] = part.trim().split(';q=');
|
|
return { tag: tag.trim().toLowerCase(), q: q ? parseFloat(q) : 1 };
|
|
})
|
|
.sort((a, b) => b.q - a.q)
|
|
.map((x) => x.tag);
|
|
}
|
|
|
|
function detectLocale(request: NextRequest): Locale {
|
|
// 1. Explicit choice via cookie
|
|
const cookieLocale = request.cookies.get(LOCALE_COOKIE)?.value;
|
|
if (isLocale(cookieLocale)) return cookieLocale;
|
|
|
|
// 2. Browser preference via Accept-Language
|
|
const accepted = parseAcceptLanguage(request.headers.get('accept-language'));
|
|
for (const tag of accepted) {
|
|
const exact = locales.find((l) => l.toLowerCase() === tag);
|
|
if (exact) return exact;
|
|
const base = locales.find((l) => l.toLowerCase().split('-')[0] === tag.split('-')[0]);
|
|
if (base) return base;
|
|
}
|
|
|
|
// 3. Fallback
|
|
return defaultLocale;
|
|
}
|
|
|
|
export function middleware(request: NextRequest) {
|
|
const { pathname } = request.nextUrl;
|
|
|
|
const pathnameHasLocale = locales.some(
|
|
(locale) => pathname === `/${locale}` || pathname.startsWith(`/${locale}/`),
|
|
);
|
|
if (pathnameHasLocale) return;
|
|
|
|
const locale = detectLocale(request);
|
|
request.nextUrl.pathname = `/${locale}${pathname === '/' ? '' : pathname}`;
|
|
const response = NextResponse.redirect(request.nextUrl);
|
|
response.cookies.set(LOCALE_COOKIE, locale, { path: '/', maxAge: 60 * 60 * 24 * 365 });
|
|
return response;
|
|
}
|
|
|
|
export const config = {
|
|
// Skip Next internals, the API, and any path containing a dot (static files).
|
|
matcher: ['/((?!_next|api|.*\\..*).*)'],
|
|
};
|