Add i18n foundation (fa-IR/en-US) and localize landing + auth.

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>
This commit is contained in:
keyhan
2026-06-11 12:05:03 +03:30
parent 2b16846f67
commit 34993d417f
48 changed files with 785 additions and 200 deletions
+53
View File
@@ -0,0 +1,53 @@
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|.*\\..*).*)'],
};