Files
cloud-host/frontend/src/middleware.ts
T
keyhan 585fdbc9b7 fix(frontend): always default first-time visitors to Persian
Drop Accept-Language detection from the locale middleware. A first
visit (no NEXT_LOCALE cookie) now always serves the default locale
(fa-IR) instead of guessing from the browser. The user's last explicit
choice, persisted in the cookie by the language switcher, remains the
sole signal on subsequent visits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 13:27:29 +03:30

86 lines
3.4 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { locales, defaultLocale, isLocale, LOCALE_COOKIE, type Locale } from '@/i18n/config';
// Host-based separation between the marketing landing site and the
// authenticated panel. Configured at runtime (server-side) so the same
// frontend image can serve both hosts. When PANEL_HOST is unset (local dev),
// host routing is disabled and everything is served from a single origin.
const PANEL_HOST = process.env.PANEL_HOST?.trim().toLowerCase();
const LANDING_HOST = process.env.LANDING_HOST?.trim().toLowerCase();
// Locale-stripped path prefixes that belong to the authenticated panel.
const PANEL_PATH_PREFIXES = ['/dashboard', '/login', '/register'];
function stripLocale(pathname: string): string {
for (const l of locales) {
if (pathname === `/${l}`) return '/';
if (pathname.startsWith(`/${l}/`)) return pathname.slice(l.length + 1);
}
return pathname;
}
function isPanelPath(pathname: string): boolean {
const p = stripLocale(pathname);
return PANEL_PATH_PREFIXES.some((pre) => p === pre || p.startsWith(`${pre}/`));
}
function hostOf(request: NextRequest): string {
return (request.headers.get('host') || '').split(':')[0].toLowerCase();
}
function detectLocale(request: NextRequest): Locale {
// Honour the user's last explicit choice, persisted in a cookie by the
// language switcher. On the very first visit (no cookie) we always serve the
// default locale (fa-IR) rather than guessing from Accept-Language, so the
// browser's language preference never overrides the default. From then on,
// the user's last selection is what determines the language.
const cookieLocale = request.cookies.get(LOCALE_COOKIE)?.value;
if (isLocale(cookieLocale)) return cookieLocale;
return defaultLocale;
}
export function middleware(request: NextRequest) {
const { pathname, search } = request.nextUrl;
// 1. Ensure a locale prefix (e.g. /login -> /fa-IR/login). On the redirect
// the host is preserved, so host routing (below) runs on the next request.
const pathnameHasLocale = locales.some(
(locale) => pathname === `/${locale}` || pathname.startsWith(`/${locale}/`),
);
if (!pathnameHasLocale) {
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;
}
// 2. Host-based separation between the landing site and the panel.
// Disabled entirely unless PANEL_HOST is configured (local dev).
if (PANEL_HOST) {
const host = hostOf(request);
const panelPath = isPanelPath(pathname);
// Authenticated routes requested on the landing host -> move to the panel.
if (LANDING_HOST && host === LANDING_HOST && panelPath) {
return NextResponse.redirect(`https://${PANEL_HOST}${pathname}${search}`, 308);
}
// Marketing pages requested on the panel host -> send to the dashboard.
if (host === PANEL_HOST && !panelPath) {
const locale = pathname.split('/')[1]; // path is already locale-prefixed
const target = request.nextUrl.clone();
target.pathname = `/${locale}/dashboard`;
return NextResponse.redirect(target);
}
}
return;
}
export const config = {
// Skip Next internals, the API, and any path containing a dot (static files).
matcher: ['/((?!_next|api|.*\\..*).*)'],
};