diff --git a/surfsense_web/.gitignore b/surfsense_web/.gitignore index 8c6362124..6ae7fe0c4 100644 --- a/surfsense_web/.gitignore +++ b/surfsense_web/.gitignore @@ -47,3 +47,6 @@ next-env.d.ts # source /.source/ + +.pnpm-store/ + diff --git a/surfsense_web/app/(home)/login/GoogleLoginButton.tsx b/surfsense_web/app/(home)/login/GoogleLoginButton.tsx index 545f279e7..d1b5b1ccc 100644 --- a/surfsense_web/app/(home)/login/GoogleLoginButton.tsx +++ b/surfsense_web/app/(home)/login/GoogleLoginButton.tsx @@ -1,10 +1,13 @@ "use client"; import { IconBrandGoogleFilled } from "@tabler/icons-react"; import { motion } from "motion/react"; +import { useTranslations } from "next-intl"; import { Logo } from "@/components/Logo"; import { AmbientBackground } from "./AmbientBackground"; export function GoogleLoginButton() { + const t = useTranslations('auth'); + const handleGoogleLogin = () => { // Redirect to Google OAuth authorization URL fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/auth/google/authorize`) @@ -31,7 +34,7 @@ export function GoogleLoginButton() {

- Welcome Back + {t('welcome_back')}

- SurfSense Cloud is currently in development. Check{" "} + {t('cloud_dev_notice')}{" "} - Docs + {t('docs')} {" "} - for more information on Self-Hosted version. + {t('cloud_dev_self_hosted')}

