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:
@@ -0,0 +1,49 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { peyda } from '../fonts';
|
||||
import '../globals.css';
|
||||
import { Providers } from '@/components/providers';
|
||||
import { I18nProvider } from '@/i18n/I18nProvider';
|
||||
import { getDictionary } from '@/i18n/dictionaries';
|
||||
import { isLocale, dirFor, locales, type Locale } from '@/i18n/config';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });
|
||||
|
||||
export function generateStaticParams() {
|
||||
return locales.map((lang) => ({ lang }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { lang: string };
|
||||
}): Promise<Metadata> {
|
||||
if (!isLocale(params.lang)) return {};
|
||||
const dict = await getDictionary(params.lang);
|
||||
return { title: dict.meta.title, description: dict.meta.description };
|
||||
}
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
params: { lang: string };
|
||||
}) {
|
||||
if (!isLocale(params.lang)) notFound();
|
||||
const locale: Locale = params.lang;
|
||||
const dict = await getDictionary(locale);
|
||||
const dir = dirFor(locale);
|
||||
const fontClass = locale === 'fa-IR' ? 'font-peyda' : 'font-sans';
|
||||
|
||||
return (
|
||||
<html lang={locale} dir={dir}>
|
||||
<body className={`${peyda.variable} ${inter.variable} ${fontClass}`}>
|
||||
<I18nProvider locale={locale} dict={dict}>
|
||||
<Providers>{children}</Providers>
|
||||
</I18nProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +1,31 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuthStore } from '@/lib/store';
|
||||
import { toast } from 'react-toastify';
|
||||
import { LogIn, ArrowLeft } from 'lucide-react';
|
||||
import { AuthShell } from '@/components/auth/AuthShell';
|
||||
import { AuthField } from '@/components/auth/AuthField';
|
||||
import { useT } from '@/i18n/I18nProvider';
|
||||
import { useLocalizedRouter } from '@/i18n/navigation';
|
||||
|
||||
export default function LoginPage() {
|
||||
const tl = useT().auth.login;
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const login = useAuthStore((s) => s.login);
|
||||
const router = useRouter();
|
||||
const router = useLocalizedRouter();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await login(email, password);
|
||||
toast.success('با موفقیت وارد شدی!');
|
||||
toast.success(tl.success);
|
||||
router.push('/dashboard');
|
||||
} catch (err: any) {
|
||||
toast.error(err.response?.data?.message || 'ورود ناموفق بود');
|
||||
toast.error(err.response?.data?.message || tl.error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -32,16 +34,16 @@ export default function LoginPage() {
|
||||
return (
|
||||
<AuthShell
|
||||
icon={<LogIn className="h-7 w-7" />}
|
||||
title="خوش آمدی"
|
||||
subtitle="به حساب ابربان خود وارد شو"
|
||||
altPrompt="هنوز حساب نداری؟"
|
||||
title={tl.title}
|
||||
subtitle={tl.subtitle}
|
||||
altPrompt={tl.altPrompt}
|
||||
altHref="/register"
|
||||
altLabel="ثبتنام کن"
|
||||
altLabel={tl.altLabel}
|
||||
>
|
||||
<form className="space-y-5" onSubmit={handleSubmit}>
|
||||
<AuthField
|
||||
id="email"
|
||||
label="ایمیل"
|
||||
label={tl.email}
|
||||
type="email"
|
||||
required
|
||||
dir="ltr"
|
||||
@@ -53,7 +55,7 @@ export default function LoginPage() {
|
||||
|
||||
<AuthField
|
||||
id="password"
|
||||
label="رمز عبور"
|
||||
label={tl.password}
|
||||
type="password"
|
||||
required
|
||||
dir="ltr"
|
||||
@@ -71,12 +73,12 @@ export default function LoginPage() {
|
||||
{isLoading ? (
|
||||
<>
|
||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||||
در حال ورود…
|
||||
{tl.submitting}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
ورود
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
{tl.submit}
|
||||
<ArrowLeft className="h-4 w-4 ltr:rotate-180" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { LandingPage } from '@/components/landing/LandingPage';
|
||||
|
||||
export default function Home() {
|
||||
return <LandingPage />;
|
||||
}
|
||||
@@ -1,28 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuthStore } from '@/lib/store';
|
||||
import { toast } from 'react-toastify';
|
||||
import { UserPlus, ArrowLeft } from 'lucide-react';
|
||||
import { AuthShell } from '@/components/auth/AuthShell';
|
||||
import { AuthField } from '@/components/auth/AuthField';
|
||||
import { useT } from '@/i18n/I18nProvider';
|
||||
import { useLocalizedRouter } from '@/i18n/navigation';
|
||||
|
||||
export default function RegisterPage() {
|
||||
const tr = useT().auth.register;
|
||||
const [form, setForm] = useState({ email: '', password: '', firstName: '', lastName: '' });
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const register = useAuthStore((s) => s.register);
|
||||
const router = useRouter();
|
||||
const router = useLocalizedRouter();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await register(form);
|
||||
toast.success('حساب با موفقیت ساخته شد!');
|
||||
toast.success(tr.success);
|
||||
router.push('/dashboard');
|
||||
} catch (err: any) {
|
||||
toast.error(err.response?.data?.message || 'ثبتنام ناموفق بود');
|
||||
toast.error(err.response?.data?.message || tr.error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -31,31 +33,31 @@ export default function RegisterPage() {
|
||||
return (
|
||||
<AuthShell
|
||||
icon={<UserPlus className="h-7 w-7" />}
|
||||
title="ساخت حساب"
|
||||
subtitle="در چند دقیقه اولین اپت را منتشر کن"
|
||||
altPrompt="قبلاً حساب ساختهای؟"
|
||||
title={tr.title}
|
||||
subtitle={tr.subtitle}
|
||||
altPrompt={tr.altPrompt}
|
||||
altHref="/login"
|
||||
altLabel="وارد شو"
|
||||
altLabel={tr.altLabel}
|
||||
>
|
||||
<form className="space-y-5" onSubmit={handleSubmit}>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<AuthField
|
||||
id="firstName"
|
||||
label="نام"
|
||||
label={tr.firstName}
|
||||
type="text"
|
||||
required
|
||||
autoComplete="given-name"
|
||||
placeholder="مثلاً علی"
|
||||
placeholder={tr.firstNamePlaceholder}
|
||||
value={form.firstName}
|
||||
onChange={(e) => setForm({ ...form, firstName: e.target.value })}
|
||||
/>
|
||||
<AuthField
|
||||
id="lastName"
|
||||
label="نام خانوادگی"
|
||||
label={tr.lastName}
|
||||
type="text"
|
||||
required
|
||||
autoComplete="family-name"
|
||||
placeholder="مثلاً رضایی"
|
||||
placeholder={tr.lastNamePlaceholder}
|
||||
value={form.lastName}
|
||||
onChange={(e) => setForm({ ...form, lastName: e.target.value })}
|
||||
/>
|
||||
@@ -63,7 +65,7 @@ export default function RegisterPage() {
|
||||
|
||||
<AuthField
|
||||
id="email"
|
||||
label="ایمیل"
|
||||
label={tr.email}
|
||||
type="email"
|
||||
required
|
||||
dir="ltr"
|
||||
@@ -75,13 +77,13 @@ export default function RegisterPage() {
|
||||
|
||||
<AuthField
|
||||
id="password"
|
||||
label="رمز عبور"
|
||||
label={tr.password}
|
||||
type="password"
|
||||
required
|
||||
minLength={8}
|
||||
dir="ltr"
|
||||
autoComplete="new-password"
|
||||
placeholder="حداقل ۸ کاراکتر"
|
||||
placeholder={tr.passwordPlaceholder}
|
||||
value={form.password}
|
||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||
/>
|
||||
@@ -94,12 +96,12 @@ export default function RegisterPage() {
|
||||
{isLoading ? (
|
||||
<>
|
||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||||
در حال ساخت حساب…
|
||||
{tr.submitting}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
ساخت حساب
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
{tr.submit}
|
||||
<ArrowLeft className="h-4 w-4 ltr:rotate-180" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
import './globals.css';
|
||||
import { Providers } from '@/components/providers';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'CloudHost - Self-Service PaaS',
|
||||
description: 'Deploy your applications to Kubernetes with ease',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { peyda } from './fonts';
|
||||
import { LandingPage } from '@/components/landing/LandingPage';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'ابربان | زیرساخت ابری، در کنترل تو',
|
||||
description:
|
||||
'ابربان؛ پلتفرم ابریِ خودسرویس روی کوبرنتیز. اپ خود را در چند ثانیه منتشر کن — دیتابیس مدیریتشده، دامنهٔ اختصاصی، SSL خودکار و لاگ زنده.',
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div dir="rtl" className={`${peyda.variable} font-peyda`}>
|
||||
<LandingPage />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user