diff --git a/package-lock.json b/package-lock.json
index 30d1f84..50deb3b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -39,6 +39,7 @@
"genkit": "^1.8.0",
"lucide-react": "^0.475.0",
"next": "15.3.3",
+ "next-international": "^1.3.0",
"next-themes": "^0.3.0",
"patch-package": "^8.0.0",
"react": "^18.3.1",
@@ -6935,6 +6936,12 @@
"node": ">=8"
}
},
+ "node_modules/international-types": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/international-types/-/international-types-0.8.1.tgz",
+ "integrity": "sha512-tajBCAHo4I0LIFlmQ9ZWfjMWVyRffzuvfbXCd6ssFt5u1Zw15DN0UBpVTItXdNa1ls+cpQt3Yw8+TxsfGF8JcA==",
+ "license": "MIT"
+ },
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
@@ -7654,6 +7661,17 @@
}
}
},
+ "node_modules/next-international": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/next-international/-/next-international-1.3.1.tgz",
+ "integrity": "sha512-ydU9jQe+4MohMWltbZae/yuWeKhmp0QKQqJNNi8WCCMwrly03qfMAHw/tWbT2qgAlG++CxF5jMXmGQZgOHeVOw==",
+ "license": "MIT",
+ "dependencies": {
+ "client-only": "^0.0.1",
+ "international-types": "^0.8.1",
+ "server-only": "^0.0.1"
+ }
+ },
"node_modules/next-themes": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz",
@@ -8902,6 +8920,12 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/server-only": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
+ "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
+ "license": "MIT"
+ },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
diff --git a/package.json b/package.json
index 509cf85..7e18003 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
"lucide-react": "^0.475.0",
"next": "15.3.3",
"next-themes": "^0.3.0",
+ "next-international": "^1.3.0",
"patch-package": "^8.0.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
diff --git a/src/app/[locale]/dashboard/layout.tsx b/src/app/[locale]/dashboard/layout.tsx
new file mode 100644
index 0000000..7b47aaf
--- /dev/null
+++ b/src/app/[locale]/dashboard/layout.tsx
@@ -0,0 +1,46 @@
+'use client';
+
+import DashboardSidebar from '@/components/layout/DashboardSidebar';
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { Loader2 } from 'lucide-react';
+import { useCurrentLocale, useI18n } from '@/locales/client';
+
+
+export default function DashboardLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const router = useRouter();
+ const locale = useCurrentLocale();
+ const t = useI18n();
+ const [isAuthenticating, setIsAuthenticating] = useState(true);
+
+ useEffect(() => {
+ const isAuthenticated = localStorage.getItem('isToyShareAuthenticated') === 'true';
+ if (!isAuthenticated) {
+ router.replace(`/${locale}/login?redirect=/${locale}/dashboard`);
+ } else {
+ setIsAuthenticating(false);
+ }
+ }, [router, locale]);
+
+ if (isAuthenticating) {
+ return (
+
+
+
{t('dashboard.layout.loading')}
+
+ );
+ }
+
+ return (
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/src/app/[locale]/dashboard/my-toys/add/page.tsx b/src/app/[locale]/dashboard/my-toys/add/page.tsx
new file mode 100644
index 0000000..900b3ed
--- /dev/null
+++ b/src/app/[locale]/dashboard/my-toys/add/page.tsx
@@ -0,0 +1,11 @@
+import AddToyForm from '@/components/toys/AddToyForm';
+// Note: AddToyForm is a client component and will need to use useI18n for its internal text.
+// This page itself is a server component.
+
+export default function AddNewToyPage() {
+ return (
+
+ );
+}
diff --git a/src/app/[locale]/dashboard/my-toys/edit/[id]/page.tsx b/src/app/[locale]/dashboard/my-toys/edit/[id]/page.tsx
new file mode 100644
index 0000000..dab59f1
--- /dev/null
+++ b/src/app/[locale]/dashboard/my-toys/edit/[id]/page.tsx
@@ -0,0 +1,58 @@
+import AddToyForm from '@/components/toys/AddToyForm';
+import { mockToys } from '@/lib/mockData';
+import type { Toy } from '@/types';
+import { Button } from '@/components/ui/button';
+import Link from 'next/link';
+import { ArrowLeft } from 'lucide-react';
+import { getI18n, getStaticParams as getLocaleStaticParams } from '@/locales/server';
+
+
+interface EditToyPageProps {
+ params: { id: string, locale: string };
+}
+
+async function getToyForEdit(id: string): Promise | undefined> {
+ await new Promise(resolve => setTimeout(resolve, 100));
+ return mockToys.find(toy => toy.id === id);
+}
+
+export default async function EditToyPage({ params }: EditToyPageProps) {
+ const t = await getI18n();
+ const toyData = await getToyForEdit(params.id);
+
+ if (!toyData) {
+ return (
+
+
{t('general.toy_not_found')}
+
{t('general.cannot_edit_nonexistent_toy')}
+
+
+
+ {t('general.back_to_my_toys')}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {t('general.back_to_my_toys')}
+
+ {/* AddToyForm is a client component and will handle its own translations via useI18n */}
+
+
+ );
+}
+
+// For static generation of edit pages if desired, similar to toy details page
+export function generateStaticParams() {
+ const localeParams = getLocaleStaticParams();
+ const toyParams = mockToys.map((toy) => ({ id: toy.id }));
+
+ return localeParams.flatMap(lang =>
+ toyParams.map(toy => ({ ...lang, id: toy.id }))
+ );
+}
diff --git a/src/app/[locale]/dashboard/my-toys/page.tsx b/src/app/[locale]/dashboard/my-toys/page.tsx
new file mode 100644
index 0000000..339ff4a
--- /dev/null
+++ b/src/app/[locale]/dashboard/my-toys/page.tsx
@@ -0,0 +1,110 @@
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import ToyCard from "@/components/toys/ToyCard"; // This component would also need translation if it has text
+import { mockToys } from "@/lib/mockData";
+import Link from "next/link";
+import { PlusCircle, Edit3, Trash2, Eye, ToyBrick as ToyBrickIcon } from "lucide-react"; // Renamed ToyBrick to avoid conflict
+import Image from "next/image";
+import type { Toy } from "@/types";
+import { Badge } from "@/components/ui/badge";
+import { getI18n } from "@/locales/server";
+
+const currentUserId = 'user1';
+const userToys = mockToys.filter(toy => toy.ownerId === currentUserId);
+
+export default async function MyToysPage() {
+ const t = await getI18n();
+ return (
+
+
+
+
{t('dashboard.my_toys.title')}
+
{t('dashboard.my_toys.description')}
+
+
+
+
+ {t('dashboard.my_toys.add_new_toy_button')}
+
+
+
+
+ {userToys.length === 0 ? (
+
+
+
+ {t('dashboard.my_toys.no_toys_title')}
+ {t('dashboard.my_toys.no_toys_description')}
+
+
+
+
+
+ {t('dashboard.my_toys.add_first_toy_button')}
+
+
+
+
+ ) : (
+
+ {userToys.map(toy => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+interface ListedToyItemProps {
+ toy: Toy & {dataAiHint?: string};
+ t: (key: string, params?: Record) => string; // Pass t function for client components or sub-server components
+}
+
+function ListedToyItem({ toy, t }: ListedToyItemProps) {
+ const placeholderHint = toy.dataAiHint || toy.category.toLowerCase() || "toy";
+ return (
+
+
+
+
+
+
+
+
+ {toy.name}
+ {toy.category}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{toy.description}
+
+ {t('toy_details.price')}:
+ {toy.pricePerDay !== undefined ? (toy.pricePerDay > 0 ? `$${toy.pricePerDay}${t('toy_details.price_per_day')}` : t('toy_details.price_free')) : 'Not set'}
+
+
+
+
+ );
+}
diff --git a/src/app/[locale]/dashboard/page.tsx b/src/app/[locale]/dashboard/page.tsx
new file mode 100644
index 0000000..6644332
--- /dev/null
+++ b/src/app/[locale]/dashboard/page.tsx
@@ -0,0 +1,107 @@
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import Link from "next/link";
+import { ToyBrick, PlusCircle, ListOrdered, User, ShoppingBag } from "lucide-react";
+import { getI18n } from "@/locales/server";
+
+const userStats = {
+ listedToys: 3,
+ activeRentals: 1,
+ pendingRequests: 2,
+};
+
+export default async function DashboardOverviewPage() {
+ const t = await getI18n();
+ return (
+
+
+
+ {t('dashboard.overview.welcome')}
+ {t('dashboard.overview.description')}
+
+
+ }
+ actionLink="/dashboard/my-toys"
+ actionLabel={t('dashboard.overview.view_my_toys')}
+ />
+ }
+ actionLink="/dashboard/rentals"
+ actionLabel={t('dashboard.overview.view_my_rentals')}
+ />
+ }
+ actionLink="/dashboard/requests"
+ actionLabel={t('dashboard.overview.manage_requests')}
+ />
+
+
+
+
+
+
+ {t('dashboard.overview.quick_actions')}
+
+
+
+
+ {t('dashboard.overview.add_new_toy_button')}
+
+
+
+
+ {t('dashboard.overview.update_profile_button')}
+
+
+
+
+
+
+
+ {t('dashboard.overview.tips_for_sharers')}
+
+
+
+ {t('dashboard.overview.tip1')}
+ {t('dashboard.overview.tip2')}
+ {t('dashboard.overview.tip3')}
+ {t('dashboard.overview.tip4')}
+
+
+
+
+
+ );
+}
+
+interface DashboardStatCardProps {
+ title: string;
+ value: string;
+ icon: React.ReactNode;
+ actionLink: string;
+ actionLabel: string;
+}
+
+function DashboardStatCard({ title, value, icon, actionLink, actionLabel }: DashboardStatCardProps) {
+ return (
+
+
+ {title}
+ {icon}
+
+
+ {value}
+
+ {actionLabel} →
+
+
+
+ );
+}
diff --git a/src/app/[locale]/dashboard/profile/page.tsx b/src/app/[locale]/dashboard/profile/page.tsx
new file mode 100644
index 0000000..0e43ff1
--- /dev/null
+++ b/src/app/[locale]/dashboard/profile/page.tsx
@@ -0,0 +1,160 @@
+'use client';
+
+import { useState } from 'react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { Save } from 'lucide-react';
+import { useToast } from '@/hooks/use-toast';
+import { Textarea } from '@/components/ui/textarea';
+import { useI18n } from '@/locales/client';
+
+const mockUserProfile = {
+ name: 'Alice Wonderland',
+ email: 'alice@example.com',
+ avatarUrl: 'https://placehold.co/100x100.png?text=AW',
+ bio: "Lover of imaginative play and sharing joy. I have a collection of classic storybooks and dress-up costumes.",
+ phone: '555-123-4567',
+ location: 'Springfield Gardens, USA',
+};
+
+export default function ProfilePage() {
+ const { toast } = useToast();
+ const t = useI18n();
+
+ const [name, setName] = useState(mockUserProfile.name);
+ const [email, setEmail] = useState(mockUserProfile.email);
+ const [avatarUrl, setAvatarUrl] = useState(mockUserProfile.avatarUrl);
+ const [bio, setBio] = useState(mockUserProfile.bio);
+ const [phone, setPhone] = useState(mockUserProfile.phone);
+ const [location, setLocation] = useState(mockUserProfile.location);
+ const [currentPassword, setCurrentPassword] = useState('');
+ const [newPassword, setNewPassword] = useState('');
+ const [confirmNewPassword, setConfirmNewPassword] = useState('');
+
+ const [isLoading, setIsLoading] = useState(false);
+ const [isPasswordLoading, setIsPasswordLoading] = useState(false);
+
+ const handleProfileUpdate = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsLoading(true);
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ console.log("Updating profile:", { name, avatarUrl, bio, phone, location });
+ toast({ title: "Profile Updated", description: "Your profile information has been saved." }); // Translate
+ setIsLoading(false);
+ };
+
+ const handlePasswordChange = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (newPassword !== confirmNewPassword) {
+ toast({ title: "Password Mismatch", description: "New passwords do not match.", variant: "destructive" }); // Translate
+ return;
+ }
+ if (newPassword.length < 6) {
+ toast({ title: "Password Too Short", description: "Password must be at least 6 characters.", variant: "destructive" }); // Translate
+ return;
+ }
+ setIsPasswordLoading(true);
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ console.log("Changing password");
+ toast({ title: "Password Updated", description: "Your password has been changed successfully." }); // Translate
+ setCurrentPassword('');
+ setNewPassword('');
+ setConfirmNewPassword('');
+ setIsPasswordLoading(false);
+ };
+
+ return (
+
+
+
{t('dashboard.profile.title')}
+
{t('dashboard.profile.description')}
+
+
+
+
+ {t('dashboard.profile.personal_info_title')}
+ {t('dashboard.profile.personal_info_description')}
+
+
+
+
+
+
+ {t('dashboard.profile.change_password_title')}
+ {t('dashboard.profile.change_password_description')}
+
+
+
+
+ {t('dashboard.profile.current_password_label')}
+ setCurrentPassword(e.target.value)} required disabled={isPasswordLoading} />
+
+
+ {t('dashboard.profile.new_password_label')}
+ setNewPassword(e.target.value)} required disabled={isPasswordLoading} />
+
+
+ {t('dashboard.profile.confirm_new_password_label')}
+ setConfirmNewPassword(e.target.value)} required disabled={isPasswordLoading} />
+
+
+
+
+
+ {isPasswordLoading ? t('dashboard.profile.updating_password_button') : t('dashboard.profile.update_password_button')}
+
+
+
+
+
+ );
+}
diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx
new file mode 100644
index 0000000..9445d76
--- /dev/null
+++ b/src/app/[locale]/layout.tsx
@@ -0,0 +1,67 @@
+import type { Metadata } from 'next';
+import { PT_Sans } from 'next/font/google';
+import '../globals.css'; // Adjusted path for globals.css
+import { Toaster } from "@/components/ui/toaster";
+import Header from '@/components/layout/Header';
+import Footer from '@/components/layout/Footer';
+import { cn } from '@/lib/utils';
+import { ThemeProvider } from '@/components/theme-provider';
+import { I18nProviderClient } from '@/locales/client';
+import { getStaticParams } from '@/locales/server';
+
+const ptSans = PT_Sans({
+ subsets: ['latin'],
+ weight: ['400', '700'],
+ variable: '--font-body',
+});
+
+export const metadata: Metadata = {
+ title: 'ToyShare - Share and Rent Toys',
+ description: 'A friendly platform to share and rent toys in your community.',
+ icons: {
+ icon: '/favicon.ico',
+ }
+};
+
+// Needed for static generation with dynamic [locale] segment
+export function generateStaticParams() {
+ return getStaticParams();
+}
+
+export default function RootLayout({
+ children,
+ params: { locale }
+}: Readonly<{
+ children: React.ReactNode;
+ params: { locale: string };
+}>) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/[locale]/login/page.tsx b/src/app/[locale]/login/page.tsx
new file mode 100644
index 0000000..0f3e1c5
--- /dev/null
+++ b/src/app/[locale]/login/page.tsx
@@ -0,0 +1,130 @@
+'use client';
+
+import { useState } from 'react';
+import { useRouter, useSearchParams } from 'next/navigation';
+import Link from 'next/link';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { LogIn } from 'lucide-react';
+import { useToast } from "@/hooks/use-toast";
+import { useI18n, useCurrentLocale } from '@/locales/client';
+
+export default function LoginPage() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const redirectPath = searchParams.get('redirect');
+ const { toast } = useToast();
+ const t = useI18n();
+ const locale = useCurrentLocale();
+
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsLoading(true);
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ if (email === "user@example.com" && password === "password") {
+ localStorage.setItem('isToyShareAuthenticated', 'true');
+ toast({
+ title: "Login Successful", // Consider translating toast messages too
+ description: "Welcome back!",
+ });
+ // If redirectPath includes locale, use it. Otherwise, prefix with current locale.
+ const finalRedirect = redirectPath ? (redirectPath.startsWith(`/${locale}`) ? redirectPath : `/${locale}${redirectPath}`) : `/${locale}/dashboard`;
+ router.push(finalRedirect);
+ } else {
+ toast({
+ title: "Login Failed",
+ description: "Invalid email or password. (Hint: user@example.com / password)",
+ variant: "destructive",
+ });
+ }
+ setIsLoading(false);
+ };
+
+ const renderForgotPasswordLink = () => {
+ const parts = t('login.forgot_password').split(/ |<\/link>/);
+ return (
+ <>
+ {parts[0]}
+ {parts[1]}
+ {parts[2]}
+ >
+ );
+ }
+
+ const renderNoAccountLink = () => {
+ const parts = t('login.no_account').split(/ |<\/link>/);
+ return (
+ <>
+ {parts[0]}
+ {parts[1]}
+ {parts[2]}
+ >
+ );
+ }
+
+
+ return (
+
+
+
+
+
+
+ {t('login.welcome_back')}
+ {t('login.description')}
+
+
+
+
+ {t('login.email_label')}
+ setEmail(e.target.value)}
+ required
+ disabled={isLoading}
+ />
+
+
+ {t('login.password_label')}
+ setPassword(e.target.value)}
+ required
+ disabled={isLoading}
+ />
+
+
+ {isLoading ? t('login.loading_button') : t('login.submit_button')}
+
+
+
+
+
+ {renderForgotPasswordLink()}
+
+
+
+ {renderNoAccountLink()}
+
+
+
+
+ );
+}
+
+function Separator() {
+ return
;
+}
diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx
new file mode 100644
index 0000000..2b474a4
--- /dev/null
+++ b/src/app/[locale]/page.tsx
@@ -0,0 +1,43 @@
+import ToyList from '@/components/toys/ToyList';
+import { mockToys } from '@/lib/mockData';
+import { Button } from '@/components/ui/button';
+import Link from 'next/link';
+import { PlusCircle } from 'lucide-react';
+import { getI18n } from '@/locales/server';
+
+export default async function HomePage() {
+ const t = await getI18n();
+
+ return (
+
+
+
+ {t('home.welcome')}
+
+
+ {t('home.discover')}
+
+
+
+
+ {t('home.explore_toys')}
+
+
+
+
+
+ {t('home.share_your_toy')}
+
+
+
+
+
+
+
+ {t('home.available_toys')}
+
+ ({...toy, dataAiHint: toy.category.toLowerCase()}))} />
+
+
+ );
+}
diff --git a/src/app/[locale]/register/page.tsx b/src/app/[locale]/register/page.tsx
new file mode 100644
index 0000000..cfe50a9
--- /dev/null
+++ b/src/app/[locale]/register/page.tsx
@@ -0,0 +1,135 @@
+'use client';
+
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
+import Link from 'next/link';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { UserPlus } from 'lucide-react';
+import { useToast } from "@/hooks/use-toast";
+import { useI18n, useCurrentLocale } from '@/locales/client';
+
+export default function RegisterPage() {
+ const router = useRouter();
+ const { toast } = useToast();
+ const t = useI18n();
+ const locale = useCurrentLocale();
+
+ const [name, setName] = useState('');
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsLoading(true);
+
+ if (password !== confirmPassword) {
+ toast({
+ title: "Registration Error", // Translate
+ description: "Passwords do not match.", // Translate
+ variant: "destructive",
+ });
+ setIsLoading(false);
+ return;
+ }
+
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ localStorage.setItem('isToyShareAuthenticated', 'true');
+ toast({
+ title: "Registration Successful", // Translate
+ description: "Your account has been created. Welcome!", // Translate
+ });
+ router.push(`/${locale}/dashboard`);
+ setIsLoading(false);
+ };
+
+ const renderHasAccountLink = () => {
+ const parts = t('register.has_account').split(/ |<\/link>/);
+ return (
+ <>
+ {parts[0]}
+ {parts[1]}
+ {parts[2]}
+ >
+ );
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/[locale]/toys/[id]/page.tsx b/src/app/[locale]/toys/[id]/page.tsx
new file mode 100644
index 0000000..f32e001
--- /dev/null
+++ b/src/app/[locale]/toys/[id]/page.tsx
@@ -0,0 +1,138 @@
+import Image from 'next/image';
+import { mockToys } from '@/lib/mockData';
+import type { Toy } from '@/types';
+import { Button } from '@/components/ui/button';
+import AvailabilityCalendar from '@/components/toys/AvailabilityCalendar';
+import { ArrowLeft, CalendarDays, DollarSign, MapPin, ShoppingBag, UserCircle2 } from 'lucide-react';
+import Link from 'next/link';
+import { Badge } from '@/components/ui/badge';
+import { Separator } from '@/components/ui/separator';
+import { getI18n, getStaticParams as getLocaleStaticParams } from '@/locales/server'; // Renamed to avoid conflict
+
+interface ToyPageProps {
+ params: { id: string, locale: string };
+}
+
+async function getToyById(id: string): Promise {
+ await new Promise(resolve => setTimeout(resolve, 200));
+ return mockToys.find(toy => toy.id === id);
+}
+
+export default async function ToyPage({ params }: ToyPageProps) {
+ const t = await getI18n();
+ const toy = await getToyById(params.id);
+
+ if (!toy) {
+ return (
+
+
{t('toy_details.toy_not_found_title')}
+
{t('toy_details.toy_not_found_description')}
+
+
+
+ {t('toy_details.back_to_toys')}
+
+
+
+ );
+ }
+
+ const placeholderHint = toy.category.toLowerCase() || "toy detail";
+
+ return (
+
+
+
+ {t('toy_details.back_to_toys')}
+
+
+
+
+
+
+
+ {toy.images.length > 1 && (
+
+ {toy.images.slice(1, 4).map((img, index) => (
+
+
+
+ ))}
+
+ )}
+
+
+
+
{toy.category}
+
{toy.name}
+
+
+
+
+
+
+
+
+
+ {t('toy_details.owner')}:
+ {toy.ownerName}
+
+
+ {toy.location && (
+
+
+
+ {t('toy_details.location')}:
+ {toy.location}
+
+
+ )}
+ {toy.pricePerDay !== undefined && (
+
+
+
+ {t('toy_details.price')}:
+
+ {toy.pricePerDay > 0 ? `$${toy.pricePerDay}${t('toy_details.price_per_day')}` : t('toy_details.price_free')}
+
+
+
+ )}
+
+
+
+
+
+
+
+ {t('toy_details.request_to_rent')}
+
+
+
+
+ );
+}
+
+export function generateStaticParams() {
+ const localeParams = getLocaleStaticParams(); // e.g. [{ locale: 'en' }, { locale: 'zh-TW' }]
+ const toyParams = mockToys.map((toy) => ({ id: toy.id }));
+
+ return localeParams.flatMap(lang =>
+ toyParams.map(toy => ({ ...lang, id: toy.id }))
+ );
+}
diff --git a/src/components/layout/DashboardSidebar.tsx b/src/components/layout/DashboardSidebar.tsx
index 35f220a..c68273b 100644
--- a/src/components/layout/DashboardSidebar.tsx
+++ b/src/components/layout/DashboardSidebar.tsx
@@ -2,37 +2,49 @@
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
-import { Home, ToyBrick, PlusCircle, ListOrdered, User, LogOut, Settings, ShoppingBag } from 'lucide-react';
+import { Home, ToyBrick, PlusCircle, ListOrdered, Settings, ShoppingBag, LogOutIcon } from 'lucide-react'; // Changed LogOut to LogOutIcon
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { Separator } from '@/components/ui/separator';
import { useToast } from "@/hooks/use-toast";
-
-const sidebarNavItems = [
- { href: '/dashboard', label: 'Overview', icon: Home },
- { href: '/dashboard/my-toys', label: 'My Toys', icon: ToyBrick },
- { href: '/dashboard/my-toys/add', label: 'Add New Toy', icon: PlusCircle },
- { href: '/dashboard/rentals', label: 'My Rentals', icon: ShoppingBag }, // Toys I'm renting
- { href: '/dashboard/requests', label: 'Rental Requests', icon: ListOrdered }, // Requests for my toys
-];
-
-const accountNavItems = [
- { href: '/dashboard/profile', label: 'Profile Settings', icon: Settings },
-];
+import { useI18n, useCurrentLocale } from '@/locales/client';
export default function DashboardSidebar() {
const pathname = usePathname();
const router = useRouter();
+ const t = useI18n();
+ const locale = useCurrentLocale();
const { toast } = useToast();
+ const sidebarNavItems = [
+ { href: '/dashboard', label: t('dashboard.sidebar.overview'), icon: Home },
+ { href: '/dashboard/my-toys', label: t('dashboard.sidebar.my_toys'), icon: ToyBrick },
+ { href: '/dashboard/my-toys/add', label: t('dashboard.sidebar.add_new_toy'), icon: PlusCircle },
+ { href: '/dashboard/rentals', label: t('dashboard.sidebar.my_rentals'), icon: ShoppingBag },
+ { href: '/dashboard/requests', label: t('dashboard.sidebar.rental_requests'), icon: ListOrdered },
+ ];
+
+ const accountNavItems = [
+ { href: '/dashboard/profile', label: t('dashboard.sidebar.profile_settings'), icon: Settings },
+ ];
+
const handleLogout = () => {
- localStorage.removeItem('isToyShareAuthenticated'); // Mock logout
- toast({ description: "You have been logged out." });
- router.push('/');
+ localStorage.removeItem('isToyShareAuthenticated');
+ toast({ description: "You have been logged out." }); // Translate if needed
+ router.push(`/${locale}/`); // Redirect to localized home page
};
- const NavLink = ({ href, label, icon: Icon }: typeof sidebarNavItems[0]) => {
- const isActive = pathname === href || (href !== '/dashboard' && pathname.startsWith(href));
+ const NavLink = ({ href, label, icon: Icon }: typeof sidebarNavItems[0] & {icon: React.ElementType}) => {
+ // For active link comparison, remove locale prefix from pathname
+ const cleanPathname = pathname.startsWith(`/${locale}`)
+ ? pathname.substring(`/${locale}`.length) || '/'
+ : pathname;
+ const cleanHref = href.startsWith(`/${locale}`)
+ ? href.substring(`/${locale}`.length) || '/'
+ : href;
+
+ const isActive = cleanPathname === cleanHref || (cleanHref !== '/dashboard' && cleanPathname.startsWith(cleanHref));
+
return (
ToyShare
- User Dashboard
+ {t('dashboard.sidebar.user_dashboard')}
- Toy Management
+ {t('dashboard.sidebar.toy_management')}
{sidebarNavItems.map((item) => (
))}
- Account
+ {t('dashboard.sidebar.account')}
{accountNavItems.map((item) => (
))}
@@ -74,8 +86,8 @@ export default function DashboardSidebar() {
-
- Logout
+ {/* Changed to LogOutIcon */}
+ {t('dashboard.sidebar.logout')}
diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx
index 960cd93..92e1a7c 100644
--- a/src/components/layout/Footer.tsx
+++ b/src/components/layout/Footer.tsx
@@ -1,12 +1,18 @@
-export default function Footer() {
+import { getCurrentLocale, getI18n } from '@/locales/server';
+
+export default async function Footer() {
+ const year = new Date().getFullYear();
+ const t = await getI18n();
+ // const locale = getCurrentLocale(); // Example if needed
+
return (
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx
index db8718f..fec8630 100644
--- a/src/components/layout/Header.tsx
+++ b/src/components/layout/Header.tsx
@@ -14,22 +14,30 @@ import {
} from "@/components/ui/dropdown-menu";
import { useState, useEffect } from 'react';
import { ThemeToggleButton } from '@/components/ui/theme-toggle';
+import LanguageSwitcher from './LanguageSwitcher';
+import { useI18n, useCurrentLocale } from '@/locales/client';
export default function Header() {
const pathname = usePathname();
+ const t = useI18n();
+ const locale = useCurrentLocale();
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
const authStatus = localStorage.getItem('isToyShareAuthenticated');
setIsAuthenticated(authStatus === 'true');
- }, [pathname]);
+ }, [pathname]); // Listen to pathname changes to re-check auth if needed after navigation
const handleLogout = () => {
localStorage.removeItem('isToyShareAuthenticated');
setIsAuthenticated(false);
- // router.push('/'); // Consider redirecting if not already handled by auth checks elsewhere
+ // No need to router.push here, links will use current locale
};
+ // Helper to remove locale prefix for path comparison
+ const cleanPathname = pathname.startsWith(`/${locale}`)
+ ? pathname.substring(`/${locale}`.length) || '/'
+ : pathname;
return (
@@ -39,11 +47,11 @@ export default function Header() {
ToyShare
-
-
+
+
-
- Browse Toys
+
+ {t('header.browse_toys')}
{isAuthenticated ? (
@@ -54,45 +62,47 @@ export default function Header() {
- My Account
+ {t('header.my_account')}
- Dashboard
+ {t('header.dashboard')}
- Profile
+ {t('header.profile')}
-
- Logout
+ {/* Using LogIn icon for logout action for consistency */}
+ {t('header.logout')}
) : (
<>
-
+
- Login
+ {t('header.login')}
- Register
+ {t('header.register')}
>
)}
+
+ {/* Mobile menu trigger could be added here if needed */}
diff --git a/src/components/layout/LanguageSwitcher.tsx b/src/components/layout/LanguageSwitcher.tsx
new file mode 100644
index 0000000..8a085de
--- /dev/null
+++ b/src/components/layout/LanguageSwitcher.tsx
@@ -0,0 +1,46 @@
+'use client';
+
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { useChangeLocale, useCurrentLocale, useI18n } from "@/locales/client";
+import { Languages } from "lucide-react";
+
+export default function LanguageSwitcher() {
+ const changeLocale = useChangeLocale();
+ const currentLocale = useCurrentLocale();
+ const t = useI18n();
+
+ return (
+
+
+
+
+ {t('lang.select_language')}
+
+
+
+ {t('lang.select_language')}
+
+ changeLocale('en')}
+ disabled={currentLocale === 'en'}
+ >
+ {t('lang.english')}
+
+ changeLocale('zh-TW')}
+ disabled={currentLocale === 'zh-TW'}
+ >
+ {t('lang.traditional_chinese')}
+
+
+
+ );
+}
diff --git a/src/components/toys/AddToyForm.tsx b/src/components/toys/AddToyForm.tsx
index d753d7b..2e4ed35 100644
--- a/src/components/toys/AddToyForm.tsx
+++ b/src/components/toys/AddToyForm.tsx
@@ -10,28 +10,31 @@ import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import { useToast } from '@/hooks/use-toast';
-import { ToyBrick, UploadCloud, Save, PlusCircle, Trash2 } from 'lucide-react';
+import { ToyBrick, Save, PlusCircle, Trash2 } from 'lucide-react';
import type { Toy } from '@/types';
+import { useI18n, useCurrentLocale } from '@/locales/client';
const toyCategories = ["Educational", "Vehicles", "Electronics", "Plush Toys", "Musical", "Outdoor", "Board Games", "Action Figures", "Dolls", "Puzzles", "Arts & Crafts", "Building Blocks"];
const daysOfWeek = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] as const;
interface AddToyFormProps {
- initialData?: Partial; // For editing existing toys
+ initialData?: Partial;
isEditMode?: boolean;
}
export default function AddToyForm({ initialData, isEditMode = false }: AddToyFormProps) {
const router = useRouter();
const { toast } = useToast();
+ const t = useI18n();
+ const locale = useCurrentLocale();
const [name, setName] = useState(initialData?.name || '');
const [description, setDescription] = useState(initialData?.description || '');
const [category, setCategory] = useState(initialData?.category || '');
const [pricePerDay, setPricePerDay] = useState(initialData?.pricePerDay?.toString() || '0');
const [location, setLocation] = useState(initialData?.location || '');
- const [images, setImages] = useState(initialData?.images || ['']); // Store image URLs
+ const [images, setImages] = useState(initialData?.images || ['']);
const [availability, setAvailability] = useState(
initialData?.availability || {
monday: true, tuesday: true, wednesday: true, thursday: true, friday: true, saturday: false, sunday: false
@@ -50,7 +53,7 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
if (images.length > 1) {
setImages(images.filter((_, i) => i !== index));
} else {
- setImages(['']); // Keep at least one field, but clear it
+ setImages(['']);
}
};
@@ -62,9 +65,8 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
e.preventDefault();
setIsLoading(true);
- // Form validation (basic example)
if (!name || !description || !category) {
- toast({ title: "Missing Fields", description: "Please fill in all required fields.", variant: "destructive" });
+ toast({ title: "Missing Fields", description: "Please fill in all required fields.", variant: "destructive" }); // Translate
setIsLoading(false);
return;
}
@@ -73,21 +75,18 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
name, description, category,
pricePerDay: parseFloat(pricePerDay) || 0,
location,
- images: images.filter(img => img.trim() !== ''), // Filter out empty image URLs
+ images: images.filter(img => img.trim() !== ''),
availability,
- // ownerId and id would be set by backend
};
- // Mock API call
console.log("Submitting toy data:", toyData);
await new Promise(resolve => setTimeout(resolve, 1500));
toast({
- title: isEditMode ? "Toy Updated!" : "Toy Added!",
- description: `${name} has been successfully ${isEditMode ? 'updated' : 'listed'}.`,
+ title: isEditMode ? "Toy Updated!" : "Toy Added!", // Translate
+ description: `${name} has been successfully ${isEditMode ? 'updated' : 'listed'}.`, // Translate
});
- router.push('/dashboard/my-toys'); // Redirect after success
- // Optionally, could clear form or reset state here
+ router.push(`/${locale}/dashboard/my-toys`);
setIsLoading(false);
};
@@ -97,31 +96,28 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
- {isEditMode ? 'Edit Your Toy' : 'Share a New Toy'}
+ {isEditMode ? t('add_toy_form.edit_title') : t('add_toy_form.add_title')}
- {isEditMode ? 'Update the details of your toy listing.' : 'Fill in the details below to list your toy for others to enjoy.'}
+ {isEditMode ? t('add_toy_form.edit_description') : t('add_toy_form.add_description')}
- {/* Toy Name */}
- Toy Name
+ {t('add_toy_form.name_label')}
setName(e.target.value)} placeholder="e.g., Red Racing Car" required disabled={isLoading} />
- {/* Description */}
- Description
+ {t('add_toy_form.description_label')}
setDescription(e.target.value)} placeholder="Describe your toy, its condition, and any accessories." required disabled={isLoading} rows={4} />
- {/* Category */}
- Category
+ {t('add_toy_form.category_label')}
-
+
{toyCategories.map(cat => {cat} )}
@@ -129,22 +125,19 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
- {/* Price per Day */}
- Rental Price per Day ($)
+ {t('add_toy_form.price_label')}
setPricePerDay(e.target.value)} placeholder="0 for free" min="0" step="0.50" disabled={isLoading} />
- {/* Location */}
- Location (Optional)
+ {t('add_toy_form.location_label')}
setLocation(e.target.value)} placeholder="e.g., Springfield Park Area" disabled={isLoading} />
- {/* Image URLs */}
-
Toy Images (URLs)
-
Enter direct URLs to your toy images. Add up to 5 images.
+
{t('add_toy_form.images_label')}
+
{t('add_toy_form.images_description')}
{images.map((imgUrl, index) => (
-
Add Another Image
+
{t('add_toy_form.add_image_button')}
)}
- {/* Availability */}
-
Weekly Availability
+
{t('add_toy_form.availability_label')}
{daysOfWeek.map(day => (
@@ -188,10 +180,10 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
- {isLoading ? (isEditMode ? 'Saving Changes...' : 'Listing Toy...') : (
+ {isLoading ? (isEditMode ? t('add_toy_form.saving_button') : t('add_toy_form.listing_button')) : (
<>
- {isEditMode ? 'Save Changes' : 'List My Toy'}
+ {isEditMode ? t('add_toy_form.save_button') : t('add_toy_form.list_button')}
>
)}
@@ -201,16 +193,3 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
);
}
-
-// Icon for file upload, not used in URL version but good for future
-function FileUploadIcon() {
- return (
-
-
-
- Click to upload or drag and drop
-
-
PNG, JPG, GIF up to 10MB
-
- );
-}
diff --git a/src/locales/client.ts b/src/locales/client.ts
new file mode 100644
index 0000000..85c9e3d
--- /dev/null
+++ b/src/locales/client.ts
@@ -0,0 +1,14 @@
+'use client';
+
+import { createI18nClient } from 'next-international/client';
+
+export const {
+ useI18n,
+ useScopedI18n,
+ I18nProviderClient,
+ useChangeLocale,
+ useCurrentLocale
+} = createI18nClient({
+ en: () => import('./en'),
+ 'zh-TW': () => import('./zh-TW'),
+});
diff --git a/src/locales/en.ts b/src/locales/en.ts
new file mode 100644
index 0000000..60c3095
--- /dev/null
+++ b/src/locales/en.ts
@@ -0,0 +1,119 @@
+export default {
+ 'header.browse_toys': 'Browse Toys',
+ 'header.login': 'Login',
+ 'header.register': 'Register',
+ 'header.my_account': 'My Account',
+ 'header.dashboard': 'Dashboard',
+ 'header.profile': 'Profile',
+ 'header.logout': 'Logout',
+ 'footer.copy': '© {year} ToyShare. All rights reserved.',
+ 'footer.tagline': 'Sharing happiness, one toy at a time.',
+ 'home.welcome': 'Welcome to ToyShare!',
+ 'home.discover': 'Discover a world of fun! Share your beloved toys or find new adventures by renting from our friendly community.',
+ 'home.explore_toys': 'Explore Toys',
+ 'home.share_your_toy': 'Share Your Toy',
+ 'home.available_toys': 'Available Toys',
+ 'login.welcome_back': 'Welcome Back!',
+ 'login.description': 'Log in to your ToyShare account to continue.',
+ 'login.email_label': 'Email Address',
+ 'login.password_label': 'Password',
+ 'login.submit_button': 'Log In',
+ 'login.loading_button': 'Logging in...',
+ 'login.forgot_password': 'Forgot your password? Reset it here.',
+ 'login.no_account': "Don't have an account? Sign up now",
+ 'register.create_account': 'Create Your Account',
+ 'register.description': 'Join ToyShare and start sharing the fun!',
+ 'register.name_label': 'Full Name',
+ 'register.confirm_password_label': 'Confirm Password',
+ 'register.submit_button': 'Create Account',
+ 'register.loading_button': 'Registering...',
+ 'register.has_account': 'Already have an account? Log in',
+ 'toy_details.back_to_toys': 'Back to All Toys',
+ 'toy_details.toy_not_found_title': 'Toy Not Found',
+ 'toy_details.toy_not_found_description': 'Sorry, the toy you are looking for does not exist or has been removed.',
+ 'toy_details.owner': 'Owner',
+ 'toy_details.location': 'Location',
+ 'toy_details.price': 'Price',
+ 'toy_details.price_free': 'Free',
+ 'toy_details.price_per_day': '/day',
+ 'toy_details.request_to_rent': 'Request to Rent',
+ 'dashboard.layout.loading': 'Loading Dashboard...',
+ 'dashboard.sidebar.user_dashboard': 'User Dashboard',
+ 'dashboard.sidebar.toy_management': 'Toy Management',
+ 'dashboard.sidebar.overview': 'Overview',
+ 'dashboard.sidebar.my_toys': 'My Toys',
+ 'dashboard.sidebar.add_new_toy': 'Add New Toy',
+ 'dashboard.sidebar.my_rentals': 'My Rentals',
+ 'dashboard.sidebar.rental_requests': 'Rental Requests',
+ 'dashboard.sidebar.account': 'Account',
+ 'dashboard.sidebar.profile_settings': 'Profile Settings',
+ 'dashboard.sidebar.logout': 'Logout',
+ 'dashboard.overview.welcome': 'Welcome to Your Dashboard!',
+ 'dashboard.overview.description': 'Manage your toys, rentals, and account settings all in one place.',
+ 'dashboard.overview.my_listed_toys': 'My Listed Toys',
+ 'dashboard.overview.view_my_toys': 'View My Toys',
+ 'dashboard.overview.toys_i_renting': "Toys I'm Renting",
+ 'dashboard.overview.view_my_rentals': 'View My Rentals',
+ 'dashboard.overview.pending_requests': 'Pending Requests',
+ 'dashboard.overview.manage_requests': 'Manage Requests',
+ 'dashboard.overview.quick_actions': 'Quick Actions',
+ 'dashboard.overview.add_new_toy_button': 'Add a New Toy',
+ 'dashboard.overview.update_profile_button': 'Update Profile',
+ 'dashboard.overview.tips_for_sharers': 'Tips for Sharers',
+ 'dashboard.overview.tip1': 'Take clear photos of your toys from multiple angles.',
+ 'dashboard.overview.tip2': 'Write detailed and accurate descriptions.',
+ 'dashboard.overview.tip3': 'Keep your availability calendar up-to-date.',
+ 'dashboard.overview.tip4': 'Respond promptly to rental requests.',
+ 'dashboard.my_toys.title': 'My Listed Toys',
+ 'dashboard.my_toys.description': "Manage the toys you've shared with the community.",
+ 'dashboard.my_toys.add_new_toy_button': 'Add New Toy',
+ 'dashboard.my_toys.no_toys_title': 'No Toys Listed Yet',
+ 'dashboard.my_toys.no_toys_description': 'Share your first toy and spread the joy!',
+ 'dashboard.my_toys.add_first_toy_button': 'Add Your First Toy',
+ 'dashboard.my_toys.view_toy_action': 'View Toy',
+ 'dashboard.my_toys.edit_toy_action': 'Edit Toy',
+ 'dashboard.my_toys.delete_toy_action': 'Delete Toy',
+ 'dashboard.profile.title': 'Profile Settings',
+ 'dashboard.profile.description': 'Manage your personal information and account settings.',
+ 'dashboard.profile.personal_info_title': 'Personal Information',
+ 'dashboard.profile.personal_info_description': 'Update your publicly visible profile information.',
+ 'dashboard.profile.avatar_url_label': 'Avatar URL',
+ 'dashboard.profile.full_name_label': 'Full Name',
+ 'dashboard.profile.email_label': 'Email Address (Read-only)',
+ 'dashboard.profile.bio_label': 'Bio',
+ 'dashboard.profile.phone_label': 'Phone Number',
+ 'dashboard.profile.location_label': 'Location',
+ 'dashboard.profile.save_button': 'Save Profile Changes',
+ 'dashboard.profile.saving_button': 'Saving Profile...',
+ 'dashboard.profile.change_password_title': 'Change Password',
+ 'dashboard.profile.change_password_description': 'Update your account password for security.',
+ 'dashboard.profile.current_password_label': 'Current Password',
+ 'dashboard.profile.new_password_label': 'New Password',
+ 'dashboard.profile.confirm_new_password_label': 'Confirm New Password',
+ 'dashboard.profile.update_password_button': 'Update Password',
+ 'dashboard.profile.updating_password_button': 'Updating Password...',
+ 'lang.english': 'English',
+ 'lang.traditional_chinese': '繁體中文',
+ 'lang.select_language': 'Select Language',
+ 'general.back_to_my_toys': 'Back to My Toys',
+ 'general.toy_not_found': 'Toy Not Found',
+ 'general.cannot_edit_nonexistent_toy': 'Cannot edit a toy that does not exist.',
+ 'add_toy_form.edit_title': 'Edit Your Toy',
+ 'add_toy_form.add_title': 'Share a New Toy',
+ 'add_toy_form.edit_description': 'Update the details of your toy listing.',
+ 'add_toy_form.add_description': 'Fill in the details below to list your toy for others to enjoy.',
+ 'add_toy_form.name_label': 'Toy Name',
+ 'add_toy_form.description_label': 'Description',
+ 'add_toy_form.category_label': 'Category',
+ 'add_toy_form.select_category_placeholder': 'Select a category',
+ 'add_toy_form.price_label': 'Rental Price per Day ($)',
+ 'add_toy_form.location_label': 'Location (Optional)',
+ 'add_toy_form.images_label': 'Toy Images (URLs)',
+ 'add_toy_form.images_description': 'Enter direct URLs to your toy images. Add up to 5 images.',
+ 'add_toy_form.add_image_button': 'Add Another Image',
+ 'add_toy_form.availability_label': 'Weekly Availability',
+ 'add_toy_form.save_button': 'Save Changes',
+ 'add_toy_form.list_button': 'List My Toy',
+ 'add_toy_form.saving_button': 'Saving Changes...',
+ 'add_toy_form.listing_button': 'Listing Toy...',
+} as const;
diff --git a/src/locales/server.ts b/src/locales/server.ts
new file mode 100644
index 0000000..73b76a4
--- /dev/null
+++ b/src/locales/server.ts
@@ -0,0 +1,11 @@
+import { createI18nServer } from 'next-international/server';
+
+export const {
+ getI18n,
+ getScopedI18n,
+ getCurrentLocale,
+ getStaticParams
+} = createI18nServer({
+ en: () => import('./en'),
+ 'zh-TW': () => import('./zh-TW'),
+});
diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts
new file mode 100644
index 0000000..ef37043
--- /dev/null
+++ b/src/locales/zh-TW.ts
@@ -0,0 +1,119 @@
+export default {
+ 'header.browse_toys': '瀏覽玩具',
+ 'header.login': '登入',
+ 'header.register': '註冊',
+ 'header.my_account': '我的帳戶',
+ 'header.dashboard': '儀表板',
+ 'header.profile': '個人資料',
+ 'header.logout': '登出',
+ 'footer.copy': '© {year} ToyShare. 版權所有。',
+ 'footer.tagline': '分享快樂,從玩具開始。',
+ 'home.welcome': '歡迎來到 ToyShare!',
+ 'home.discover': '發現充滿樂趣的世界!分享您心愛的玩具,或從我們友善的社群租借玩具,開啟新的冒險旅程。',
+ 'home.explore_toys': '探索玩具',
+ 'home.share_your_toy': '分享您的玩具',
+ 'home.available_toys': '現有玩具',
+ 'login.welcome_back': '歡迎回來!',
+ 'login.description': '登入您的 ToyShare 帳戶以繼續。',
+ 'login.email_label': '電子郵件地址',
+ 'login.password_label': '密碼',
+ 'login.submit_button': '登入',
+ 'login.loading_button': '登入中...',
+ 'login.forgot_password': '忘記密碼? 在此重設。',
+ 'login.no_account': '還沒有帳戶? 立即註冊',
+ 'register.create_account': '建立您的帳戶',
+ 'register.description': '加入 ToyShare,開始分享樂趣!',
+ 'register.name_label': '全名',
+ 'register.confirm_password_label': '確認密碼',
+ 'register.submit_button': '建立帳戶',
+ 'register.loading_button': '註冊中...',
+ 'register.has_account': '已經有帳戶了? 登入',
+ 'toy_details.back_to_toys': '返回所有玩具',
+ 'toy_details.toy_not_found_title': '找不到玩具',
+ 'toy_details.toy_not_found_description': '抱歉,您尋找的玩具不存在或已被移除。',
+ 'toy_details.owner': '擁有者',
+ 'toy_details.location': '地點',
+ 'toy_details.price': '價格',
+ 'toy_details.price_free': '免費',
+ 'toy_details.price_per_day': '/天',
+ 'toy_details.request_to_rent': '請求租借',
+ 'dashboard.layout.loading': '正在載入儀表板...',
+ 'dashboard.sidebar.user_dashboard': '使用者儀表板',
+ 'dashboard.sidebar.toy_management': '玩具管理',
+ 'dashboard.sidebar.overview': '總覽',
+ 'dashboard.sidebar.my_toys': '我的玩具',
+ 'dashboard.sidebar.add_new_toy': '新增玩具',
+ 'dashboard.sidebar.my_rentals': '我的租借',
+ 'dashboard.sidebar.rental_requests': '租借請求',
+ 'dashboard.sidebar.account': '帳戶',
+ 'dashboard.sidebar.profile_settings': '個人資料設定',
+ 'dashboard.sidebar.logout': '登出',
+ 'dashboard.overview.welcome': '歡迎來到您的儀表板!',
+ 'dashboard.overview.description': '集中管理您的玩具、租借和帳戶設定。',
+ 'dashboard.overview.my_listed_toys': '我列出的玩具',
+ 'dashboard.overview.view_my_toys': '查看我的玩具',
+ 'dashboard.overview.toys_i_renting': '我正在租借的玩具',
+ 'dashboard.overview.view_my_rentals': '查看我的租借',
+ 'dashboard.overview.pending_requests': '待處理請求',
+ 'dashboard.overview.manage_requests': '管理請求',
+ 'dashboard.overview.quick_actions': '快速操作',
+ 'dashboard.overview.add_new_toy_button': '新增玩具',
+ 'dashboard.overview.update_profile_button': '更新個人資料',
+ 'dashboard.overview.tips_for_sharers': '給分享者的提示',
+ 'dashboard.overview.tip1': '從多個角度拍攝玩具的清晰照片。',
+ 'dashboard.overview.tip2': '撰寫詳細且準確的描述。',
+ 'dashboard.overview.tip3': '保持您的可用性日曆最新。',
+ 'dashboard.overview.tip4': '及時回應租借請求。',
+ 'dashboard.my_toys.title': '我列出的玩具',
+ 'dashboard.my_toys.description': '管理您與社群分享的玩具。',
+ 'dashboard.my_toys.add_new_toy_button': '新增玩具',
+ 'dashboard.my_toys.no_toys_title': '尚無列出玩具',
+ 'dashboard.my_toys.no_toys_description': '分享您的第一個玩具,傳播歡樂!',
+ 'dashboard.my_toys.add_first_toy_button': '新增您的第一個玩具',
+ 'dashboard.my_toys.view_toy_action': '查看玩具',
+ 'dashboard.my_toys.edit_toy_action': '編輯玩具',
+ 'dashboard.my_toys.delete_toy_action': '刪除玩具',
+ 'dashboard.profile.title': '個人資料設定',
+ 'dashboard.profile.description': '管理您的個人資訊和帳戶設定。',
+ 'dashboard.profile.personal_info_title': '個人資訊',
+ 'dashboard.profile.personal_info_description': '更新您公開顯示的個人資料資訊。',
+ 'dashboard.profile.avatar_url_label': '頭像 URL',
+ 'dashboard.profile.full_name_label': '全名',
+ 'dashboard.profile.email_label': '電子郵件地址 (唯讀)',
+ 'dashboard.profile.bio_label': '簡介',
+ 'dashboard.profile.phone_label': '電話號碼',
+ 'dashboard.profile.location_label': '地點',
+ 'dashboard.profile.save_button': '儲存個人資料變更',
+ 'dashboard.profile.saving_button': '儲存個人資料中...',
+ 'dashboard.profile.change_password_title': '更改密碼',
+ 'dashboard.profile.change_password_description': '為了安全,更新您的帳戶密碼。',
+ 'dashboard.profile.current_password_label': '目前密碼',
+ 'dashboard.profile.new_password_label': '新密碼',
+ 'dashboard.profile.confirm_new_password_label': '確認新密碼',
+ 'dashboard.profile.update_password_button': '更新密碼',
+ 'dashboard.profile.updating_password_button': '更新密碼中...',
+ 'lang.english': 'English',
+ 'lang.traditional_chinese': '繁體中文',
+ 'lang.select_language': '選擇語言',
+ 'general.back_to_my_toys': '返回我的玩具',
+ 'general.toy_not_found': '找不到玩具',
+ 'general.cannot_edit_nonexistent_toy': '無法編輯不存在的玩具。',
+ 'add_toy_form.edit_title': '編輯您的玩具',
+ 'add_toy_form.add_title': '分享一個新玩具',
+ 'add_toy_form.edit_description': '更新您玩具列表的詳細資訊。',
+ 'add_toy_form.add_description': '填寫以下詳細資訊,將您的玩具列出供他人享用。',
+ 'add_toy_form.name_label': '玩具名稱',
+ 'add_toy_form.description_label': '描述',
+ 'add_toy_form.category_label': '類別',
+ 'add_toy_form.select_category_placeholder': '選擇一個類別',
+ 'add_toy_form.price_label': '每日租金 ($)',
+ 'add_toy_form.location_label': '地點 (選填)',
+ 'add_toy_form.images_label': '玩具圖片 (URL)',
+ 'add_toy_form.images_description': '輸入玩具圖片的直接 URL。最多可新增 5 張圖片。',
+ 'add_toy_form.add_image_button': '新增另一張圖片',
+ 'add_toy_form.availability_label': '每週可用時間',
+ 'add_toy_form.save_button': '儲存變更',
+ 'add_toy_form.list_button': '列出我的玩具',
+ 'add_toy_form.saving_button': '儲存變更中...',
+ 'add_toy_form.listing_button': '列出玩具中...',
+} as const;
diff --git a/src/middleware.ts b/src/middleware.ts
new file mode 100644
index 0000000..57ed565
--- /dev/null
+++ b/src/middleware.ts
@@ -0,0 +1,18 @@
+import type { NextRequest } from 'next/server';
+import { createI18nMiddleware } from 'next-international/middleware';
+
+const I18nMiddleware = createI18nMiddleware({
+ locales: ['en', 'zh-TW'],
+ defaultLocale: 'en',
+ urlMappingStrategy: 'rewrite', // Or 'redirect', depending on preference
+ // prefix: 'as-needed' // default is 'as-needed'
+});
+
+export function middleware(request: NextRequest) {
+ return I18nMiddleware(request);
+}
+
+export const config = {
+ // Matcher ignoring `/_next/` and `/api/`
+ matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
+};