feat(ai): localize AI features

This commit is contained in:
Sepehr Ramezani
2026-02-15 17:38:16 +01:00
parent 8f9031f076
commit 9eb3bd912a
72 changed files with 17098 additions and 7759 deletions

View File

@@ -127,7 +127,7 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) {
<FeatureToggle
name={t('titleSuggestions.available').replace('💡 ', '')}
description="Suggest titles for untitled notes after 50+ words"
description={t('aiSettings.titleSuggestionsDesc')}
checked={settings.titleSuggestions}
onChange={(checked) => handleToggle('titleSuggestions', checked)}
/>
@@ -141,7 +141,7 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) {
<FeatureToggle
name={t('paragraphRefactor.title')}
description="AI-powered text improvement options"
description={t('aiSettings.paragraphRefactorDesc')}
checked={settings.paragraphRefactor}
onChange={(checked) => handleToggle('paragraphRefactor', checked)}
/>
@@ -159,7 +159,7 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) {
{t('aiSettings.frequency')}
</Label>
<p className="text-xs text-gray-500 mb-3">
How often to analyze note connections
{t('aiSettings.frequencyDesc')}
</p>
<RadioGroup
value={settings.memoryEchoFrequency}
@@ -192,7 +192,7 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) {
<Card className="p-4">
<Label className="text-base font-medium mb-1">{t('aiSettings.provider')}</Label>
<p className="text-sm text-gray-500 mb-4">
Choose your preferred AI provider
{t('aiSettings.providerDesc')}
</p>
<RadioGroup
@@ -206,7 +206,7 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) {
{t('aiSettings.providerAuto')}
</Label>
<p className="text-sm text-gray-500">
Ollama when available, OpenAI fallback
{t('aiSettings.providerAutoDesc')}
</p>
</div>
</div>
@@ -218,7 +218,7 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) {
{t('aiSettings.providerOllama')}
</Label>
<p className="text-sm text-gray-500">
100% private, runs locally on your machine
{t('aiSettings.providerOllamaDesc')}
</p>
</div>
</div>
@@ -230,7 +230,7 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) {
{t('aiSettings.providerOpenAI')}
</Label>
<p className="text-sm text-gray-500">
Most accurate, requires API key
{t('aiSettings.providerOpenAIDesc')}
</p>
</div>
</div>

View File

@@ -55,7 +55,10 @@ export function AutoLabelSuggestionDialog({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ notebookId }),
body: JSON.stringify({
notebookId,
language: document.documentElement.lang || 'en',
}),
})
const data = await response.json()
@@ -68,7 +71,7 @@ export function AutoLabelSuggestionDialog({
} else {
// No suggestions is not an error - just close the dialog
if (data.message) {
}
}
onOpenChange(false)
}
} catch (error) {
@@ -113,7 +116,7 @@ export function AutoLabelSuggestionDialog({
if (data.success) {
toast.success(
t('ai.autoLabels.created', { count: data.data.createdCount }) ||
`${data.data.createdCount} labels created successfully`
`${data.data.createdCount} labels created successfully`
)
onLabelsCreated()
onOpenChange(false)

View File

@@ -38,7 +38,11 @@ export function BatchOrganizationDialog({
try {
const response = await fetch('/api/ai/batch-organize', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
language: document.documentElement.lang || 'en'
}),
})
const data = await response.json()
@@ -125,7 +129,7 @@ export function BatchOrganizationDialog({
if (data.success) {
toast.success(
t('ai.batchOrganization.success', { count: data.data.movedCount }) ||
`${data.data.movedCount} notes moved successfully`
`${data.data.movedCount} notes moved successfully`
)
onNotesMoved()
onOpenChange(false)
@@ -306,7 +310,7 @@ export function BatchOrganizationDialog({
) : (
<>
<CheckCircle2 className="h-4 w-4 mr-2" />
{t('ai.batchOrganization.apply')}
{t('ai.batchOrganization.apply', { count: selectedNotes.size })}
</>
)}
</Button>

View File

@@ -21,13 +21,13 @@ export function DemoModeToggle({ demoMode, onToggle }: DemoModeToggleProps) {
try {
await onToggle(checked)
if (checked) {
toast.success('🧪 Demo Mode activated! Memory Echo will now work instantly.')
toast.success(t('demoMode.activated'))
} else {
toast.success('Demo Mode disabled. Normal parameters restored.')
toast.success(t('demoMode.deactivated'))
}
} catch (error) {
console.error('Error toggling demo mode:', error)
toast.error('Failed to toggle demo mode')
toast.error(t('demoMode.toggleFailed'))
} finally {
setIsPending(false)
}
@@ -53,14 +53,11 @@ export function DemoModeToggle({ demoMode, onToggle }: DemoModeToggleProps) {
</div>
<div>
<CardTitle className="text-base flex items-center gap-2">
🧪 Demo Mode
🧪 {t('demoMode.title')}
{demoMode && <Zap className="h-4 w-4 text-amber-500 animate-pulse" />}
</CardTitle>
<CardDescription className="text-xs mt-1">
{demoMode
? 'Test Memory Echo instantly with relaxed parameters'
: 'Enable instant testing of Memory Echo feature'
}
{t('demoMode.description')}
</CardDescription>
</div>
</div>
@@ -77,31 +74,25 @@ export function DemoModeToggle({ demoMode, onToggle }: DemoModeToggleProps) {
<CardContent className="pt-0 space-y-2">
<div className="rounded-lg bg-white dark:bg-zinc-900 border border-amber-200 dark:border-amber-900/30 p-3">
<p className="text-xs font-semibold text-gray-700 dark:text-gray-300 mb-2">
Demo parameters active:
{t('demoMode.parametersActive')}
</p>
<div className="space-y-1.5 text-xs text-gray-600 dark:text-gray-400">
<div className="flex items-start gap-2">
<Target className="h-3.5 w-3.5 mt-0.5 text-amber-600 flex-shrink-0" />
<span>
<strong>50% similarity</strong> threshold (normally 75%)
</span>
<span>{t('demoMode.similarityThreshold')}</span>
</div>
<div className="flex items-start gap-2">
<Zap className="h-3.5 w-3.5 mt-0.5 text-amber-600 flex-shrink-0" />
<span>
<strong>0-day delay</strong> between notes (normally 7 days)
</span>
<span>{t('demoMode.delayBetweenNotes')}</span>
</div>
<div className="flex items-start gap-2">
<Lightbulb className="h-3.5 w-3.5 mt-0.5 text-amber-600 flex-shrink-0" />
<span>
<strong>Unlimited insights</strong> (no frequency limits)
</span>
<span>{t('demoMode.unlimitedInsights')}</span>
</div>
</div>
</div>
<p className="text-xs text-amber-700 dark:text-amber-400 text-center">
💡 Create 2+ similar notes and see Memory Echo in action!
💡 {t('demoMode.createNotesTip')}
</p>
</CardContent>
)}

View File

@@ -53,7 +53,7 @@ export function FavoritesSection({ pinnedNotes, onEdit, isLoading }: FavoritesSe
<div className="flex items-center gap-2">
<span className="text-2xl">📌</span>
<h2 className="text-lg font-semibold text-foreground">
Pinned Notes
{t('notes.pinnedNotes')}
<span className="text-sm font-medium text-muted-foreground ml-2">
({pinnedNotes.length})
</span>

View File

@@ -77,7 +77,7 @@ export function GhostTags({ suggestions, addedTags, isAnalyzing, onSelectTag, on
onSelectTag(suggestion.tag);
}}
className={cn("flex items-center px-3 py-1 text-xs font-medium", colorClasses.text)}
title={isNewLabel ? "Créer ce nouveau label et l'ajouter" : t('ai.clickToAddTag')}
title={isNewLabel ? t('ai.autoLabels.createNewLabel') : t('ai.clickToAddTag')}
>
{isNewLabel && <Plus className="w-3 h-3 mr-1" />}
{!isNewLabel && <Sparkles className="w-3 h-3 mr-1.5 opacity-50" />}

View File

@@ -355,7 +355,7 @@ export function Header({
</DropdownMenuItem>
<DropdownMenuItem onClick={() => router.push('/admin')} className="cursor-pointer">
<Shield className="mr-2 h-4 w-4" />
<span>Admin</span>
<span>{t('nav.adminDashboard')}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => signOut()} className="cursor-pointer text-red-600 focus:text-red-600">
<LogOut className="mr-2 h-4 w-4" />

View File

@@ -1,6 +1,7 @@
'use client'
import { useState, useEffect } from 'react'
import { useLanguage } from '@/lib/i18n/LanguageProvider'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
@@ -33,6 +34,7 @@ interface MemoryEchoNotificationProps {
}
export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationProps) {
const { t } = useLanguage()
const [insight, setInsight] = useState<MemoryEchoInsight | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [isDismissed, setIsDismissed] = useState(false)
@@ -137,9 +139,9 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
<Sparkles className="h-5 w-5 text-amber-600 dark:text-amber-400" />
</div>
<div>
<h2 className="text-xl font-semibold">💡 Memory Echo Discovery</h2>
<h2 className="text-xl font-semibold">{t('memoryEcho.title')}</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">
These notes are connected by {similarityPercentage}% similarity
{t('connection.similarityInfo', { similarity: similarityPercentage })}
</p>
</div>
</div>
@@ -179,7 +181,7 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-4">
{insight.note1.content}
</p>
<p className="text-xs text-gray-500 mt-2">Click to view note </p>
<p className="text-xs text-gray-500 mt-2">{t('memoryEcho.clickToView')}</p>
</div>
{/* Note 2 */}
@@ -198,37 +200,35 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-4">
{insight.note2.content}
</p>
<p className="text-xs text-gray-500 mt-2">Click to view note </p>
<p className="text-xs text-gray-500 mt-2">{t('memoryEcho.clickToView')}</p>
</div>
</div>
{/* Feedback Section */}
<div className="flex items-center justify-between px-6 py-4 border-t dark:border-zinc-700 bg-gray-50 dark:bg-zinc-800/50">
<p className="text-sm text-gray-600 dark:text-gray-400">
Is this connection helpful?
{t('connection.isHelpful')}
</p>
<div className="flex items-center gap-2">
<button
onClick={() => handleFeedback('thumbs_up')}
className={`px-4 py-2 rounded-lg flex items-center gap-2 transition-colors ${
insight.feedback === 'thumbs_up'
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
: 'hover:bg-green-50 text-green-600 dark:hover:bg-green-950/20'
}`}
className={`px-4 py-2 rounded-lg flex items-center gap-2 transition-colors ${insight.feedback === 'thumbs_up'
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
: 'hover:bg-green-50 text-green-600 dark:hover:bg-green-950/20'
}`}
>
<ThumbsUp className="h-4 w-4" />
Helpful
{t('memoryEcho.helpful')}
</button>
<button
onClick={() => handleFeedback('thumbs_down')}
className={`px-4 py-2 rounded-lg flex items-center gap-2 transition-colors ${
insight.feedback === 'thumbs_down'
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
: 'hover:bg-red-50 text-red-600 dark:hover:bg-red-950/20'
}`}
className={`px-4 py-2 rounded-lg flex items-center gap-2 transition-colors ${insight.feedback === 'thumbs_down'
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
: 'hover:bg-red-50 text-red-600 dark:hover:bg-red-950/20'
}`}
>
<ThumbsDown className="h-4 w-4" />
Not Helpful
{t('memoryEcho.notHelpful')}
</button>
</div>
</div>
@@ -248,11 +248,11 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
</div>
<div>
<CardTitle className="text-base flex items-center gap-2">
💡 I noticed something...
{t('memoryEcho.title')}
<Sparkles className="h-4 w-4 text-amber-500" />
</CardTitle>
<CardDescription className="text-xs mt-1">
Proactive connections between your notes
{t('memoryEcho.description')}
</CardDescription>
</div>
</div>
@@ -298,7 +298,7 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
className="flex-1 bg-amber-600 hover:bg-amber-700 text-white"
onClick={handleView}
>
View Connection
{t('memoryEcho.viewConnection')}
</Button>
<div className="flex items-center gap-1 border-l pl-2">
@@ -307,7 +307,7 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
size="icon"
className="h-8 w-8 text-green-600 hover:text-green-700 hover:bg-green-50 dark:hover:bg-green-950/20"
onClick={() => handleFeedback('thumbs_up')}
title="Helpful"
title={t('memoryEcho.helpful')}
>
<ThumbsUp className="h-4 w-4" />
</Button>
@@ -316,7 +316,7 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
size="icon"
className="h-8 w-8 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/20"
onClick={() => handleFeedback('thumbs_down')}
title="Not Helpful"
title={t('memoryEcho.notHelpful')}
>
<ThumbsDown className="h-4 w-4" />
</Button>
@@ -328,7 +328,7 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
className="w-full text-center text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 py-1"
onClick={handleDismiss}
>
Dismiss for now
{t('memoryEcho.dismiss')}
</button>
</CardContent>
</Card>

View File

@@ -1122,7 +1122,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
}
}
toast.success('Notes fusionnées avec succès !')
toast.success(t('toast.notesFusionSuccess'))
triggerRefresh()
onClose()
}}

View File

@@ -57,7 +57,10 @@ export function NotebookSuggestionToast({
const response = await fetch('/api/ai/suggest-notebook', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ noteContent })
body: JSON.stringify({
noteContent,
language: document.documentElement.lang || 'en',
})
})
const data = await response.json()

View File

@@ -52,7 +52,10 @@ export function NotebookSummaryDialog({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ notebookId }),
body: JSON.stringify({
notebookId,
language: document.documentElement.lang || 'en',
}),
})
const data = await response.json()
@@ -82,6 +85,10 @@ export function NotebookSummaryDialog({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader className="sr-only">
<DialogTitle>{t('notebook.generating')}</DialogTitle>
<DialogDescription>{t('notebook.generatingDescription') || 'Please wait...'}</DialogDescription>
</DialogHeader>
<div className="flex flex-col items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p className="mt-4 text-sm text-muted-foreground">

View File

@@ -1,7 +1,7 @@
'use client'
import { Note } from '@/lib/types'
import { Clock, FileText, Tag } from 'lucide-react'
import { Clock } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useLanguage } from '@/lib/i18n'
@@ -11,27 +11,24 @@ interface RecentNotesSectionProps {
}
export function RecentNotesSection({ recentNotes, onEdit }: RecentNotesSectionProps) {
const { language } = useLanguage()
const { t } = useLanguage()
// Show only the 3 most recent notes
const topThree = recentNotes.slice(0, 3)
if (topThree.length === 0) return null
return (
<section data-testid="recent-notes-section" className="mb-6">
{/* Minimalist header - matching your app style */}
<div className="flex items-center gap-2 mb-3 px-1">
<Clock className="w-3.5 h-3.5 text-muted-foreground" />
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
{language === 'fr' ? 'Récent' : 'Recent'}
{t('notes.recent')}
</span>
<span className="text-xs text-muted-foreground">
· {topThree.length}
</span>
</div>
{/* Compact 3-card row */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{topThree.map((note, index) => (
<CompactCard
@@ -46,7 +43,6 @@ export function RecentNotesSection({ recentNotes, onEdit }: RecentNotesSectionPr
)
}
// Compact card - matching your app's clean design
function CompactCard({
note,
index,
@@ -56,9 +52,8 @@ function CompactCard({
index: number
onEdit?: (note: Note, readOnly?: boolean) => void
}) {
const { language } = useLanguage()
// Use contentUpdatedAt - only reflects actual content changes, not property changes (size, color, etc.)
const timeAgo = getCompactTime(note.contentUpdatedAt || note.updatedAt, language)
const { t } = useLanguage()
const timeAgo = getCompactTime(note.contentUpdatedAt || note.updatedAt, t)
const isFirstNote = index === 0
return (
@@ -69,7 +64,6 @@ function CompactCard({
isFirstNote && "ring-2 ring-primary/20"
)}
>
{/* Subtle left accent - colored based on recency */}
<div className={cn(
"absolute left-0 top-0 bottom-0 w-1 rounded-l-xl",
isFirstNote
@@ -79,74 +73,54 @@ function CompactCard({
: "bg-muted dark:bg-muted/60"
)} />
{/* Content with left padding for accent line */}
<div className="pl-2">
{/* Title */}
<h3 className="text-sm font-semibold text-foreground line-clamp-1 mb-2">
{note.title || (language === 'fr' ? 'Sans titre' : 'Untitled')}
{note.title || t('notes.untitled')}
</h3>
{/* Preview - 2 lines max */}
<p className="text-xs text-muted-foreground line-clamp-2 mb-3 min-h-[2.5rem]">
{note.content?.substring(0, 80) || ''}
{note.content && note.content.length > 80 && '...'}
</p>
{/* Footer with time and indicators */}
<div className="flex items-center justify-between pt-2 border-t border-border">
{/* Time - left */}
<span className="text-xs text-muted-foreground flex items-center gap-1">
<Clock className="w-3 h-3" />
<span className="font-medium">{timeAgo}</span>
</span>
{/* Indicators - right */}
<div className="flex items-center gap-1.5">
{/* Notebook indicator */}
{note.notebookId && (
<div className="w-1.5 h-1.5 rounded-full bg-primary dark:bg-primary/70" title="In notebook" />
<div className="w-1.5 h-1.5 rounded-full bg-primary dark:bg-primary/70" title={t('notes.inNotebook')} />
)}
{/* Labels indicator */}
{note.labels && note.labels.length > 0 && (
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500 dark:bg-emerald-400" title={`${note.labels.length} ${language === 'fr' ? 'étiquettes' : 'labels'}`} />
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500 dark:bg-emerald-400" title={t('labels.count', { count: note.labels.length })} />
)}
</div>
</div>
</div>
{/* Hover indicator - top right */}
<div className="absolute top-3 right-3 w-2 h-2 rounded-full bg-primary opacity-0 group-hover:opacity-100 transition-opacity duration-200" />
</button>
)
}
// Compact time display - matching your app's style
// NOTE: Ensure dates are properly parsed from database (may come as strings)
function getCompactTime(date: Date | string, language: string): string {
function getCompactTime(date: Date | string, t: (key: string, params?: Record<string, any>) => string): string {
const now = new Date()
const then = date instanceof Date ? date : new Date(date)
// Validate date
if (isNaN(then.getTime())) {
console.warn('Invalid date provided to getCompactTime:', date)
return language === 'fr' ? 'date invalide' : 'invalid date'
return t('common.error')
}
const seconds = Math.floor((now.getTime() - then.getTime()) / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
if (language === 'fr') {
if (seconds < 60) return 'à l\'instant'
if (minutes < 60) return `il y a ${minutes}m`
if (hours < 24) return `il y a ${hours}h`
const days = Math.floor(hours / 24)
return `il y a ${days}j`
} else {
if (seconds < 60) return 'just now'
if (minutes < 60) return `${minutes}m ago`
if (hours < 24) return `${hours}h ago`
const days = Math.floor(hours / 24)
return `${days}d ago`
}
if (seconds < 60) return t('time.justNow')
if (minutes < 60) return t('time.minutesAgo', { count: minutes })
if (hours < 24) return t('time.hoursAgo', { count: hours })
const days = Math.floor(hours / 24)
return t('time.daysAgo', { count: days })
}

View File

@@ -1,7 +1,10 @@
'use client'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { useState, useEffect } from "react"
import { useLanguage } from '@/lib/i18n'
interface ReminderDialogProps {
open: boolean
@@ -18,6 +21,7 @@ export function ReminderDialog({
onSave,
onRemove
}: ReminderDialogProps) {
const { t } = useLanguage()
const [reminderDate, setReminderDate] = useState('')
const [reminderTime, setReminderTime] = useState('')
@@ -51,7 +55,6 @@ export function ReminderDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
onInteractOutside={(event) => {
// Prevent dialog from closing when interacting with Sonner toasts
const target = event.target as HTMLElement;
const isSonnerElement =
@@ -75,12 +78,12 @@ export function ReminderDialog({
}}
>
<DialogHeader>
<DialogTitle>Set Reminder</DialogTitle>
<DialogTitle>{t('reminder.setReminder')}</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label htmlFor="reminder-date" className="text-sm font-medium">
Date
{t('reminder.reminderDate')}
</label>
<Input
id="reminder-date"
@@ -92,7 +95,7 @@ export function ReminderDialog({
</div>
<div className="space-y-2">
<label htmlFor="reminder-time" className="text-sm font-medium">
Time
{t('reminder.reminderTime')}
</label>
<Input
id="reminder-time"
@@ -107,16 +110,16 @@ export function ReminderDialog({
<div>
{currentReminder && (
<Button variant="outline" onClick={() => { onRemove(); onOpenChange(false); }}>
Remove Reminder
{t('reminder.removeReminder')}
</Button>
)}
</div>
<div className="flex gap-2">
<Button variant="ghost" onClick={() => onOpenChange(false)}>
Cancel
{t('reminder.cancel')}
</Button>
<Button onClick={handleSave}>
Set Reminder
{t('reminder.save')}
</Button>
</div>
</div>

View File

@@ -5,6 +5,7 @@ import { Label } from '@/components/ui/label'
import { Loader2, Check } from 'lucide-react'
import { cn } from '@/lib/utils'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
interface SettingInputProps {
label: string
@@ -25,6 +26,7 @@ export function SettingInput({
placeholder,
disabled
}: SettingInputProps) {
const { t } = useLanguage()
const [isLoading, setIsLoading] = useState(false)
const [isSaved, setIsSaved] = useState(false)
@@ -35,15 +37,12 @@ export function SettingInput({
try {
await onChange(newValue)
setIsSaved(true)
toast.success('Setting saved')
toast.success(t('toast.saved'))
// Clear saved indicator after 2 seconds
setTimeout(() => setIsSaved(false), 2000)
} catch (err) {
console.error('Error updating setting:', err)
toast.error('Failed to save setting', {
description: 'Please try again'
})
toast.error(t('toast.saveFailed'))
} finally {
setIsLoading(false)
}

View File

@@ -5,6 +5,7 @@ import { Label } from '@/components/ui/label'
import { Loader2 } from 'lucide-react'
import { cn } from '@/lib/utils'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
interface SelectOption {
value: string
@@ -29,6 +30,7 @@ export function SettingSelect({
onChange,
disabled
}: SettingSelectProps) {
const { t } = useLanguage()
const [isLoading, setIsLoading] = useState(false)
const handleChange = async (newValue: string) => {
@@ -36,14 +38,10 @@ export function SettingSelect({
try {
await onChange(newValue)
toast.success('Setting saved', {
description: `${label} has been updated`
})
toast.success(t('toast.saved'))
} catch (err) {
console.error('Error updating setting:', err)
toast.error('Failed to save setting', {
description: 'Please try again'
})
toast.error(t('toast.saveFailed'))
} finally {
setIsLoading(false)
}

View File

@@ -6,6 +6,7 @@ import { Label } from '@/components/ui/label'
import { Loader2, Check, X } from 'lucide-react'
import { cn } from '@/lib/utils'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
interface SettingToggleProps {
label: string
@@ -22,6 +23,7 @@ export function SettingToggle({
onChange,
disabled
}: SettingToggleProps) {
const { t } = useLanguage()
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(false)
@@ -31,15 +33,11 @@ export function SettingToggle({
try {
await onChange(newChecked)
toast.success('Setting saved', {
description: `${label} has been ${newChecked ? 'enabled' : 'disabled'}`
})
toast.success(t('toast.saved'))
} catch (err) {
console.error('Error updating setting:', err)
setError(true)
toast.error('Failed to save setting', {
description: 'Please try again'
})
toast.error(t('toast.saveFailed'))
} finally {
setIsLoading(false)
}

View File

@@ -4,6 +4,7 @@ import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { Settings, Sparkles, Palette, User, Database, Info, Check } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useLanguage } from '@/lib/i18n'
interface SettingsSection {
id: string
@@ -18,41 +19,42 @@ interface SettingsNavProps {
export function SettingsNav({ className }: SettingsNavProps) {
const pathname = usePathname()
const { t } = useLanguage()
const sections: SettingsSection[] = [
{
id: 'general',
label: 'General',
label: t('generalSettings.title'),
icon: <Settings className="h-5 w-5" />,
href: '/settings/general'
},
{
id: 'ai',
label: 'AI',
label: t('aiSettings.title'),
icon: <Sparkles className="h-5 w-5" />,
href: '/settings/ai'
},
{
id: 'appearance',
label: 'Appearance',
label: t('appearance.title'),
icon: <Palette className="h-5 w-5" />,
href: '/settings/appearance'
},
{
id: 'profile',
label: 'Profile',
label: t('profile.title'),
icon: <User className="h-5 w-5" />,
href: '/settings/profile'
},
{
id: 'data',
label: 'Data',
label: t('dataManagement.title'),
icon: <Database className="h-5 w-5" />,
href: '/settings/data'
},
{
id: 'about',
label: 'About',
label: t('about.title'),
icon: <Info className="h-5 w-5" />,
href: '/settings/about'
}

View File

@@ -109,11 +109,11 @@ export function Sidebar({ className, user }: { className?: string, user?: any })
{/* Footer / Copyright / Terms */}
<div className="mt-auto px-6 py-4 text-[10px] text-gray-400">
<div className="flex gap-2 mb-1">
<Link href="#" className="hover:underline">Confidentialité</Link>
<Link href="#" className="hover:underline">{t('footer.privacy')}</Link>
<span></span>
<Link href="#" className="hover:underline">Conditions</Link>
<Link href="#" className="hover:underline">{t('footer.terms')}</Link>
</div>
<p>Open Source Clone</p>
<p>{t('footer.openSource')}</p>
</div>
</aside>
)

View File

@@ -14,10 +14,12 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { useSession, signOut } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import { LogOut, Settings, User, Shield } from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
export function UserNav({ user }: { user?: any }) {
const { data: session } = useSession()
const router = useRouter()
const { t } = useLanguage()
const currentUser = user || session?.user
@@ -51,23 +53,23 @@ export function UserNav({ user }: { user?: any }) {
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => router.push('/settings/profile')}>
<User className="mr-2 h-4 w-4" />
<span>Profile</span>
<span>{t('nav.profile')}</span>
</DropdownMenuItem>
{userRole === 'ADMIN' && (
<DropdownMenuItem onClick={() => router.push('/admin')}>
<Shield className="mr-2 h-4 w-4" />
<span>Admin Dashboard</span>
<span>{t('nav.adminDashboard')}</span>
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={() => router.push('/settings')}>
<Settings className="mr-2 h-4 w-4" />
<span>Diagnostics</span>
<span>{t('nav.diagnostics')}</span>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => signOut({ callbackUrl: '/login' })}>
<LogOut className="mr-2 h-4 w-4" />
<span>Log out</span>
<span>{t('nav.logout')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>