@@ -91,7 +94,7 @@ export function GoogleLoginButton() {
- Continue with Google + {t('continue_with_google')} diff --git a/surfsense_web/app/(home)/login/LocalLoginForm.tsx b/surfsense_web/app/(home)/login/LocalLoginForm.tsx index 21737714c..93720cfd1 100644 --- a/surfsense_web/app/(home)/login/LocalLoginForm.tsx +++ b/surfsense_web/app/(home)/login/LocalLoginForm.tsx @@ -5,9 +5,12 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { toast } from "sonner"; +import { useTranslations } from "next-intl"; import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-errors"; export function LocalLoginForm() { + const t = useTranslations('auth'); + const tCommon = useTranslations('common'); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [showPassword, setShowPassword] = useState(false); @@ -29,7 +32,7 @@ export function LocalLoginForm() { setErrorTitle(null); // Show loading toast - const loadingToast = toast.loading("Signing you in..."); + const loadingToast = toast.loading(tCommon('loading')); try { // Create form data for the API request @@ -56,7 +59,7 @@ export function LocalLoginForm() { } // Success toast - toast.success("Login successful!", { + toast.success(t('login_success'), { id: loadingToast, description: "Redirecting to dashboard...", duration: 2000, @@ -167,84 +170,84 @@ export function LocalLoginForm() { )} - + -
- +
+ + setUsername(e.target.value)} + className={`mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-colors ${ + error + ? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700" + : "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700" + }`} + disabled={isLoading} + /> +
+ +
+ +
setUsername(e.target.value)} - className={`mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-colors ${ + value={password} + onChange={(e) => setPassword(e.target.value)} + className={`mt-1 block w-full rounded-md border pr-10 px-3 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-colors ${ error ? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700" : "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700" }`} disabled={isLoading} /> -
- -
- -
- setPassword(e.target.value)} - className={`mt-1 block w-full rounded-md border pr-10 px-3 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-colors ${ - error - ? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700" - : "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700" - }`} - disabled={isLoading} - /> - -
+ {showPassword ? : } +
+
- - + + - {authType === "LOCAL" && ( -
-

- Don't have an account?{" "} - - Register here - -

-
- )} + {authType === "LOCAL" && ( +
+

+ {t('dont_have_account')}{" "} + + {t('sign_up')} + +

+
+ )}
); } diff --git a/surfsense_web/app/(home)/login/page.tsx b/surfsense_web/app/(home)/login/page.tsx index c3f5501a6..8ce073d54 100644 --- a/surfsense_web/app/(home)/login/page.tsx +++ b/surfsense_web/app/(home)/login/page.tsx @@ -5,6 +5,7 @@ import { AnimatePresence, motion } from "motion/react"; import { useSearchParams } from "next/navigation"; import { Suspense, useEffect, useState } from "react"; import { toast } from "sonner"; +import { useTranslations } from "next-intl"; import { Logo } from "@/components/Logo"; import { getAuthErrorDetails, shouldRetry } from "@/lib/auth-errors"; import { AmbientBackground } from "./AmbientBackground"; @@ -12,6 +13,8 @@ import { GoogleLoginButton } from "./GoogleLoginButton"; import { LocalLoginForm } from "./LocalLoginForm"; function LoginContent() { + const t = useTranslations('auth'); + const tCommon = useTranslations('common'); const [authType, setAuthType] = useState(null); const [isLoading, setIsLoading] = useState(true); const [urlError, setUrlError] = useState<{ title: string; message: string } | null>(null); @@ -26,15 +29,15 @@ function LoginContent() { // Show registration success message if (registered === "true") { - toast.success("Registration successful!", { - description: "You can now sign in with your credentials", + toast.success(t('register_success'), { + description: t('login_subtitle'), duration: 5000, }); } // Show logout confirmation if (logout === "true") { - toast.success("Logged out successfully", { + toast.success(tCommon('success'), { description: "You have been securely logged out", duration: 3000, }); @@ -93,7 +96,7 @@ function LoginContent() {
- Loading... + {tCommon('loading')}
@@ -110,7 +113,7 @@ function LoginContent() {

- Sign In + {t('sign_in')}

{/* URL Error Display */} diff --git a/surfsense_web/app/(home)/register/page.tsx b/surfsense_web/app/(home)/register/page.tsx index c10f92b71..701d0169a 100644 --- a/surfsense_web/app/(home)/register/page.tsx +++ b/surfsense_web/app/(home)/register/page.tsx @@ -5,11 +5,14 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { toast } from "sonner"; +import { useTranslations } from "next-intl"; import { Logo } from "@/components/Logo"; import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-errors"; import { AmbientBackground } from "../login/AmbientBackground"; export default function RegisterPage() { + const t = useTranslations('auth'); + const tCommon = useTranslations('common'); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); @@ -31,10 +34,10 @@ export default function RegisterPage() { // Form validation if (password !== confirmPassword) { - setError("Passwords do not match"); - setErrorTitle("Password Mismatch"); - toast.error("Password Mismatch", { - description: "The passwords you entered do not match", + setError(t('passwords_no_match')); + setErrorTitle(t('password_mismatch')); + toast.error(t('password_mismatch'), { + description: t('passwords_no_match_desc'), duration: 4000, }); return; @@ -45,7 +48,7 @@ export default function RegisterPage() { setErrorTitle(null); // Show loading toast - const loadingToast = toast.loading("Creating your account..."); + const loadingToast = toast.loading(t('creating_account')); try { const response = await fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/auth/register`, { @@ -83,9 +86,9 @@ export default function RegisterPage() { } // Success toast - toast.success("Account created successfully!", { + toast.success(t('register_success'), { id: loadingToast, - description: "Redirecting to login page...", + description: t('redirecting_login'), duration: 2000, }); @@ -120,7 +123,7 @@ export default function RegisterPage() { // Add retry action if the error is retryable if (shouldRetry(errorCode)) { toastOptions.action = { - label: "Retry", + label: tCommon('retry'), onClick: () => handleSubmit(e), }; } @@ -137,7 +140,7 @@ export default function RegisterPage() {

- Create an Account + {t('create_account')}

@@ -209,7 +212,7 @@ export default function RegisterPage() { htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300" > - Email + {t('email')} - Password + {t('password')} - Confirm Password + {t('confirm_password')} - {isLoading ? "Creating account..." : "Register"} + {isLoading ? t('creating_account_btn') : t('register')}

- Already have an account?{" "} + {t('already_have_account')}{" "} - Sign in + {t('sign_in')}

diff --git a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx index f3c1531a8..11ef393a4 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx @@ -3,7 +3,8 @@ import { Loader2 } from "lucide-react"; import { usePathname, useRouter } from "next/navigation"; import type React from "react"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; +import { useTranslations } from "next-intl"; import { DashboardBreadcrumb } from "@/components/dashboard-breadcrumb"; import { AppSidebarProvider } from "@/components/sidebar/AppSidebarProvider"; import { ThemeTogglerComponent } from "@/components/theme/theme-toggle"; @@ -11,6 +12,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Separator } from "@/components/ui/separator"; import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; import { useLLMPreferences } from "@/hooks/use-llm-configs"; +import { LanguageSwitcher } from "@/components/LanguageSwitcher"; export function DashboardClientLayout({ children, @@ -23,6 +25,7 @@ export function DashboardClientLayout({ navSecondary: any[]; navMain: any[]; }) { + const t = useTranslations('dashboard'); const router = useRouter(); const pathname = usePathname(); const searchSpaceIdNum = Number(searchSpaceId); @@ -33,6 +36,26 @@ export function DashboardClientLayout({ // Skip onboarding check if we're already on the onboarding page const isOnboardingPage = pathname?.includes("/onboard"); + // Translate navigation items + const tNavMenu = useTranslations('nav_menu'); + const translatedNavMain = useMemo(() => { + return navMain.map((item) => ({ + ...item, + title: tNavMenu(item.title.toLowerCase().replace(/ /g, '_')), + items: item.items?.map((subItem: any) => ({ + ...subItem, + title: tNavMenu(subItem.title.toLowerCase().replace(/ /g, '_')), + })), + })); + }, [navMain, tNavMenu]); + + const translatedNavSecondary = useMemo(() => { + return navSecondary.map((item) => ({ + ...item, + title: item.title === 'All Search Spaces' ? tNavMenu('all_search_spaces') : item.title, + })); + }, [navSecondary, tNavMenu]); + const [open, setOpen] = useState(() => { try { const match = document.cookie.match(/(?:^|; )sidebar_state=([^;]+)/); @@ -75,8 +98,8 @@ export function DashboardClientLayout({
- Loading Configuration - Checking your LLM preferences... + {t('loading_config')} + {t('checking_llm_prefs')} @@ -93,9 +116,9 @@ export function DashboardClientLayout({ - Configuration Error + {t('config_error')} - Failed to load your LLM configuration + {t('failed_load_llm_config')}

{error}

@@ -110,8 +133,8 @@ export function DashboardClientLayout({ {/* Use AppSidebarProvider which fetches user, search space, and recent chats */}
@@ -121,7 +144,10 @@ export function DashboardClientLayout({
- +
+ + +
{children} diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx index 3537885ca..ffdb3247b 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx @@ -14,6 +14,7 @@ import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { toast } from "sonner"; +import { useTranslations } from "next-intl"; import { AlertDialog, AlertDialogAction, @@ -61,21 +62,23 @@ import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors"; import { cn } from "@/lib/utils"; -// Helper function to format date with time -const formatDateTime = (dateString: string | null): string => { - if (!dateString) return "Never"; - - const date = new Date(dateString); - return new Intl.DateTimeFormat("en-US", { - year: "numeric", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - }).format(date); -}; - export default function ConnectorsPage() { + const t = useTranslations('connectors'); + const tCommon = useTranslations('common'); + + // Helper function to format date with time + const formatDateTime = (dateString: string | null): string => { + if (!dateString) return t('never'); + + const date = new Date(dateString); + return new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }).format(date); + }; const router = useRouter(); const params = useParams(); const searchSpaceId = params.search_space_id as string; @@ -104,10 +107,10 @@ export default function ConnectorsPage() { useEffect(() => { if (error) { - toast.error("Failed to load connectors"); + toast.error(t('failed_load')); console.error("Error fetching connectors:", error); } - }, [error]); + }, [error, t]); // Handle connector deletion const handleDeleteConnector = async () => { @@ -115,10 +118,10 @@ export default function ConnectorsPage() { try { await deleteConnector(connectorToDelete); - toast.success("Connector deleted successfully"); + toast.success(t('delete_success')); } catch (error) { console.error("Error deleting connector:", error); - toast.error("Failed to delete connector"); + toast.error(t('delete_failed')); } finally { setConnectorToDelete(null); } @@ -142,10 +145,10 @@ export default function ConnectorsPage() { const endDateStr = endDate ? format(endDate, "yyyy-MM-dd") : undefined; await indexConnector(selectedConnectorForIndexing, searchSpaceId, startDateStr, endDateStr); - toast.success("Connector content indexing started"); + toast.success(t('indexing_started')); } catch (error) { console.error("Error indexing connector content:", error); - toast.error(error instanceof Error ? error.message : "Failed to index connector content"); + toast.error(error instanceof Error ? error.message : t('indexing_failed')); } finally { setIndexingConnectorId(null); setSelectedConnectorForIndexing(null); @@ -159,10 +162,10 @@ export default function ConnectorsPage() { setIndexingConnectorId(connectorId); try { await indexConnector(connectorId, searchSpaceId); - toast.success("Connector content indexing started"); + toast.success(t('indexing_started')); } catch (error) { console.error("Error indexing connector content:", error); - toast.error(error instanceof Error ? error.message : "Failed to index connector content"); + toast.error(error instanceof Error ? error.message : t('indexing_failed')); } finally { setIndexingConnectorId(null); } @@ -255,21 +258,21 @@ export default function ConnectorsPage() { className="mb-8 flex items-center justify-between" >
-

Connectors

+

{t('title')}

- Manage your connected services and data sources. + {t('subtitle')}

- Your Connectors - View and manage all your connected services. + {t('your_connectors')} + {t('view_manage')} {isLoading ? ( @@ -281,13 +284,13 @@ export default function ConnectorsPage() {
) : connectors.length === 0 ? (
-

No connectors found

+

{t('no_connectors')}

- You haven't added any connectors yet. Add one to enhance your search capabilities. + {t('no_connectors_desc')}

) : ( @@ -295,11 +298,11 @@ export default function ConnectorsPage() { - Name - Type - Last Indexed - Periodic - Actions + {t('name')} + {t('type')} + {t('last_indexed')} + {t('periodic')} + {t('actions')} @@ -310,7 +313,7 @@ export default function ConnectorsPage() { {connector.is_indexable ? formatDateTime(connector.last_indexed_at) - : "Not indexable"} + : t('not_indexable')} {connector.is_indexable ? ( @@ -365,11 +368,11 @@ export default function ConnectorsPage() { ) : ( )} - Index with Date Range + {t('index_date_range')} -

Index with Date Range

+

{t('index_date_range')}

@@ -387,11 +390,11 @@ export default function ConnectorsPage() { ) : ( )} - Quick Index + {t('quick_index')} -

Quick Index (Auto Date Range)

+

{t('quick_index_auto')}

@@ -426,7 +429,7 @@ export default function ConnectorsPage() { } > - Edit + {tCommon('edit')} @@ -437,26 +440,25 @@ export default function ConnectorsPage() { onClick={() => setConnectorToDelete(connector.id)} > - Delete + {tCommon('delete')} - Delete Connector + {t('delete_connector')} - Are you sure you want to delete this connector? This action cannot - be undone. + {t('delete_confirm')} setConnectorToDelete(null)}> - Cancel + {tCommon('cancel')} - Delete + {tCommon('delete')} @@ -476,15 +478,15 @@ export default function ConnectorsPage() { - Select Date Range for Indexing + {t('select_date_range')} - Choose the start and end dates for indexing content. Leave empty to use default range. + {t('select_date_range_desc')}
- + @@ -510,7 +512,7 @@ export default function ConnectorsPage() {
- + @@ -540,7 +542,7 @@ export default function ConnectorsPage() { setEndDate(undefined); }} > - Clear Dates + {t('clear_dates')}
@@ -578,9 +580,9 @@ export default function ConnectorsPage() { setEndDate(undefined); }} > - Cancel + {tCommon('cancel')} - +
diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/page.tsx index 7e63f5815..4937da17f 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/page.tsx @@ -10,6 +10,7 @@ import { AnimatePresence, motion, type Variants } from "motion/react"; import Link from "next/link"; import { useParams } from "next/navigation"; import { useState } from "react"; +import { useTranslations } from "next-intl"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"; @@ -36,40 +37,40 @@ interface ConnectorCategory { const connectorCategories: ConnectorCategory[] = [ { id: "search-engines", - title: "Search Engines", + title: "search_engines", connectors: [ { id: "tavily-api", title: "Tavily API", - description: "Search the web using the Tavily API", + description: "tavily_desc", icon: getConnectorIcon(EnumConnectorName.TAVILY_API, "h-6 w-6"), status: "available", }, { id: "searxng", title: "SearxNG", - description: "Use your own SearxNG meta-search instance for web results.", + description: "searxng_desc", icon: getConnectorIcon(EnumConnectorName.SEARXNG_API, "h-6 w-6"), status: "available", }, { id: "linkup-api", title: "Linkup API", - description: "Search the web using the Linkup API", + description: "linkup_desc", icon: getConnectorIcon(EnumConnectorName.LINKUP_API, "h-6 w-6"), status: "available", }, { id: "elasticsearch-connector", title: "Elasticsearch", - description: "Connect to Elasticsearch to index and search documents, logs and metrics.", + description: "elasticsearch_desc", icon: getConnectorIcon(EnumConnectorName.ELASTICSEARCH_CONNECTOR, "h-6 w-6"), status: "available", }, { id: "baidu-search-api", title: "Baidu Search", - description: "Search the Chinese web using Baidu AI Search API", + description: "baidu_desc", icon: getConnectorIcon(EnumConnectorName.BAIDU_SEARCH_API, "h-6 w-6"), status: "available", }, @@ -77,26 +78,26 @@ const connectorCategories: ConnectorCategory[] = [ }, { id: "team-chats", - title: "Team Chats", + title: "team_chats", connectors: [ { id: "slack-connector", title: "Slack", - description: "Connect to your Slack workspace to access messages and channels.", + description: "slack_desc", icon: getConnectorIcon(EnumConnectorName.SLACK_CONNECTOR, "h-6 w-6"), status: "available", }, { id: "ms-teams", title: "Microsoft Teams", - description: "Connect to Microsoft Teams to access your team's conversations.", + description: "teams_desc", icon: , status: "coming-soon", }, { id: "discord-connector", title: "Discord", - description: "Connect to Discord servers to access messages and channels.", + description: "discord_desc", icon: getConnectorIcon(EnumConnectorName.DISCORD_CONNECTOR, "h-6 w-6"), status: "available", }, @@ -104,26 +105,26 @@ const connectorCategories: ConnectorCategory[] = [ }, { id: "project-management", - title: "Project Management", + title: "project_management", connectors: [ { id: "linear-connector", title: "Linear", - description: "Connect to Linear to search issues, comments and project data.", + description: "linear_desc", icon: getConnectorIcon(EnumConnectorName.LINEAR_CONNECTOR, "h-6 w-6"), status: "available", }, { id: "jira-connector", title: "Jira", - description: "Connect to Jira to search issues, tickets and project data.", + description: "jira_desc", icon: getConnectorIcon(EnumConnectorName.JIRA_CONNECTOR, "h-6 w-6"), status: "available", }, { id: "clickup-connector", title: "ClickUp", - description: "Connect to ClickUp to search tasks, comments and project data.", + description: "clickup_desc", icon: getConnectorIcon(EnumConnectorName.CLICKUP_CONNECTOR, "h-6 w-6"), status: "available", }, @@ -131,40 +132,40 @@ const connectorCategories: ConnectorCategory[] = [ }, { id: "knowledge-bases", - title: "Knowledge Bases", + title: "knowledge_bases", connectors: [ { id: "notion-connector", title: "Notion", - description: "Connect to your Notion workspace to access pages and databases.", + description: "notion_desc", icon: getConnectorIcon(EnumConnectorName.NOTION_CONNECTOR, "h-6 w-6"), status: "available", }, { id: "github-connector", title: "GitHub", - description: "Connect a GitHub PAT to index code and docs from accessible repositories.", + description: "github_desc", icon: getConnectorIcon(EnumConnectorName.GITHUB_CONNECTOR, "h-6 w-6"), status: "available", }, { id: "confluence-connector", title: "Confluence", - description: "Connect to Confluence to search pages, comments and documentation.", + description: "confluence_desc", icon: getConnectorIcon(EnumConnectorName.CONFLUENCE_CONNECTOR, "h-6 w-6"), status: "available", }, { id: "airtable-connector", title: "Airtable", - description: "Connect to Airtable to search records, tables and database content.", + description: "airtable_desc", icon: getConnectorIcon(EnumConnectorName.AIRTABLE_CONNECTOR, "h-6 w-6"), status: "available", }, { id: "luma-connector", title: "Luma", - description: "Connect to Luma to search events", + description: "luma_desc", icon: getConnectorIcon(EnumConnectorName.LUMA_CONNECTOR, "h-6 w-6"), status: "available", }, @@ -172,26 +173,26 @@ const connectorCategories: ConnectorCategory[] = [ }, { id: "communication", - title: "Communication", + title: "communication", connectors: [ { id: "google-calendar-connector", title: "Google Calendar", - description: "Connect to Google Calendar to search events, meetings and schedules.", + description: "calendar_desc", icon: getConnectorIcon(EnumConnectorName.GOOGLE_CALENDAR_CONNECTOR, "h-6 w-6"), status: "available", }, { id: "google-gmail-connector", title: "Gmail", - description: "Connect to your Gmail account to search through your emails.", + description: "gmail_desc", icon: getConnectorIcon(EnumConnectorName.GOOGLE_GMAIL_CONNECTOR, "h-6 w-6"), status: "available", }, { id: "zoom", title: "Zoom", - description: "Connect to Zoom to access meeting recordings and transcripts.", + description: "zoom_desc", icon: , status: "coming-soon", }, @@ -238,6 +239,7 @@ const cardVariants: Variants = { }; export default function ConnectorsPage() { + const t = useTranslations('add_connector'); const params = useParams(); const searchSpaceId = params.search_space_id as string; const [expandedCategories, setExpandedCategories] = useState([ @@ -266,10 +268,10 @@ export default function ConnectorsPage() { className="mb-12 text-center" >

- Connect Your Tools + {t('title')}

- Integrate with your favorite services to enhance your research capabilities. + {t('subtitle')}

@@ -291,7 +293,7 @@ export default function ConnectorsPage() { className="w-full" >
-

{category.title}

+

{t(category.title)}

@@ -357,7 +359,7 @@ export default function ConnectorsPage() { -

{connector.description}

+

{t(connector.description)}

@@ -367,7 +369,7 @@ export default function ConnectorsPage() { className="w-full" > )} {connector.status === "connected" && ( @@ -393,7 +395,7 @@ export default function ConnectorsPage() { variant="outline" className="w-full border-green-500 text-green-600 hover:bg-green-50 dark:hover:bg-green-950" > - Manage + {t('manage')} )} diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx index cbb3314f0..4a4964ee6 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx @@ -3,6 +3,7 @@ import { CircleAlert, CircleX, Columns3, Filter, ListFilter, Trash } from "lucide-react"; import { AnimatePresence, motion, type Variants } from "motion/react"; import React, { useMemo, useRef } from "react"; +import { useTranslations } from "next-intl"; import { AlertDialog, AlertDialogAction, @@ -55,6 +56,7 @@ export function DocumentsFilters({ columnVisibility: ColumnVisibility; onToggleColumn: (id: keyof ColumnVisibility, checked: boolean) => void; }) { + const t = useTranslations('documents'); const id = React.useId(); const inputRef = useRef(null); @@ -90,9 +92,9 @@ export function DocumentsFilters({ className="peer min-w-60 ps-9" value={searchValue} onChange={(e) => onSearch(e.target.value)} - placeholder="Filter by title..." + placeholder={t('filter_placeholder')} type="text" - aria-label="Filter by title" + aria-label={t('filter_placeholder')} /> void; }) { + const t = useTranslations('documents'); const sorted = React.useMemo( () => sortDocuments(documents, sortKey, sortDesc), [documents, sortKey, sortDesc] @@ -101,15 +103,15 @@ export function DocumentsTableShell({
-

Loading documents...

+

{t('loading')}

) : error ? (
-

Error loading documents

+

{t('error_loading')}

@@ -117,7 +119,7 @@ export function DocumentsTableShell({
-

No documents found

+

{t('no_documents')}

) : ( @@ -140,7 +142,7 @@ export function DocumentsTableShell({ className="flex h-full w-full cursor-pointer select-none items-center justify-between gap-2" onClick={() => onSortHeader("title")} > - Title + {t('title')} {sortKey === "title" ? ( sortDesc ? ( @@ -158,7 +160,7 @@ export function DocumentsTableShell({ className="flex h-full w-full cursor-pointer select-none items-center justify-between gap-2" onClick={() => onSortHeader("document_type")} > - Type + {t('type')} {sortKey === "document_type" ? ( sortDesc ? ( @@ -170,7 +172,7 @@ export function DocumentsTableShell({ )} {columnVisibility.content && ( - Content Summary + {t('content_summary')} )} {columnVisibility.created_at && ( @@ -264,7 +266,7 @@ export function DocumentsTableShell({ content={doc.content} trigger={ } /> @@ -335,7 +337,7 @@ export function DocumentsTableShell({ size="sm" className="w-fit text-xs p-0 h-auto" > - View Full Content + {t('view_full')} } /> diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/PaginationControls.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/PaginationControls.tsx index 2b7434d02..b53b66f46 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/PaginationControls.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/PaginationControls.tsx @@ -2,6 +2,7 @@ import { ChevronFirst, ChevronLast, ChevronLeft, ChevronRight } from "lucide-react"; import { motion } from "motion/react"; +import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Pagination, PaginationContent, PaginationItem } from "@/components/ui/pagination"; @@ -38,6 +39,7 @@ export function PaginationControls({ canNext: boolean; id: string; }) { + const t = useTranslations('documents'); const start = total === 0 ? 0 : pageIndex * pageSize + 1; const end = Math.min((pageIndex + 1) * pageSize, total); @@ -50,7 +52,7 @@ export function PaginationControls({ transition={{ type: "spring", stiffness: 300, damping: 30 }} >
{/* Pagination */} - + ); } // Pagination Component -function LogsPagination({ table, id }: { table: any; id: string }) { +function LogsPagination({ table, id, t }: { table: any; id: string; t: (key: string) => string }) { return (
+ + + + {languages.find(lang => lang.code === locale)?.name || 'English'} + + + + {languages.map((language) => ( + + + {language.flag} + {language.name} + + + ))} + + + ); +} + diff --git a/surfsense_web/components/dashboard-breadcrumb.tsx b/surfsense_web/components/dashboard-breadcrumb.tsx index e77a1fdb3..93bb770f1 100644 --- a/surfsense_web/components/dashboard-breadcrumb.tsx +++ b/surfsense_web/components/dashboard-breadcrumb.tsx @@ -2,6 +2,7 @@ import { usePathname } from "next/navigation"; import React from "react"; +import { useTranslations } from "next-intl"; import { Breadcrumb, BreadcrumbItem, @@ -17,6 +18,7 @@ interface BreadcrumbItemInterface { } export function DashboardBreadcrumb() { + const t = useTranslations('breadcrumb'); const pathname = usePathname(); // Parse the pathname to create breadcrumb items @@ -25,11 +27,11 @@ export function DashboardBreadcrumb() { const breadcrumbs: BreadcrumbItemInterface[] = []; // Always start with Dashboard - breadcrumbs.push({ label: "Dashboard", href: "/dashboard" }); + breadcrumbs.push({ label: t('dashboard'), href: "/dashboard" }); // Handle search space if (segments[0] === "dashboard" && segments[1]) { - breadcrumbs.push({ label: `Search Space ${segments[1]}`, href: `/dashboard/${segments[1]}` }); + breadcrumbs.push({ label: `${t('search_space')} ${segments[1]}`, href: `/dashboard/${segments[1]}` }); // Handle specific sections if (segments[2]) { @@ -38,12 +40,13 @@ export function DashboardBreadcrumb() { // Map section names to more readable labels const sectionLabels: Record = { - researcher: "Researcher", - documents: "Documents", - connectors: "Connectors", - podcasts: "Podcasts", - logs: "Logs", - chats: "Chats", + researcher: t('researcher'), + documents: t('documents'), + connectors: t('connectors'), + podcasts: t('podcasts'), + logs: t('logs'), + chats: t('chats'), + settings: t('settings'), }; sectionLabel = sectionLabels[section] || sectionLabel; @@ -56,14 +59,14 @@ export function DashboardBreadcrumb() { // Handle documents sub-sections if (section === "documents") { const documentLabels: Record = { - upload: "Upload Documents", - youtube: "Add YouTube Videos", - webpage: "Add Webpages", + upload: t('upload_documents'), + youtube: t('add_youtube'), + webpage: t('add_webpages'), }; const documentLabel = documentLabels[subSection] || subSectionLabel; breadcrumbs.push({ - label: "Documents", + label: t('documents'), href: `/dashboard/${segments[1]}/documents`, }); breadcrumbs.push({ label: documentLabel }); @@ -105,13 +108,13 @@ export function DashboardBreadcrumb() { } const connectorLabels: Record = { - add: "Add Connector", - manage: "Manage Connectors", + add: t('add_connector'), + manage: t('manage_connectors'), }; const connectorLabel = connectorLabels[subSection] || subSectionLabel; breadcrumbs.push({ - label: "Connectors", + label: t('connectors'), href: `/dashboard/${segments[1]}/connectors`, }); breadcrumbs.push({ label: connectorLabel }); @@ -120,12 +123,12 @@ export function DashboardBreadcrumb() { // Handle other sub-sections const subSectionLabels: Record = { - upload: "Upload Documents", - youtube: "Add YouTube Videos", - webpage: "Add Webpages", - add: "Add Connector", - edit: "Edit Connector", - manage: "Manage", + upload: t('upload_documents'), + youtube: t('add_youtube'), + webpage: t('add_webpages'), + add: t('add_connector'), + edit: t('edit_connector'), + manage: t('manage'), }; subSectionLabel = subSectionLabels[subSection] || subSectionLabel; diff --git a/surfsense_web/components/onboard/add-provider-step.tsx b/surfsense_web/components/onboard/add-provider-step.tsx index 0086674d2..9d48a56fe 100644 --- a/surfsense_web/components/onboard/add-provider-step.tsx +++ b/surfsense_web/components/onboard/add-provider-step.tsx @@ -4,6 +4,7 @@ import { AlertCircle, Bot, Plus, Trash2 } from "lucide-react"; import { motion } from "motion/react"; import { useState } from "react"; import { toast } from "sonner"; +import { useTranslations } from "next-intl"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -34,6 +35,7 @@ export function AddProviderStep({ onConfigCreated, onConfigDeleted, }: AddProviderStepProps) { + const t = useTranslations('onboard'); const { llmConfigs, createLLMConfig, deleteLLMConfig } = useLLMConfigs(searchSpaceId); const [isAddingNew, setIsAddingNew] = useState(false); const [formData, setFormData] = useState({ @@ -94,15 +96,14 @@ export function AddProviderStep({ - Add at least one LLM provider to continue. You can configure multiple providers and choose - specific roles for each one in the next step. + {t('add_provider_instruction')} {/* Existing Configurations */} {llmConfigs.length > 0 && (
-

Your LLM Configurations

+

{t('your_llm_configs')}

{llmConfigs.map((config) => ( {config.provider}

- Model: {config.model_name} - {config.language && ` • Language: ${config.language}`} - {config.api_base && ` • Base: ${config.api_base}`} + {t('model')}: {config.model_name} + {config.language && ` • ${t('language')}: ${config.language}`} + {config.api_base && ` • ${t('base')}: ${config.api_base}`}

) : ( - Add New LLM Provider + {t('add_new_llm_provider')} - Configure a new language model provider for your AI assistant + {t('configure_new_provider')}
- + handleInputChange("name", e.target.value)} required @@ -186,13 +187,13 @@ export function AddProviderStep({
- + handleInputChange("language", value)} > - + {LANGUAGES.map((language) => ( @@ -227,10 +228,10 @@ export function AddProviderStep({ {formData.provider === "CUSTOM" && (
- + handleInputChange("custom_provider", e.target.value)} required @@ -239,27 +240,27 @@ export function AddProviderStep({ )}
- + handleInputChange("model_name", e.target.value)} required /> {selectedProvider && (

- Examples: {selectedProvider.example} + {t('examples')}: {selectedProvider.example}

)}
- + handleInputChange("api_key", e.target.value)} required @@ -267,10 +268,10 @@ export function AddProviderStep({
- + handleInputChange("api_base", e.target.value)} /> @@ -286,7 +287,7 @@ export function AddProviderStep({
diff --git a/surfsense_web/components/onboard/assign-roles-step.tsx b/surfsense_web/components/onboard/assign-roles-step.tsx index c9c3ae807..bcb18c143 100644 --- a/surfsense_web/components/onboard/assign-roles-step.tsx +++ b/surfsense_web/components/onboard/assign-roles-step.tsx @@ -3,6 +3,7 @@ import { AlertCircle, Bot, Brain, CheckCircle, Zap } from "lucide-react"; import { motion } from "motion/react"; import { useEffect, useState } from "react"; +import { useTranslations } from "next-intl"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; @@ -16,39 +17,40 @@ import { } from "@/components/ui/select"; import { useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs"; -const ROLE_DESCRIPTIONS = { - long_context: { - icon: Brain, - title: "Long Context LLM", - description: "Handles complex tasks requiring extensive context understanding and reasoning", - color: "bg-blue-100 text-blue-800 border-blue-200", - examples: "Document analysis, research synthesis, complex Q&A", - }, - fast: { - icon: Zap, - title: "Fast LLM", - description: "Optimized for quick responses and real-time interactions", - color: "bg-green-100 text-green-800 border-green-200", - examples: "Quick searches, simple questions, instant responses", - }, - strategic: { - icon: Bot, - title: "Strategic LLM", - description: "Advanced reasoning for planning and strategic decision making", - color: "bg-purple-100 text-purple-800 border-purple-200", - examples: "Planning workflows, strategic analysis, complex problem solving", - }, -}; - interface AssignRolesStepProps { searchSpaceId: number; onPreferencesUpdated?: () => Promise; } export function AssignRolesStep({ searchSpaceId, onPreferencesUpdated }: AssignRolesStepProps) { + const t = useTranslations('onboard'); const { llmConfigs } = useLLMConfigs(searchSpaceId); const { preferences, updatePreferences } = useLLMPreferences(searchSpaceId); + const ROLE_DESCRIPTIONS = { + long_context: { + icon: Brain, + title: t('long_context_llm_title'), + description: t('long_context_llm_desc'), + color: "bg-blue-100 text-blue-800 border-blue-200", + examples: t('long_context_llm_examples'), + }, + fast: { + icon: Zap, + title: t('fast_llm_title'), + description: t('fast_llm_desc'), + color: "bg-green-100 text-green-800 border-green-200", + examples: t('fast_llm_examples'), + }, + strategic: { + icon: Bot, + title: t('strategic_llm_title'), + description: t('strategic_llm_desc'), + color: "bg-purple-100 text-purple-800 border-purple-200", + examples: t('strategic_llm_examples'), + }, + }; + const [assignments, setAssignments] = useState({ long_context_llm_id: preferences.long_context_llm_id || "", fast_llm_id: preferences.fast_llm_id || "", @@ -109,9 +111,9 @@ export function AssignRolesStep({ searchSpaceId, onPreferencesUpdated }: AssignR return (
-

No LLM Configurations Found

+

{t('no_llm_configs_found')}

- Please add at least one LLM provider in the previous step before assigning roles. + {t('add_provider_before_roles')}

); @@ -123,8 +125,7 @@ export function AssignRolesStep({ searchSpaceId, onPreferencesUpdated }: AssignR - Assign your LLM configurations to specific roles. Each role serves different purposes in - your workflow. + {t('assign_roles_instruction')} @@ -161,17 +162,17 @@ export function AssignRolesStep({ searchSpaceId, onPreferencesUpdated }: AssignR
- Use cases: {role.examples} + {t('use_cases')}: {role.examples}
- +