diff --git a/app/(auth)/register/page.tsx b/app/[locale]/(auth)/register/page.tsx
similarity index 68%
rename from app/(auth)/register/page.tsx
rename to app/[locale]/(auth)/register/page.tsx
index ff2f1e80f1..bd3676967a 100644
--- a/app/(auth)/register/page.tsx
+++ b/app/[locale]/(auth)/register/page.tsx
@@ -4,13 +4,16 @@ import Link from "next/link";
import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import { useActionState, useEffect, useState } from "react";
+import { useTranslations } from "next-intl";
import { AuthForm } from "@/components/auth-form";
import { SubmitButton } from "@/components/submit-button";
import { toast } from "@/components/toast";
-import { type RegisterActionState, register } from "../actions";
+import { type RegisterActionState, register } from "@/app/(auth)/actions";
export default function Page() {
const router = useRouter();
+ const t = useTranslations('auth.signUp');
+ const formLabels = useTranslations('auth.form');
const [email, setEmail] = useState("");
const [isSuccessful, setIsSuccessful] = useState(false);
@@ -27,16 +30,16 @@ export default function Page() {
// biome-ignore lint/correctness/useExhaustiveDependencies: router and updateSession are stable refs
useEffect(() => {
if (state.status === "user_exists") {
- toast({ type: "error", description: "Account already exists!" });
+ toast({ type: "error", description: t('errors.accountExists') });
} else if (state.status === "failed") {
- toast({ type: "error", description: "Failed to create account!" });
+ toast({ type: "error", description: t('errors.createFailed') });
} else if (state.status === "invalid_data") {
toast({
type: "error",
- description: "Failed validating your submission!",
+ description: t('errors.validationFailed'),
});
} else if (state.status === "success") {
- toast({ type: "success", description: "Account created successfully!" });
+ toast({ type: "success", description: t('success') });
setIsSuccessful(true);
updateSession();
@@ -53,22 +56,26 @@ export default function Page() {
-
Sign Up
+
{t('title')}
- Create an account with your email and password
+ {t('description')}
-
- Sign Up
+
+ {t('button')}
- {"Already have an account? "}
+ {t('hasAccount')}{' '}
- Sign in
-
- {" instead."}
+ {t('signInLink')}
+ {' '}
+ {t('instead')}
diff --git a/app/(chat)/chat/[id]/page.tsx b/app/[locale]/(chat)/chat/[id]/page.tsx
similarity index 89%
rename from app/(chat)/chat/[id]/page.tsx
rename to app/[locale]/(chat)/chat/[id]/page.tsx
index 1bd5693765..3377d02f51 100644
--- a/app/(chat)/chat/[id]/page.tsx
+++ b/app/[locale]/(chat)/chat/[id]/page.tsx
@@ -1,6 +1,7 @@
import { cookies } from "next/headers";
import { notFound, redirect } from "next/navigation";
import { Suspense } from "react";
+import { setRequestLocale } from "next-intl/server";
import { auth } from "@/app/(auth)/auth";
import { Chat } from "@/components/chat";
@@ -9,7 +10,10 @@ import { DEFAULT_CHAT_MODEL } from "@/lib/ai/models";
import { getChatById, getMessagesByChatId } from "@/lib/db/queries";
import { convertToUIMessages } from "@/lib/utils";
-export default function Page(props: { params: Promise<{ id: string }> }) {
+export default async function Page(props: { params: Promise<{ id: string; locale: string }> }) {
+ const { locale } = await props.params;
+ setRequestLocale(locale);
+
return (
}>
diff --git a/app/[locale]/(chat)/layout.tsx b/app/[locale]/(chat)/layout.tsx
new file mode 100644
index 0000000000..e820f634c8
--- /dev/null
+++ b/app/[locale]/(chat)/layout.tsx
@@ -0,0 +1,71 @@
+import { cookies } from "next/headers";
+import Script from "next/script";
+import { Suspense } from "react";
+import { NextIntlClientProvider, hasLocale } from "next-intl";
+import { getMessages, setRequestLocale } from "next-intl/server";
+import { notFound } from "next/navigation";
+import { AppSidebar } from "@/components/app-sidebar";
+import { DataStreamProvider } from "@/components/data-stream-provider";
+import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
+import { auth } from "@/app/(auth)/auth";
+import { routing } from "@/i18n/routing";
+import { vazirmatn } from "@/lib/fonts";
+
+export function generateStaticParams() {
+ return routing.locales.map((locale) => ({ locale }));
+}
+
+export default async function Layout({
+ children,
+ params,
+}: {
+ children: React.ReactNode;
+ params: Promise<{ locale: string }>;
+}) {
+ const { locale } = await params;
+
+ // Validate locale
+ if (!hasLocale(routing.locales, locale)) {
+ notFound();
+ }
+
+ // Enable static rendering
+ setRequestLocale(locale);
+
+ const messages = await getMessages();
+ const isPersian = locale === 'fa';
+
+ return (
+ <>
+
+
+ }>
+
+
+ {children}
+
+
+
+
+ >
+ );
+}
+
+async function SidebarWrapper({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const [session, cookieStore] = await Promise.all([auth(), cookies()]);
+ const isCollapsed = cookieStore.get("sidebar_state")?.value !== "true";
+
+ return (
+
+
+ {children}
+
+ );
+}
diff --git a/app/(chat)/page.tsx b/app/[locale]/(chat)/page.tsx
similarity index 85%
rename from app/(chat)/page.tsx
rename to app/[locale]/(chat)/page.tsx
index 332ed4bfdd..d476f589ef 100644
--- a/app/(chat)/page.tsx
+++ b/app/[locale]/(chat)/page.tsx
@@ -1,11 +1,15 @@
import { cookies } from "next/headers";
import { Suspense } from "react";
+import { setRequestLocale } from "next-intl/server";
import { Chat } from "@/components/chat";
import { DataStreamHandler } from "@/components/data-stream-handler";
import { DEFAULT_CHAT_MODEL } from "@/lib/ai/models";
import { generateUUID } from "@/lib/utils";
-export default function Page() {
+export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
+ const { locale } = await params;
+ setRequestLocale(locale);
+
return (
}>
diff --git a/app/globals.css b/app/globals.css
index de6fc04600..7960f5e9d3 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -4,6 +4,28 @@
/* include utility classes in streamdown */
@source "../node_modules/streamdown/dist/index.js";
+/* Vazirmatn font for Persian */
+@font-face {
+ font-family: "Vazirmatn";
+ src: url("/fonts/Vazirmatn-Regular.ttf") format("truetype");
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: "Vazirmatn";
+ src: url("/fonts/Vazirmatn-Bold.ttf") format("truetype");
+ font-weight: 700;
+ font-style: normal;
+ font-display: swap;
+}
+
+/* Apply Vazirmatn for RTL elements */
+* {
+ font-family: "Vazirmatn", var(--font-geist), sans-serif;
+}
+
/* custom variant for setting dark mode programmatically */
@custom-variant dark (&:is(.dark, .dark *));
diff --git a/app/layout.tsx b/app/layout.tsx
index 66db5da925..4d7ecca30d 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,17 +1,11 @@
-import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Toaster } from "sonner";
import { ThemeProvider } from "@/components/theme-provider";
+import { Suspense } from "react";
import "./globals.css";
import { SessionProvider } from "next-auth/react";
-export const metadata: Metadata = {
- metadataBase: new URL("https://chat.vercel.ai"),
- title: "Next.js Chatbot Template",
- description: "Next.js chatbot template using the AI SDK.",
-};
-
export const viewport = {
maximumScale: 1, // Disable auto-zoom on mobile Safari
};
@@ -48,7 +42,7 @@ const THEME_COLOR_SCRIPT = `\
updateThemeColor();
})();`;
-export default function RootLayout({
+export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
@@ -56,11 +50,6 @@ export default function RootLayout({
return (
@@ -71,16 +60,18 @@ export default function RootLayout({
}}
/>
-
-
-
- {children}
-
+
+
}>
+
+
+ {children}
+
+
);
diff --git a/app/page.tsx b/app/page.tsx
new file mode 100644
index 0000000000..d61f3a4feb
--- /dev/null
+++ b/app/page.tsx
@@ -0,0 +1,6 @@
+import { redirect } from 'next/navigation';
+import { routing } from '@/i18n/routing';
+
+export default function RootPage() {
+ redirect(`/${routing.defaultLocale}`);
+}
diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx
index ec3f27356a..4e06b43ff6 100644
--- a/components/app-sidebar.tsx
+++ b/components/app-sidebar.tsx
@@ -7,6 +7,7 @@ import { useState } from "react";
import { toast } from "sonner";
import { useSWRConfig } from "swr";
import { unstable_serialize } from "swr/infinite";
+import { useTranslations, useLocale } from "next-intl";
import { PlusIcon, TrashIcon } from "@/components/icons";
import {
getChatHistoryPaginationKey,
@@ -39,6 +40,10 @@ export function AppSidebar({ user }: { user: User | undefined }) {
const { setOpenMobile } = useSidebar();
const { mutate } = useSWRConfig();
const [showDeleteAllDialog, setShowDeleteAllDialog] = useState(false);
+ const t = useTranslations('chat.sidebar');
+ const deleteT = useTranslations('sidebarHistory.deleteAll');
+ const locale = useLocale();
+ const isRTL = locale === 'fa';
const handleDeleteAll = () => {
const deletePromise = fetch("/api/history", {
@@ -46,21 +51,21 @@ export function AppSidebar({ user }: { user: User | undefined }) {
});
toast.promise(deletePromise, {
- loading: "Deleting all chats...",
+ loading: deleteT('loading'),
success: () => {
mutate(unstable_serialize(getChatHistoryPaginationKey));
setShowDeleteAllDialog(false);
router.replace("/");
router.refresh();
- return "All chats deleted successfully";
+ return deleteT('success');
},
- error: "Failed to delete all chats",
+ error: deleteT('error'),
});
};
return (
<>
-
+
@@ -72,7 +77,7 @@ export function AppSidebar({ user }: { user: User | undefined }) {
}}
>
- Chatbot
+ {t('title')}
@@ -89,7 +94,7 @@ export function AppSidebar({ user }: { user: User | undefined }) {
- Delete All Chats
+ {t('deleteAll')}
)}
@@ -109,7 +114,7 @@ export function AppSidebar({ user }: { user: User | undefined }) {
- New Chat
+ {t('newChat')}
@@ -128,16 +133,15 @@ export function AppSidebar({ user }: { user: User | undefined }) {
>
- Delete all chats?
+ {deleteT('title')}
- This action cannot be undone. This will permanently delete all
- your chats and remove them from our servers.
+ {deleteT('description')}
- Cancel
+ {deleteT('cancel')}
- Delete All
+ {deleteT('delete')}
diff --git a/components/auth-form.tsx b/components/auth-form.tsx
index 6ad4a8c36b..0971811d42 100644
--- a/components/auth-form.tsx
+++ b/components/auth-form.tsx
@@ -7,12 +7,18 @@ export function AuthForm({
action,
children,
defaultEmail = "",
+ labels,
}: {
action: NonNullable<
string | ((formData: FormData) => void | Promise
) | undefined
>;
children: React.ReactNode;
defaultEmail?: string;
+ labels: {
+ email: string;
+ emailPlaceholder: string;
+ password: string;
+ };
}) {
return (
@@ -69,7 +71,7 @@ export function PureMessageActions({
return (
-
+
@@ -87,7 +89,7 @@ export function PureMessageActions({
});
toast.promise(upvote, {
- loading: "Upvoting Response...",
+ loading: t('upvote.loading'),
success: () => {
mutate(
`/api/vote?chatId=${chatId}`,
@@ -112,12 +114,12 @@ export function PureMessageActions({
{ revalidate: false }
);
- return "Upvoted Response!";
+ return t('upvote.success');
},
- error: "Failed to upvote response.",
+ error: t('upvote.error'),
});
}}
- tooltip="Upvote Response"
+ tooltip={t('upvote.tooltip')}
>
@@ -136,7 +138,7 @@ export function PureMessageActions({
});
toast.promise(downvote, {
- loading: "Downvoting Response...",
+ loading: t('downvote.loading'),
success: () => {
mutate(
`/api/vote?chatId=${chatId}`,
@@ -161,12 +163,12 @@ export function PureMessageActions({
{ revalidate: false }
);
- return "Downvoted Response!";
+ return t('downvote.success');
},
- error: "Failed to downvote response.",
+ error: t('downvote.error'),
});
}}
- tooltip="Downvote Response"
+ tooltip={t('downvote.tooltip')}
>
diff --git a/components/multimodal-input.tsx b/components/multimodal-input.tsx
index ed62df19c9..2bf3bbd3e6 100644
--- a/components/multimodal-input.tsx
+++ b/components/multimodal-input.tsx
@@ -16,6 +16,7 @@ import {
} from "react";
import { toast } from "sonner";
import { useLocalStorage, useWindowSize } from "usehooks-ts";
+import { useTranslations } from "next-intl";
import {
ModelSelector,
ModelSelectorContent,
@@ -84,6 +85,8 @@ function PureMultimodalInput({
selectedModelId: string;
onModelChange?: (modelId: string) => void;
}) {
+ const t = useTranslations('input');
+ const chatT = useTranslations('chat');
const textareaRef = useRef(null);
const { width } = useWindowSize();
@@ -206,9 +209,9 @@ function PureMultimodalInput({
const { error } = await response.json();
toast.error(error);
} catch (_error) {
- toast.error("Failed to upload file, please try again!");
+ toast.error(t('uploadFailed'));
}
- }, []);
+ }, [t]);
const handleFileChange = useCallback(
async (event: ChangeEvent) => {
@@ -276,12 +279,12 @@ function PureMultimodalInput({
]);
} catch (error) {
console.error("Error uploading pasted images:", error);
- toast.error("Failed to upload pasted image(s)");
+ toast.error(t('pasteFailed'));
} finally {
setUploadQueue([]);
}
},
- [setAttachments, uploadFile]
+ [setAttachments, uploadFile, t]
);
// Add paste event listener to textarea
@@ -324,7 +327,7 @@ function PureMultimodalInput({
return;
}
if (status !== "ready") {
- toast.error("Please wait for the model to finish its response!");
+ toast.error(t('waitForResponse'));
} else {
submitForm();
}
@@ -371,7 +374,7 @@ function PureMultimodalInput({
maxHeight={200}
minHeight={44}
onChange={handleInput}
- placeholder="Send a message..."
+ placeholder={chatT('inputPlaceholder')}
ref={textareaRef}
rows={1}
value={input}
@@ -468,6 +471,7 @@ function PureModelSelectorCompact({
selectedModelId: string;
onModelChange?: (modelId: string) => void;
}) {
+ const chatT = useTranslations('chat');
const [open, setOpen] = useState(false);
const selectedModel =
@@ -494,7 +498,7 @@ function PureModelSelectorCompact({
-
+
{Object.entries(modelsByProvider).map(
([providerKey, providerModels]) => (
diff --git a/components/sidebar-history-item.tsx b/components/sidebar-history-item.tsx
index 7a20c61a20..9c17e89512 100644
--- a/components/sidebar-history-item.tsx
+++ b/components/sidebar-history-item.tsx
@@ -1,5 +1,6 @@
import Link from "next/link";
import { memo } from "react";
+import { useTranslations } from "next-intl";
import { useChatVisibility } from "@/hooks/use-chat-visibility";
import type { Chat } from "@/lib/db/schema";
import {
@@ -41,6 +42,7 @@ const PureChatItem = ({
chatId: chat.id,
initialVisibilityType: chat.visibility,
});
+ const t = useTranslations('sidebarHistory.item');
return (
@@ -57,7 +59,7 @@ const PureChatItem = ({
showOnHover={!isActive}
>
- More
+ {t('more')}
@@ -65,7 +67,7 @@ const PureChatItem = ({
- Share
+ {t('share')}
@@ -77,7 +79,7 @@ const PureChatItem = ({
>
- Private
+ {t('private')}
{visibilityType === "private" ? (
@@ -91,7 +93,7 @@ const PureChatItem = ({
>
- Public
+ {t('public')}
{visibilityType === "public" ? : null}
@@ -104,7 +106,7 @@ const PureChatItem = ({
onSelect={() => onDelete(chat.id)}
>
- Delete
+ {t('delete')}
diff --git a/components/sidebar-history.tsx b/components/sidebar-history.tsx
index 9ac1ab5e03..70cc727190 100644
--- a/components/sidebar-history.tsx
+++ b/components/sidebar-history.tsx
@@ -7,6 +7,7 @@ import type { User } from "next-auth";
import { useState } from "react";
import { toast } from "sonner";
import useSWRInfinite from "swr/infinite";
+import { useTranslations } from "next-intl";
import {
AlertDialog,
AlertDialogAction,
@@ -101,6 +102,7 @@ export function SidebarHistory({ user }: { user: User | undefined }) {
const { setOpenMobile } = useSidebar();
const pathname = usePathname();
const id = pathname?.startsWith("/chat/") ? pathname.split("/")[2] : null;
+ const t = useTranslations('sidebarHistory');
const {
data: paginatedChatHistories,
@@ -135,7 +137,7 @@ export function SidebarHistory({ user }: { user: User | undefined }) {
});
toast.promise(deletePromise, {
- loading: "Deleting chat...",
+ loading: t('delete.loading'),
success: () => {
mutate((chatHistories) => {
if (chatHistories) {
@@ -153,9 +155,9 @@ export function SidebarHistory({ user }: { user: User | undefined }) {
router.refresh();
}
- return "Chat deleted successfully";
+ return t('delete.success');
},
- error: "Failed to delete chat",
+ error: t('delete.error'),
});
};
@@ -164,7 +166,7 @@ export function SidebarHistory({ user }: { user: User | undefined }) {
- Login to save and revisit previous chats!
+ {t('loginPrompt')}
@@ -175,7 +177,7 @@ export function SidebarHistory({ user }: { user: User | undefined }) {
return (
- Today
+ {t('today')}
@@ -205,7 +207,7 @@ export function SidebarHistory({ user }: { user: User | undefined }) {
- Your conversations will appear here once you start chatting!
+ {t('noChats')}
@@ -230,7 +232,7 @@ export function SidebarHistory({ user }: { user: User | undefined }) {
{groupedChats.today.length > 0 && (
- Today
+ {t('today')}
{groupedChats.today.map((chat) => (
0 && (
- Yesterday
+ {t('yesterday')}
{groupedChats.yesterday.map((chat) => (
0 && (
- Last 7 days
+ {t('last7Days')}
{groupedChats.lastWeek.map((chat) => (
0 && (
- Last 30 days
+ {t('last30Days')}
{groupedChats.lastMonth.map((chat) => (
0 && (
- Older than last month
+ {t('older')}
{groupedChats.older.map((chat) => (
- You have reached the end of your chat history.
+ {t('endOfHistory')}
) : (
-
Loading Chats...
+
{t('loading')}
)}
@@ -357,16 +359,15 @@ export function SidebarHistory({ user }: { user: User | undefined }) {
- Are you absolutely sure?
+ {t('delete.title')}
- This action cannot be undone. This will permanently delete your
- chat and remove it from our servers.
+ {t('delete.description')}
- Cancel
+ {t('delete.cancel')}
- Continue
+ {t('delete.continue')}
diff --git a/components/suggested-actions.tsx b/components/suggested-actions.tsx
index 8f63336a15..a23c8a1963 100644
--- a/components/suggested-actions.tsx
+++ b/components/suggested-actions.tsx
@@ -3,6 +3,7 @@
import type { UseChatHelpers } from "@ai-sdk/react";
import { motion } from "framer-motion";
import { memo } from "react";
+import { useTranslations } from "next-intl";
import type { ChatMessage } from "@/lib/types";
import { Suggestion } from "./elements/suggestion";
import type { VisibilityType } from "./visibility-selector";
@@ -14,11 +15,13 @@ type SuggestedActionsProps = {
};
function PureSuggestedActions({ chatId, sendMessage }: SuggestedActionsProps) {
+ const t = useTranslations('chat.suggestions');
+
const suggestedActions = [
- "What are the advantages of using Next.js?",
- "Write code to demonstrate Dijkstra's algorithm",
- "Help me write an essay about Silicon Valley",
- "What is the weather in San Francisco?",
+ t('nextjs'),
+ t('dijkstra'),
+ t('essay'),
+ t('weather'),
];
return (
diff --git a/components/ui/sidebar.tsx b/components/ui/sidebar.tsx
index cd77cbbb1c..919ef9500b 100644
--- a/components/ui/sidebar.tsx
+++ b/components/ui/sidebar.tsx
@@ -1,9 +1,10 @@
"use client";
import { cva, type VariantProps } from "class-variance-authority";
-import { PanelLeft } from "lucide-react";
+import { PanelLeft, PanelRight } from "lucide-react";
import { Slot as SlotPrimitive } from "radix-ui";
import * as React from "react";
+import { useLocale } from "next-intl";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
@@ -273,6 +274,8 @@ const SidebarTrigger = React.forwardRef<
React.ComponentProps
>(({ className, onClick, ...props }, ref) => {
const { toggleSidebar } = useSidebar();
+ const locale = useLocale();
+ const isRTL = locale === 'fa';
return (
);
diff --git a/components/visibility-selector.tsx b/components/visibility-selector.tsx
index fed34373d8..3d018bea03 100644
--- a/components/visibility-selector.tsx
+++ b/components/visibility-selector.tsx
@@ -1,6 +1,7 @@
"use client";
-import { type ReactNode, useMemo, useState } from "react";
+import { useMemo, useState } from "react";
+import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
@@ -19,26 +20,6 @@ import {
export type VisibilityType = "private" | "public";
-const visibilities: Array<{
- id: VisibilityType;
- label: string;
- description: string;
- icon: ReactNode;
-}> = [
- {
- id: "private",
- label: "Private",
- description: "Only you can access this chat",
- icon: ,
- },
- {
- id: "public",
- label: "Public",
- description: "Anyone with the link can access this chat",
- icon: ,
- },
-];
-
export function VisibilitySelector({
chatId,
className,
@@ -48,15 +29,31 @@ export function VisibilitySelector({
selectedVisibilityType: VisibilityType;
} & React.ComponentProps) {
const [open, setOpen] = useState(false);
+ const t = useTranslations('visibility');
const { visibilityType, setVisibilityType } = useChatVisibility({
chatId,
initialVisibilityType: selectedVisibilityType,
});
+ const visibilities = useMemo(() => [
+ {
+ id: "private" as const,
+ label: t('private.label'),
+ description: t('private.description'),
+ icon: ,
+ },
+ {
+ id: "public" as const,
+ label: t('public.label'),
+ description: t('public.description'),
+ icon: ,
+ },
+ ], [t]);
+
const selectedVisibility = useMemo(
() => visibilities.find((visibility) => visibility.id === visibilityType),
- [visibilityType]
+ [visibilityType, visibilities]
);
return (
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000000..c50aa3288f
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,19 @@
+services:
+ db:
+ image: docker.arvancloud.ir/postgres:latest
+ container_name: chatbot-db
+ restart: unless-stopped
+ env_file: .env.local
+ ports:
+ - 5434:5432
+ volumes:
+ - ./db-data/:/var/lib/postgresql
+ redis:
+ image: docker.arvancloud.ir/redis
+ container_name: chatbot-redis
+ command: redis-server /usr/local/etc/redis/redis.conf
+ ports:
+ - "6379:6379"
+ volumes:
+ - ./redis-data:/data
+ - ./redis.conf:/usr/local/etc/redis/redis.conf
\ No newline at end of file
diff --git a/i18n/locales/en/translation.json b/i18n/locales/en/translation.json
new file mode 100644
index 0000000000..12bacca928
--- /dev/null
+++ b/i18n/locales/en/translation.json
@@ -0,0 +1,268 @@
+{
+ "metadata": {
+ "title": "AI Chatbot",
+ "description": "Next.js chatbot template using the AI SDK."
+ },
+ "auth": {
+ "signIn": {
+ "title": "Sign In",
+ "description": "Use your email and password to sign in",
+ "button": "Sign in",
+ "noAccount": "Don't have an account?",
+ "signUpLink": "Sign up",
+ "forFree": "for free.",
+ "errors": {
+ "invalidCredentials": "Invalid credentials!",
+ "validationFailed": "Failed validating your submission!"
+ }
+ },
+ "signUp": {
+ "title": "Sign Up",
+ "description": "Create an account with your email and password",
+ "button": "Sign Up",
+ "hasAccount": "Already have an account?",
+ "signInLink": "Sign in",
+ "instead": "instead.",
+ "errors": {
+ "accountExists": "Account already exists!",
+ "createFailed": "Failed to create account!",
+ "validationFailed": "Failed validating your submission!"
+ },
+ "success": "Account created successfully!"
+ },
+ "form": {
+ "email": "Email Address",
+ "emailPlaceholder": "user@acme.com",
+ "password": "Password"
+ }
+ },
+ "chat": {
+ "newChat": "New Chat",
+ "inputPlaceholder": "Send a message...",
+ "searchModels": "Search models...",
+ "deployWithVercel": "Deploy with Vercel",
+ "sidebar": {
+ "title": "Chatbot",
+ "deleteAll": "Delete All Chats",
+ "newChat": "New Chat"
+ },
+ "greeting": {
+ "hello": "Hello there!",
+ "help": "How can I help you today?"
+ },
+ "suggestions": {
+ "nextjs": "What are the advantages of using Next.js?",
+ "dijkstra": "Write code to demonstrate Dijkstra's algorithm",
+ "essay": "Help me write an essay about Silicon Valley",
+ "weather": "What is the weather in San Francisco?"
+ }
+ },
+ "visibility": {
+ "private": {
+ "label": "Private",
+ "description": "Only you can access this chat"
+ },
+ "public": {
+ "label": "Public",
+ "description": "Anyone with the link can access this chat"
+ }
+ },
+ "sidebarHistory": {
+ "loginPrompt": "Login to save and revisit previous chats!",
+ "today": "Today",
+ "yesterday": "Yesterday",
+ "last7Days": "Last 7 days",
+ "last30Days": "Last 30 days",
+ "older": "Older than last month",
+ "noChats": "Your conversations will appear here once you start chatting!",
+ "endOfHistory": "You have reached the end of your chat history.",
+ "loading": "Loading Chats...",
+ "delete": {
+ "title": "Are you absolutely sure?",
+ "description": "This action cannot be undone. This will permanently delete your chat and remove it from our servers.",
+ "cancel": "Cancel",
+ "continue": "Continue",
+ "loading": "Deleting chat...",
+ "success": "Chat deleted successfully",
+ "error": "Failed to delete chat"
+ },
+ "deleteAll": {
+ "title": "Delete all chats?",
+ "description": "This action cannot be undone. This will permanently delete all your chats and remove them from our servers.",
+ "cancel": "Cancel",
+ "delete": "Delete All",
+ "loading": "Deleting all chats...",
+ "success": "All chats deleted successfully",
+ "error": "Failed to delete all chats"
+ },
+ "item": {
+ "more": "More",
+ "share": "Share",
+ "private": "Private",
+ "public": "Public",
+ "delete": "Delete"
+ }
+ },
+ "messages": {
+ "actions": {
+ "edit": "Edit",
+ "copy": "Copy",
+ "upvote": {
+ "tooltip": "Upvote Response",
+ "loading": "Upvoting Response...",
+ "success": "Upvoted Response!",
+ "error": "Failed to upvote response."
+ },
+ "downvote": {
+ "tooltip": "Downvote Response",
+ "loading": "Downvoting Response...",
+ "success": "Downvoted Response!",
+ "error": "Failed to downvote response."
+ },
+ "noText": "There's no text to copy!",
+ "copied": "Copied to clipboard!"
+ },
+ "weatherDenied": "Weather lookup was denied.",
+ "deny": "Deny",
+ "allow": "Allow",
+ "error": {
+ "createDocument": "Error creating document:",
+ "updateDocument": "Error updating document:",
+ "generic": "Error:"
+ },
+ "thinking": "Thinking",
+ "scrollToBottom": "Scroll to bottom"
+ },
+ "input": {
+ "uploadFailed": "Failed to upload file, please try again!",
+ "pasteFailed": "Failed to upload pasted image(s)",
+ "waitForResponse": "Please wait for the model to finish its response!",
+ "uploadFiles": "Upload files",
+ "submit": "Submit",
+ "removeAttachment": "Remove attachment"
+ },
+ "aiGateway": {
+ "title": "Activate AI Gateway",
+ "description": "This application requires the owner to activate Vercel AI Gateway.",
+ "cancel": "Cancel",
+ "activate": "Activate"
+ },
+ "suggestion": {
+ "assistant": "Assistant",
+ "apply": "Apply"
+ },
+ "version": {
+ "viewingPrevious": "You are viewing a previous version",
+ "restoreToEdit": "Restore this version to make edits",
+ "restore": "Restore this version",
+ "backToLatest": "Back to latest version"
+ },
+ "console": {
+ "title": "Console",
+ "resize": "Resize console",
+ "initializing": "Initializing...",
+ "noOutput": "No console output"
+ },
+ "artifact": {
+ "actions": {
+ "failed": "Failed to execute action",
+ "close": "Close"
+ }
+ },
+ "artifacts": {
+ "text": {
+ "description": "Useful for text content, like drafting essays and emails.",
+ "viewChanges": "View changes",
+ "previous": "View Previous version",
+ "next": "View Next version",
+ "copy": "Copy to clipboard",
+ "copied": "Copied to clipboard!",
+ "polish": "Add final polish",
+ "polishText": "Please add final polish and check for grammar, add section titles for better structure, and ensure everything reads smoothly.",
+ "suggestions": "Request suggestions",
+ "suggestionsText": "Please add suggestions you have that could improve the writing."
+ },
+ "code": {
+ "description": "Useful for code generation; Code execution is only available for python code.",
+ "run": "Run",
+ "execute": "Execute code",
+ "previous": "View Previous version",
+ "next": "View Next version",
+ "copy": "Copy code to clipboard",
+ "copied": "Copied to clipboard!",
+ "comments": "Add comments",
+ "commentsText": "Add comments to the code snippet for understanding",
+ "logs": "Add logs",
+ "logsText": "Add logs to the code snippet for debugging"
+ },
+ "sheet": {
+ "description": "Useful for working with spreadsheets",
+ "previous": "View Previous version",
+ "next": "View Next version",
+ "copy": "Copy as .csv",
+ "copied": "Copied csv to clipboard!",
+ "format": "Format and clean data",
+ "formatText": "Can you please format and clean the data?",
+ "analyze": "Analyze and visualize data",
+ "analyzeText": "Can you please analyze and visualize the data by creating a new code artifact in python?"
+ },
+ "image": {
+ "description": "Useful for image generation",
+ "previous": "View Previous version",
+ "next": "View Next version",
+ "copy": "Copy image to clipboard",
+ "copied": "Copied image to clipboard!"
+ }
+ },
+ "ui": {
+ "sidebar": {
+ "title": "Sidebar",
+ "description": "Displays the mobile sidebar.",
+ "toggle": "Toggle Sidebar"
+ },
+ "close": "Close",
+ "carousel": {
+ "previous": "Previous slide",
+ "next": "Next slide"
+ },
+ "cancel": "Cancel"
+ },
+ "elements": {
+ "source": {
+ "used": "Used {{count}} sources"
+ },
+ "webPreview": {
+ "placeholder": "Enter URL...",
+ "title": "Preview"
+ },
+ "reasoning": {
+ "thinking": "Thinking",
+ "thought": "Thought",
+ "seconds": "{{duration}}s"
+ },
+ "citation": {
+ "previous": "Previous",
+ "next": "Next"
+ },
+ "branch": {
+ "previous": "Previous branch",
+ "next": "Next branch"
+ }
+ },
+ "ai": {
+ "removeAttachment": "Remove attachment",
+ "uploadFiles": "Upload files",
+ "submit": "Submit",
+ "togglePlan": "Toggle plan",
+ "openIn": {
+ "github": "GitHub",
+ "scira": "Scira AI",
+ "openai": "OpenAI",
+ "claude": "Claude",
+ "cursor": "Cursor",
+ "chatgpt": "ChatGPT",
+ "t3": "T3",
+ "v0": "v0"
+ }
+ }
+}
diff --git a/i18n/locales/fa/translation.json b/i18n/locales/fa/translation.json
new file mode 100644
index 0000000000..51b4d035f5
--- /dev/null
+++ b/i18n/locales/fa/translation.json
@@ -0,0 +1,268 @@
+{
+ "metadata": {
+ "title": "چتبات هوش مصنوعی",
+ "description": "قالب چتبات Next.js با استفاده از AI SDK."
+ },
+ "auth": {
+ "signIn": {
+ "title": "ورود",
+ "description": "با ایمیل و رمز عبور خود وارد شوید",
+ "button": "ورود",
+ "noAccount": "حساب کاربری ندارید؟",
+ "signUpLink": "ثبتنام",
+ "forFree": "رایگان.",
+ "errors": {
+ "invalidCredentials": "اعتبارنامه نامعتبر!",
+ "validationFailed": "اعتبارسنجی اطلاعات شما ناموفق بود!"
+ }
+ },
+ "signUp": {
+ "title": "ثبتنام",
+ "description": "با ایمیل و رمز عبور خود حساب کاربری ایجاد کنید",
+ "button": "ثبتنام",
+ "hasAccount": "حساب کاربری دارید؟",
+ "signInLink": "ورود",
+ "instead": "به جای آن.",
+ "errors": {
+ "accountExists": "حساب کاربری قبلاً وجود دارد!",
+ "createFailed": "ایجاد حساب کاربری ناموفق بود!",
+ "validationFailed": "اعتبارسنجی اطلاعات شما ناموفق بود!"
+ },
+ "success": "حساب کاربری با موفقیت ایجاد شد!"
+ },
+ "form": {
+ "email": "آدرس ایمیل",
+ "emailPlaceholder": "user@acme.com",
+ "password": "رمز عبور"
+ }
+ },
+ "chat": {
+ "newChat": "گفتگوی جدید",
+ "inputPlaceholder": "پیامی ارسال کنید...",
+ "searchModels": "جستجوی مدلها...",
+ "deployWithVercel": "دیپلوی با Vercel",
+ "sidebar": {
+ "title": "چتبات",
+ "deleteAll": "حذف همه گفتگوها",
+ "newChat": "گفتگوی جدید"
+ },
+ "greeting": {
+ "hello": "سلام!",
+ "help": "چطور میتوانم به شما کمک کنم؟"
+ },
+ "suggestions": {
+ "nextjs": "مزایای استفاده از Next.js چیست؟",
+ "dijkstra": "کد الگوریتم دایکسترا را بنویس",
+ "essay": "در نوشتن مقاله درباره سیلیکون ولی به من کمک کن",
+ "weather": "آب و هوای سانفرانسیسکو چگونه است؟"
+ }
+ },
+ "visibility": {
+ "private": {
+ "label": "خصوصی",
+ "description": "فقط شما میتوانید به این گفتگو دسترسی داشته باشید"
+ },
+ "public": {
+ "label": "عمومی",
+ "description": "هر کسی با لینک میتواند به این گفتگو دسترسی داشته باشید"
+ }
+ },
+ "sidebarHistory": {
+ "loginPrompt": "برای ذخیره و مشاهده گفتگوهای قبلی، وارد شوید!",
+ "today": "امروز",
+ "yesterday": "دیروز",
+ "last7Days": "۷ روز گذشته",
+ "last30Days": "۳۰ روز گذشته",
+ "older": "قدیمیتر از ماه گذشته",
+ "noChats": "گفتگوهای شما پس از شروع چت کردن اینجا نمایش داده میشوند!",
+ "endOfHistory": "به انتهای تاریخچه گفتگوهای خود رسیدید.",
+ "loading": "در حال بارگذاری گفتگوها...",
+ "delete": {
+ "title": "آیا کاملاً مطمئن هستید؟",
+ "description": "این عمل قابل بازگشت نیست. این گفتگو به طور دائمی حذف خواهد شد و از سرورهای ما پاک میشود.",
+ "cancel": "لغو",
+ "continue": "ادامه",
+ "loading": "در حال حذف گفتگو...",
+ "success": "گفتگو با موفقیت حذف شد",
+ "error": "حذف گفتگو ناموفق بود"
+ },
+ "deleteAll": {
+ "title": "حذف همه گفتگوها؟",
+ "description": "این عمل قابل بازگشت نیست. این کار همه گفتگوهای شما را به طور دائمی حذف خواهد کرد و از سرورهای ما پاک میشود.",
+ "cancel": "لغو",
+ "delete": "حذف همه",
+ "loading": "در حال حذف همه گفتگوها...",
+ "success": "همه گفتگوها با موفقیت حذف شدند",
+ "error": "حذف همه گفتگوها ناموفق بود"
+ },
+ "item": {
+ "more": "بیشتر",
+ "share": "اشتراکگذاری",
+ "private": "خصوصی",
+ "public": "عمومی",
+ "delete": "حذف"
+ }
+ },
+ "messages": {
+ "actions": {
+ "edit": "ویرایش",
+ "copy": "کپی",
+ "upvote": {
+ "tooltip": "رای مثبت به پاسخ",
+ "loading": "در حال رای مثبت...",
+ "success": "رای مثبت ثبت شد!",
+ "error": "رای مثبت ناموفق بود."
+ },
+ "downvote": {
+ "tooltip": "رای منفی به پاسخ",
+ "loading": "در حال رای منفی...",
+ "success": "رای منفی ثبت شد!",
+ "error": "رای منفی ناموفق بود."
+ },
+ "noText": "متنی برای کپی وجود ندارد!",
+ "copied": "در کلیپبورد کپی شد!"
+ },
+ "weatherDenied": "جستجوی آب و هوا رد شد.",
+ "deny": "رد",
+ "allow": "اجازه",
+ "error": {
+ "createDocument": "خطا در ایجاد سند:",
+ "updateDocument": "خطا در بهروزرسانی سند:",
+ "generic": "خطا:"
+ },
+ "thinking": "در حال فکر کردن",
+ "scrollToBottom": "اسکرول به پایین"
+ },
+ "input": {
+ "uploadFailed": "آپلود فایل ناموفق بود، لطفاً دوباره تلاش کنید!",
+ "pasteFailed": "آپلود تصویر(های) الصاقی ناموفق بود",
+ "waitForResponse": "لطفاً منتظر پایان پاسخ مدل بمانید!",
+ "uploadFiles": "آپلود فایلها",
+ "submit": "ارسال",
+ "removeAttachment": "حذف پیوست"
+ },
+ "aiGateway": {
+ "title": "فعالسازی AI Gateway",
+ "description": "این برنامه نیازمند فعالسازی Vercel AI Gateway توسط مالک است.",
+ "cancel": "لغو",
+ "activate": "فعالسازی"
+ },
+ "suggestion": {
+ "assistant": "دستیار",
+ "apply": "اعمال"
+ },
+ "version": {
+ "viewingPrevious": "شما در حال مشاهده یک نسخه قبلی هستید",
+ "restoreToEdit": "این نسخه را بازیابی کنید تا بتوانید ویرایش کنید",
+ "restore": "بازیابی این نسخه",
+ "backToLatest": "بازگشت به آخرین نسخه"
+ },
+ "console": {
+ "title": "کنسول",
+ "resize": "تغییر اندازه کنسول",
+ "initializing": "در حال راهاندازی...",
+ "noOutput": "خروجی کنسولی وجود ندارد"
+ },
+ "artifact": {
+ "actions": {
+ "failed": "اجرای عملیات ناموفق بود",
+ "close": "بستن"
+ }
+ },
+ "artifacts": {
+ "text": {
+ "description": "برای محتوای متنی مفید است، مانند نوشتن مقالات و ایمیلها.",
+ "viewChanges": "مشاهده تغییرات",
+ "previous": "مشاهده نسخه قبلی",
+ "next": "مشاهده نسخه بعدی",
+ "copy": "کپی در کلیپبورد",
+ "copied": "در کلیپبورد کپی شد!",
+ "polish": "افزودن ویرایش نهایی",
+ "polishText": "لطفاً ویرایش نهایی را انجام دهید و دستور زبان را بررسی کنید، عناوین بخش را برای ساختار بهتر اضافه کنید و مطمئن شوید همه چیز روان خوانده میشود.",
+ "suggestions": "درخواست پیشنهادات",
+ "suggestionsText": "لطفاً پیشنهاداتی که میتواند نوشتار را بهبود بخشد اضافه کنید."
+ },
+ "code": {
+ "description": "برای تولید کد مفید است؛ اجرای کد فقط برای کد پایتون در دسترس است.",
+ "run": "اجرا",
+ "execute": "اجرای کد",
+ "previous": "مشاهده نسخه قبلی",
+ "next": "مشاهده نسخه بعدی",
+ "copy": "کپی کد در کلیپبورد",
+ "copied": "در کلیپبورد کپی شد!",
+ "comments": "افزودن نظرات",
+ "commentsText": "نظراتی به قطعه کد برای درک بهتر اضافه کنید",
+ "logs": "افزودن لاگها",
+ "logsText": "لاگهایی به قطعه کد برای اشکالزدایی اضافه کنید"
+ },
+ "sheet": {
+ "description": "برای کار با صفحات گسترده مفید است",
+ "previous": "مشاهده نسخه قبلی",
+ "next": "مشاهده نسخه بعدی",
+ "copy": "کپی به صورت .csv",
+ "copied": "CSV در کلیپبورد کپی شد!",
+ "format": "فرمت و پاکسازی دادهها",
+ "formatText": "آیا میتوانید لطفاً دادهها را فرمت و پاکسازی کنید؟",
+ "analyze": "تحلیل و مصورسازی دادهها",
+ "analyzeText": "آیا میتوانید لطفاً دادهها را تحلیل و مصورسازی کنید با ایجاد یک قطعه کد جدید در پایتون؟"
+ },
+ "image": {
+ "description": "برای تولید تصویر مفید است",
+ "previous": "مشاهده نسخه قبلی",
+ "next": "مشاهده نسخه بعدی",
+ "copy": "کپی تصویر در کلیپبورد",
+ "copied": "تصویر در کلیپبورد کپی شد!"
+ }
+ },
+ "ui": {
+ "sidebar": {
+ "title": "نوار کناری",
+ "description": "نمایش نوار کناری موبایل.",
+ "toggle": "تغییر وضعیت نوار کناری"
+ },
+ "close": "بستن",
+ "carousel": {
+ "previous": "اسلاید قبلی",
+ "next": "اسلاید بعدی"
+ },
+ "cancel": "لغو"
+ },
+ "elements": {
+ "source": {
+ "used": "از {{count}} منبع استفاده شد"
+ },
+ "webPreview": {
+ "placeholder": "URL را وارد کنید...",
+ "title": "پیشنمایش"
+ },
+ "reasoning": {
+ "thinking": "در حال فکر کردن",
+ "thought": "فکر",
+ "seconds": "{{duration}} ثانیه"
+ },
+ "citation": {
+ "previous": "قبلی",
+ "next": "بعدی"
+ },
+ "branch": {
+ "previous": "شاخه قبلی",
+ "next": "شاخه بعدی"
+ }
+ },
+ "ai": {
+ "removeAttachment": "حذف پیوست",
+ "uploadFiles": "آپلود فایلها",
+ "submit": "ارسال",
+ "togglePlan": "تغییر وضعیت برنامه",
+ "openIn": {
+ "github": "GitHub",
+ "scira": "Scira AI",
+ "openai": "OpenAI",
+ "claude": "Claude",
+ "cursor": "Cursor",
+ "chatgpt": "ChatGPT",
+ "t3": "T3",
+ "v0": "v0"
+ }
+ }
+}
diff --git a/i18n/request.ts b/i18n/request.ts
new file mode 100644
index 0000000000..e8e5e9cae4
--- /dev/null
+++ b/i18n/request.ts
@@ -0,0 +1,17 @@
+import { getRequestConfig } from 'next-intl/server';
+import { hasLocale } from 'next-intl';
+import { routing } from './routing';
+
+export default getRequestConfig(async ({ requestLocale }) => {
+ const requested = await requestLocale;
+ const locale = hasLocale(routing.locales, requested)
+ ? requested
+ : routing.defaultLocale;
+
+ const messages = (await import(`./locales/${locale}/translation.json`)).default;
+
+ return {
+ locale,
+ messages
+ };
+});
diff --git a/i18n/routing.ts b/i18n/routing.ts
new file mode 100644
index 0000000000..19d4c84bea
--- /dev/null
+++ b/i18n/routing.ts
@@ -0,0 +1,6 @@
+import { defineRouting } from 'next-intl/routing';
+
+export const routing = defineRouting({
+ locales: ['en', 'fa'],
+ defaultLocale: 'en'
+});
diff --git a/lib/fonts.ts b/lib/fonts.ts
new file mode 100644
index 0000000000..ce61e587ec
--- /dev/null
+++ b/lib/fonts.ts
@@ -0,0 +1,18 @@
+import localFont from "next/font/local";
+
+export const vazirmatn = localFont({
+ src: [
+ {
+ path: "../public/fonts/Vazirmatn-Regular.ttf",
+ weight: "400",
+ style: "normal",
+ },
+ {
+ path: "../public/fonts/Vazirmatn-Bold.ttf",
+ weight: "700",
+ style: "normal",
+ },
+ ],
+ variable: "--font-vazirmatn",
+ display: "swap",
+});
diff --git a/next.config.ts b/next.config.ts
index 1bb1a86fdd..2db2efc13f 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,4 +1,5 @@
import type { NextConfig } from "next";
+import createNextIntlPlugin from 'next-intl/plugin';
const nextConfig: NextConfig = {
cacheComponents: true,
@@ -16,4 +17,5 @@ const nextConfig: NextConfig = {
},
};
-export default nextConfig;
+const withNextIntl = createNextIntlPlugin();
+export default withNextIntl(nextConfig);
diff --git a/package.json b/package.json
index 5ca72b5f17..0686b71b3d 100644
--- a/package.json
+++ b/package.json
@@ -68,6 +68,7 @@
"nanoid": "^5.1.3",
"next": "16.0.10",
"next-auth": "5.0.0-beta.25",
+ "next-intl": "^4.8.2",
"next-themes": "^0.3.0",
"orderedmap": "^2.1.1",
"papaparse": "^5.5.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ec703b5cd7..59bd1303ea 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -158,6 +158,9 @@ importers:
next-auth:
specifier: 5.0.0-beta.25
version: 5.0.0-beta.25(next@16.0.10(@opentelemetry/api@1.9.0)(@playwright/test@1.51.0)(react-dom@19.0.1(react@19.0.1))(react@19.0.1))(react@19.0.1)
+ next-intl:
+ specifier: ^4.8.2
+ version: 4.8.2(next@16.0.10(@opentelemetry/api@1.9.0)(@playwright/test@1.51.0)(react-dom@19.0.1(react@19.0.1))(react@19.0.1))(react@19.0.1)(typescript@5.8.2)
next-themes:
specifier: ^0.3.0
version: 0.3.0(react-dom@19.0.1(react@19.0.1))(react@19.0.1)
@@ -1045,6 +1048,24 @@ packages:
'@floating-ui/utils@0.2.9':
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
+ '@formatjs/ecma402-abstract@3.1.1':
+ resolution: {integrity: sha512-jhZbTwda+2tcNrs4kKvxrPLPjx8QsBCLCUgrrJ/S+G9YrGHWLhAyFMMBHJBnBoOwuLHd7L14FgYudviKaxkO2Q==}
+
+ '@formatjs/fast-memoize@3.1.0':
+ resolution: {integrity: sha512-b5mvSWCI+XVKiz5WhnBCY3RJ4ZwfjAidU0yVlKa3d3MSgKmH1hC3tBGEAtYyN5mqL7N0G5x0BOUYyO8CEupWgg==}
+
+ '@formatjs/icu-messageformat-parser@3.5.1':
+ resolution: {integrity: sha512-sSDmSvmmoVQ92XqWb499KrIhv/vLisJU8ITFrx7T7NZHUmMY7EL9xgRowAosaljhqnj/5iufG24QrdzB6X3ItA==}
+
+ '@formatjs/icu-skeleton-parser@2.1.1':
+ resolution: {integrity: sha512-PSFABlcNefjI6yyk8f7nyX1DC7NHmq6WaCHZLySEXBrXuLOB2f935YsnzuPjlz+ibhb9yWTdPeVX1OVcj24w2Q==}
+
+ '@formatjs/intl-localematcher@0.5.10':
+ resolution: {integrity: sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==}
+
+ '@formatjs/intl-localematcher@0.8.1':
+ resolution: {integrity: sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA==}
+
'@icons-pack/react-simple-icons@13.8.0':
resolution: {integrity: sha512-iZrhL1fSklfCCVn68IYHaAoKfcby3RakUTn2tRPyHBkhr2tkYqeQbjJWf+NizIYBzKBn2IarDJXmTdXd6CuEfw==}
peerDependencies:
@@ -1346,6 +1367,88 @@ packages:
'@panva/hkdf@1.2.1':
resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
+ '@parcel/watcher-android-arm64@2.5.6':
+ resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ '@parcel/watcher-darwin-arm64@2.5.6':
+ resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@parcel/watcher-darwin-x64@2.5.6':
+ resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@parcel/watcher-freebsd-x64@2.5.6':
+ resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@parcel/watcher-linux-arm-glibc@2.5.6':
+ resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ '@parcel/watcher-linux-arm-musl@2.5.6':
+ resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ '@parcel/watcher-linux-arm64-glibc@2.5.6':
+ resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@parcel/watcher-linux-arm64-musl@2.5.6':
+ resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@parcel/watcher-linux-x64-glibc@2.5.6':
+ resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ '@parcel/watcher-linux-x64-musl@2.5.6':
+ resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ '@parcel/watcher-win32-arm64@2.5.6':
+ resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@parcel/watcher-win32-ia32@2.5.6':
+ resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@parcel/watcher-win32-x64@2.5.6':
+ resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ '@parcel/watcher@2.5.6':
+ resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==}
+ engines: {node: '>= 10.0.0'}
+
'@playwright/test@1.51.0':
resolution: {integrity: sha512-dJ0dMbZeHhI+wb77+ljx/FeC8VBP6j/rj9OAojO08JI80wTZy6vRk9KvHKiDCUh4iMpEiseMgqRBIeW+eKX6RA==}
engines: {node: '>=18'}
@@ -2175,6 +2278,9 @@ packages:
peerDependencies:
'@redis/client': ^5.9.0
+ '@schummar/icu-type-parser@1.21.5':
+ resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==}
+
'@shikijs/core@3.21.0':
resolution: {integrity: sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA==}
@@ -2199,9 +2305,84 @@ packages:
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+ '@swc/core-darwin-arm64@1.15.11':
+ resolution: {integrity: sha512-QoIupRWVH8AF1TgxYyeA5nS18dtqMuxNwchjBIwJo3RdwLEFiJq6onOx9JAxHtuPwUkIVuU2Xbp+jCJ7Vzmgtg==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@swc/core-darwin-x64@1.15.11':
+ resolution: {integrity: sha512-S52Gu1QtPSfBYDiejlcfp9GlN+NjTZBRRNsz8PNwBgSE626/FUf2PcllVUix7jqkoMC+t0rS8t+2/aSWlMuQtA==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@swc/core-linux-arm-gnueabihf@1.15.11':
+ resolution: {integrity: sha512-lXJs8oXo6Z4yCpimpQ8vPeCjkgoHu5NoMvmJZ8qxDyU99KVdg6KwU9H79vzrmB+HfH+dCZ7JGMqMF//f8Cfvdg==}
+ engines: {node: '>=10'}
+ cpu: [arm]
+ os: [linux]
+
+ '@swc/core-linux-arm64-gnu@1.15.11':
+ resolution: {integrity: sha512-chRsz1K52/vj8Mfq/QOugVphlKPWlMh10V99qfH41hbGvwAU6xSPd681upO4bKiOr9+mRIZZW+EfJqY42ZzRyA==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@swc/core-linux-arm64-musl@1.15.11':
+ resolution: {integrity: sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@swc/core-linux-x64-gnu@1.15.11':
+ resolution: {integrity: sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@swc/core-linux-x64-musl@1.15.11':
+ resolution: {integrity: sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@swc/core-win32-arm64-msvc@1.15.11':
+ resolution: {integrity: sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@swc/core-win32-ia32-msvc@1.15.11':
+ resolution: {integrity: sha512-6XnzORkZCQzvTQ6cPrU7iaT9+i145oLwnin8JrfsLG41wl26+5cNQ2XV3zcbrnFEV6esjOceom9YO1w9mGJByw==}
+ engines: {node: '>=10'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@swc/core-win32-x64-msvc@1.15.11':
+ resolution: {integrity: sha512-IQ2n6af7XKLL6P1gIeZACskSxK8jWtoKpJWLZmdXTDj1MGzktUy4i+FvpdtxFmJWNavRWH1VmTr6kAubRDHeKw==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@swc/core@1.15.11':
+ resolution: {integrity: sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@swc/helpers': '>=0.5.17'
+ peerDependenciesMeta:
+ '@swc/helpers':
+ optional: true
+
+ '@swc/counter@0.1.3':
+ resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
+
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
+ '@swc/types@0.1.25':
+ resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==}
+
'@tailwindcss/node@4.1.16':
resolution: {integrity: sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==}
@@ -2460,6 +2641,7 @@ packages:
'@vercel/postgres@0.10.0':
resolution: {integrity: sha512-fSD23DxGND40IzSkXjcFcxr53t3Tiym59Is0jSYIFpG4/0f0KO9SGtcp1sXiebvPaGe7N/tU05cH4yt2S6/IPg==}
engines: {node: '>=18.14'}
+ deprecated: '@vercel/postgres is deprecated. If you are setting up a new database, you can choose an alternate storage solution from the Vercel Marketplace. If you had an existing Vercel Postgres database, it should have been migrated to Neon as a native Vercel integration. You can find more details and the guide to migrate to Neon''s SDKs here: https://neon.com/docs/guides/vercel-postgres-transition-guide'
'@xyflow/react@12.10.0':
resolution: {integrity: sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw==}
@@ -2671,6 +2853,9 @@ packages:
supports-color:
optional: true
+ decimal.js@10.6.0:
+ resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
+
decode-named-character-reference@1.1.0:
resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==}
@@ -2997,12 +3182,18 @@ packages:
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
+ icu-minify@4.8.2:
+ resolution: {integrity: sha512-LHBQV+skKkjZSPd590pZ7ZAHftUgda3eFjeuNwA8/15L8T8loCNBktKQyTlkodAU86KovFXeg/9WntlAo5wA5A==}
+
import-in-the-middle@1.15.0:
resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==}
inline-style-parser@0.2.4:
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
+ intl-messageformat@11.1.2:
+ resolution: {integrity: sha512-ucSrQmZGAxfiBHfBRXW/k7UC8MaGFlEj4Ry1tKiDcmgwQm1y3EDl40u+4VNHYomxJQMJi9NEI3riDRlth96jKg==}
+
is-alphabetical@1.0.4:
resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==}
@@ -3029,6 +3220,14 @@ packages:
is-decimal@2.0.1:
resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
is-hexadecimal@1.0.4:
resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==}
@@ -3397,6 +3596,10 @@ packages:
engines: {node: ^18 || >=20}
hasBin: true
+ negotiator@1.0.0:
+ resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
+ engines: {node: '>= 0.6'}
+
next-auth@5.0.0-beta.25:
resolution: {integrity: sha512-2dJJw1sHQl2qxCrRk+KTQbeH+izFbGFPuJj5eGgBZFYyiYYtvlrBeUw1E/OJJxTRjuxbSYGnCTkUIRsIIW0bog==}
peerDependencies:
@@ -3413,6 +3616,19 @@ packages:
nodemailer:
optional: true
+ next-intl-swc-plugin-extractor@4.8.2:
+ resolution: {integrity: sha512-sHDs36L1VZmFHj3tPHsD+KZJtnsRudHlNvT0ieIe3iFVn5OpGLTxW3d/Zc/2LXSj5GpGuR6wQeikbhFjU9tMQQ==}
+
+ next-intl@4.8.2:
+ resolution: {integrity: sha512-GuuwyvyEI49/oehQbBXEoY8KSIYCzmfMLhmIwhMXTb+yeBmly1PnJcpgph3KczQ+HTJMXwXCmkizgtT8jBMf3A==}
+ peerDependencies:
+ next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
+ typescript: ^5.0.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
next-themes@0.3.0:
resolution: {integrity: sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==}
peerDependencies:
@@ -3440,6 +3656,9 @@ packages:
sass:
optional: true
+ node-addon-api@7.1.1:
+ resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
+
node-gyp-build@4.8.4:
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
hasBin: true
@@ -3504,6 +3723,10 @@ packages:
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+ picomatch@4.0.3:
+ resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
+ engines: {node: '>=12'}
+
pkg-types@2.3.0:
resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
@@ -3517,6 +3740,9 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ po-parser@2.1.1:
+ resolution: {integrity: sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==}
+
postcss-selector-parser@6.0.10:
resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
engines: {node: '>=4'}
@@ -3995,6 +4221,11 @@ packages:
'@types/react':
optional: true
+ use-intl@4.8.2:
+ resolution: {integrity: sha512-3VNXZgDnPFqhIYosQ9W1Hc6K5q+ZelMfawNbexdwL/dY7BTHbceLUBX5Eeex9lgogxTp0pf1SjHuhYNAjr9H3g==}
+ peerDependencies:
+ react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
+
use-sidecar@1.1.3:
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
engines: {node: '>=10'}
@@ -4568,6 +4799,37 @@ snapshots:
'@floating-ui/utils@0.2.9': {}
+ '@formatjs/ecma402-abstract@3.1.1':
+ dependencies:
+ '@formatjs/fast-memoize': 3.1.0
+ '@formatjs/intl-localematcher': 0.8.1
+ decimal.js: 10.6.0
+ tslib: 2.8.1
+
+ '@formatjs/fast-memoize@3.1.0':
+ dependencies:
+ tslib: 2.8.1
+
+ '@formatjs/icu-messageformat-parser@3.5.1':
+ dependencies:
+ '@formatjs/ecma402-abstract': 3.1.1
+ '@formatjs/icu-skeleton-parser': 2.1.1
+ tslib: 2.8.1
+
+ '@formatjs/icu-skeleton-parser@2.1.1':
+ dependencies:
+ '@formatjs/ecma402-abstract': 3.1.1
+ tslib: 2.8.1
+
+ '@formatjs/intl-localematcher@0.5.10':
+ dependencies:
+ tslib: 2.8.1
+
+ '@formatjs/intl-localematcher@0.8.1':
+ dependencies:
+ '@formatjs/fast-memoize': 3.1.0
+ tslib: 2.8.1
+
'@icons-pack/react-simple-icons@13.8.0(react@19.0.1)':
dependencies:
react: 19.0.1
@@ -4811,6 +5073,66 @@ snapshots:
'@panva/hkdf@1.2.1': {}
+ '@parcel/watcher-android-arm64@2.5.6':
+ optional: true
+
+ '@parcel/watcher-darwin-arm64@2.5.6':
+ optional: true
+
+ '@parcel/watcher-darwin-x64@2.5.6':
+ optional: true
+
+ '@parcel/watcher-freebsd-x64@2.5.6':
+ optional: true
+
+ '@parcel/watcher-linux-arm-glibc@2.5.6':
+ optional: true
+
+ '@parcel/watcher-linux-arm-musl@2.5.6':
+ optional: true
+
+ '@parcel/watcher-linux-arm64-glibc@2.5.6':
+ optional: true
+
+ '@parcel/watcher-linux-arm64-musl@2.5.6':
+ optional: true
+
+ '@parcel/watcher-linux-x64-glibc@2.5.6':
+ optional: true
+
+ '@parcel/watcher-linux-x64-musl@2.5.6':
+ optional: true
+
+ '@parcel/watcher-win32-arm64@2.5.6':
+ optional: true
+
+ '@parcel/watcher-win32-ia32@2.5.6':
+ optional: true
+
+ '@parcel/watcher-win32-x64@2.5.6':
+ optional: true
+
+ '@parcel/watcher@2.5.6':
+ dependencies:
+ detect-libc: 2.1.2
+ is-glob: 4.0.3
+ node-addon-api: 7.1.1
+ picomatch: 4.0.3
+ optionalDependencies:
+ '@parcel/watcher-android-arm64': 2.5.6
+ '@parcel/watcher-darwin-arm64': 2.5.6
+ '@parcel/watcher-darwin-x64': 2.5.6
+ '@parcel/watcher-freebsd-x64': 2.5.6
+ '@parcel/watcher-linux-arm-glibc': 2.5.6
+ '@parcel/watcher-linux-arm-musl': 2.5.6
+ '@parcel/watcher-linux-arm64-glibc': 2.5.6
+ '@parcel/watcher-linux-arm64-musl': 2.5.6
+ '@parcel/watcher-linux-x64-glibc': 2.5.6
+ '@parcel/watcher-linux-x64-musl': 2.5.6
+ '@parcel/watcher-win32-arm64': 2.5.6
+ '@parcel/watcher-win32-ia32': 2.5.6
+ '@parcel/watcher-win32-x64': 2.5.6
+
'@playwright/test@1.51.0':
dependencies:
playwright: 1.51.0
@@ -5658,6 +5980,8 @@ snapshots:
dependencies:
'@redis/client': 5.9.0
+ '@schummar/icu-type-parser@1.21.5': {}
+
'@shikijs/core@3.21.0':
dependencies:
'@shikijs/types': 3.21.0
@@ -5693,10 +6017,62 @@ snapshots:
'@standard-schema/spec@1.1.0': {}
+ '@swc/core-darwin-arm64@1.15.11':
+ optional: true
+
+ '@swc/core-darwin-x64@1.15.11':
+ optional: true
+
+ '@swc/core-linux-arm-gnueabihf@1.15.11':
+ optional: true
+
+ '@swc/core-linux-arm64-gnu@1.15.11':
+ optional: true
+
+ '@swc/core-linux-arm64-musl@1.15.11':
+ optional: true
+
+ '@swc/core-linux-x64-gnu@1.15.11':
+ optional: true
+
+ '@swc/core-linux-x64-musl@1.15.11':
+ optional: true
+
+ '@swc/core-win32-arm64-msvc@1.15.11':
+ optional: true
+
+ '@swc/core-win32-ia32-msvc@1.15.11':
+ optional: true
+
+ '@swc/core-win32-x64-msvc@1.15.11':
+ optional: true
+
+ '@swc/core@1.15.11':
+ dependencies:
+ '@swc/counter': 0.1.3
+ '@swc/types': 0.1.25
+ optionalDependencies:
+ '@swc/core-darwin-arm64': 1.15.11
+ '@swc/core-darwin-x64': 1.15.11
+ '@swc/core-linux-arm-gnueabihf': 1.15.11
+ '@swc/core-linux-arm64-gnu': 1.15.11
+ '@swc/core-linux-arm64-musl': 1.15.11
+ '@swc/core-linux-x64-gnu': 1.15.11
+ '@swc/core-linux-x64-musl': 1.15.11
+ '@swc/core-win32-arm64-msvc': 1.15.11
+ '@swc/core-win32-ia32-msvc': 1.15.11
+ '@swc/core-win32-x64-msvc': 1.15.11
+
+ '@swc/counter@0.1.3': {}
+
'@swc/helpers@0.5.15':
dependencies:
tslib: 2.8.1
+ '@swc/types@0.1.25':
+ dependencies:
+ '@swc/counter': 0.1.3
+
'@tailwindcss/node@4.1.16':
dependencies:
'@jridgewell/remapping': 2.3.5
@@ -6110,6 +6486,8 @@ snapshots:
dependencies:
ms: 2.1.3
+ decimal.js@10.6.0: {}
+
decode-named-character-reference@1.1.0:
dependencies:
character-entities: 2.0.2
@@ -6497,6 +6875,10 @@ snapshots:
html-void-elements@3.0.0: {}
+ icu-minify@4.8.2:
+ dependencies:
+ '@formatjs/icu-messageformat-parser': 3.5.1
+
import-in-the-middle@1.15.0:
dependencies:
acorn: 8.15.0
@@ -6506,6 +6888,13 @@ snapshots:
inline-style-parser@0.2.4: {}
+ intl-messageformat@11.1.2:
+ dependencies:
+ '@formatjs/ecma402-abstract': 3.1.1
+ '@formatjs/fast-memoize': 3.1.0
+ '@formatjs/icu-messageformat-parser': 3.5.1
+ tslib: 2.8.1
+
is-alphabetical@1.0.4: {}
is-alphabetical@2.0.1: {}
@@ -6530,6 +6919,12 @@ snapshots:
is-decimal@2.0.1: {}
+ is-extglob@2.1.1: {}
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
is-hexadecimal@1.0.4: {}
is-hexadecimal@2.0.1: {}
@@ -7079,12 +7474,33 @@ snapshots:
nanoid@5.1.3: {}
+ negotiator@1.0.0: {}
+
next-auth@5.0.0-beta.25(next@16.0.10(@opentelemetry/api@1.9.0)(@playwright/test@1.51.0)(react-dom@19.0.1(react@19.0.1))(react@19.0.1))(react@19.0.1):
dependencies:
'@auth/core': 0.37.2
next: 16.0.10(@opentelemetry/api@1.9.0)(@playwright/test@1.51.0)(react-dom@19.0.1(react@19.0.1))(react@19.0.1)
react: 19.0.1
+ next-intl-swc-plugin-extractor@4.8.2: {}
+
+ next-intl@4.8.2(next@16.0.10(@opentelemetry/api@1.9.0)(@playwright/test@1.51.0)(react-dom@19.0.1(react@19.0.1))(react@19.0.1))(react@19.0.1)(typescript@5.8.2):
+ dependencies:
+ '@formatjs/intl-localematcher': 0.5.10
+ '@parcel/watcher': 2.5.6
+ '@swc/core': 1.15.11
+ icu-minify: 4.8.2
+ negotiator: 1.0.0
+ next: 16.0.10(@opentelemetry/api@1.9.0)(@playwright/test@1.51.0)(react-dom@19.0.1(react@19.0.1))(react@19.0.1)
+ next-intl-swc-plugin-extractor: 4.8.2
+ po-parser: 2.1.1
+ react: 19.0.1
+ use-intl: 4.8.2(react@19.0.1)
+ optionalDependencies:
+ typescript: 5.8.2
+ transitivePeerDependencies:
+ - '@swc/helpers'
+
next-themes@0.3.0(react-dom@19.0.1(react@19.0.1))(react@19.0.1):
dependencies:
react: 19.0.1
@@ -7115,6 +7531,8 @@ snapshots:
- '@babel/core'
- babel-plugin-macros
+ node-addon-api@7.1.1: {}
+
node-gyp-build@4.8.4:
optional: true
@@ -7197,6 +7615,8 @@ snapshots:
picocolors@1.1.1: {}
+ picomatch@4.0.3: {}
+
pkg-types@2.3.0:
dependencies:
confbox: 0.2.2
@@ -7211,6 +7631,8 @@ snapshots:
optionalDependencies:
fsevents: 2.3.2
+ po-parser@2.1.1: {}
+
postcss-selector-parser@6.0.10:
dependencies:
cssesc: 3.0.0
@@ -7857,6 +8279,14 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.18
+ use-intl@4.8.2(react@19.0.1):
+ dependencies:
+ '@formatjs/fast-memoize': 3.1.0
+ '@schummar/icu-type-parser': 1.21.5
+ icu-minify: 4.8.2
+ intl-messageformat: 11.1.2
+ react: 19.0.1
+
use-sidecar@1.1.3(@types/react@18.3.18)(react@19.0.1):
dependencies:
detect-node-es: 1.1.0
diff --git a/proxy.ts b/proxy.ts
index ca5a19ddae..b6dd772d28 100644
--- a/proxy.ts
+++ b/proxy.ts
@@ -1,6 +1,10 @@
import { type NextRequest, NextResponse } from "next/server";
+import createIntlMiddleware from 'next-intl/middleware';
import { getToken } from "next-auth/jwt";
import { guestRegex, isDevelopmentEnvironment } from "./lib/constants";
+import { routing } from "./i18n/routing";
+
+const intlMiddleware = createIntlMiddleware(routing);
export async function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
@@ -13,10 +17,41 @@ export async function proxy(request: NextRequest) {
return new Response("pong", { status: 200 });
}
- if (pathname.startsWith("/api/auth")) {
+ // Check if this is an API route (with or without locale prefix)
+ const isApiRoute = pathname.startsWith("/api") ||
+ routing.locales.some(locale => pathname.startsWith(`/${locale}/api`));
+
+ const isApiAuthRoute = pathname.startsWith("/api/auth") ||
+ routing.locales.some(locale => pathname.startsWith(`/${locale}/api/auth`));
+
+ // Handle API routes
+ if (isApiRoute) {
+ if (isApiAuthRoute) {
+ return NextResponse.next();
+ }
+
+ const token = await getToken({
+ req: request,
+ secret: process.env.AUTH_SECRET,
+ secureCookie: !isDevelopmentEnvironment,
+ });
+
+ if (!token) {
+ const redirectUrl = encodeURIComponent(request.url);
+
+ return NextResponse.redirect(
+ new URL(`/api/auth/guest?redirectUrl=${redirectUrl}`, request.url)
+ );
+ }
+
return NextResponse.next();
}
+ // Skip auth checks for API auth routes
+ if (isApiAuthRoute) {
+ return intlMiddleware(request);
+ }
+
const token = await getToken({
req: request,
secret: process.env.AUTH_SECRET,
@@ -33,18 +68,27 @@ export async function proxy(request: NextRequest) {
const isGuest = guestRegex.test(token?.email ?? "");
- if (token && !isGuest && ["/login", "/register"].includes(pathname)) {
+ // Check for login/register with or without locale prefix
+ const isLoginOrRegister = pathname === "/login" || pathname === "/register" ||
+ routing.locales.some(locale => pathname === `/${locale}/login` || pathname === `/${locale}/register`);
+
+ if (token && !isGuest && isLoginOrRegister) {
return NextResponse.redirect(new URL("/", request.url));
}
- return NextResponse.next();
+ // Apply i18n middleware for page routes
+ return intlMiddleware(request);
}
export const config = {
matcher: [
+ // Root path
"/",
- "/chat/:id",
+ // Locale paths
+ "/:locale(en|fa)/:path*",
+ // API paths
"/api/:path*",
+ // Auth paths (without locale)
"/login",
"/register",
@@ -53,7 +97,8 @@ export const config = {
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
+ * - fonts (font files)
*/
- "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
+ "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|fonts).*)",
],
};
diff --git a/public/fonts/Vazirmatn-Bold.ttf b/public/fonts/Vazirmatn-Bold.ttf
new file mode 100644
index 0000000000..efa9b095da
Binary files /dev/null and b/public/fonts/Vazirmatn-Bold.ttf differ
diff --git a/public/fonts/Vazirmatn-Regular.ttf b/public/fonts/Vazirmatn-Regular.ttf
new file mode 100644
index 0000000000..64e4a81895
Binary files /dev/null and b/public/fonts/Vazirmatn-Regular.ttf differ