fix: comprehensive i18n — replace hardcoded French/English strings with t() calls
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 1m7s
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 1m7s
Replaced ~100+ hardcoded French and English text strings across 30+ components with proper i18n t() calls. Added 57 new translation keys to all 15 locale files (ar, de, en, es, fa, fr, hi, it, ja, ko, nl, pl, pt, ru, zh). Key changes: - contextual-ai-chat.tsx: 30 French strings → t() (actions, toasts, labels, placeholders) - ai-chat.tsx: 15 French/English strings → t() (header, tabs, welcome, insights, history) - note-inline-editor.tsx: 20 French fallbacks removed (toolbar, save status, checklist) - lab-skeleton.tsx: French loading text → t() - admin-header.tsx, header.tsx, editor-connections-section.tsx: French fallbacks removed - New AI chat component, agent cards, sidebar, settings panel i18n cleanup Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -40,12 +40,16 @@ import type { TitleSuggestion } from '@/hooks/use-title-suggestions'
|
||||
import { EditorConnectionsSection } from './editor-connections-section'
|
||||
import { ComparisonModal } from './comparison-modal'
|
||||
import { FusionModal } from './fusion-modal'
|
||||
import { AIAssistantActionBar } from './ai-assistant-action-bar'
|
||||
|
||||
import { ContextualAIChat } from './contextual-ai-chat'
|
||||
import { useLabels } from '@/context/LabelContext'
|
||||
import { useNoteRefresh } from '@/context/NoteRefreshContext'
|
||||
import { useNotebooks } from '@/context/notebooks-context'
|
||||
import { NoteSize } from '@/lib/types'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { getAISettings } from '@/app/actions/ai-settings'
|
||||
|
||||
interface NoteEditorProps {
|
||||
note: Note
|
||||
@@ -54,6 +58,19 @@ interface NoteEditorProps {
|
||||
}
|
||||
|
||||
export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps) {
|
||||
const { data: session } = useSession()
|
||||
const [aiAssistantEnabled, setAiAssistantEnabled] = useState(true)
|
||||
const [autoLabelingEnabled, setAutoLabelingEnabled] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.user?.id) {
|
||||
getAISettings(session.user.id).then(settings => {
|
||||
setAiAssistantEnabled(settings.paragraphRefactor !== false)
|
||||
setAutoLabelingEnabled(settings.autoLabeling !== false)
|
||||
}).catch(err => console.error("Failed to fetch AI settings", err))
|
||||
}
|
||||
}, [session?.user?.id])
|
||||
|
||||
const { labels: globalLabels, addLabel, refreshLabels, setNotebookId: setContextNotebookId } = useLabels()
|
||||
const { triggerRefresh } = useNoteRefresh()
|
||||
const { t } = useLanguage()
|
||||
@@ -81,7 +98,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
const { suggestions, isAnalyzing } = useAutoTagging({
|
||||
content: note.type === 'text' ? content : '',
|
||||
notebookId: note.notebookId,
|
||||
enabled: note.type === 'text'
|
||||
enabled: note.type === 'text' && autoLabelingEnabled
|
||||
})
|
||||
|
||||
// Reminder state
|
||||
@@ -108,6 +125,12 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
|
||||
// AI processing state for ActionBar
|
||||
const [isProcessingAI, setIsProcessingAI] = useState(false)
|
||||
const [aiOpen, setAiOpen] = useState(false)
|
||||
// Track previous content for copilot action undo
|
||||
const [previousContentForCopilot, setPreviousContentForCopilot] = useState<string | null>(null)
|
||||
|
||||
// Notebooks (for copilot chat scope)
|
||||
const { notebooks } = useNotebooks()
|
||||
|
||||
// Memory Echo Connections state
|
||||
const [comparisonNotes, setComparisonNotes] = useState<Array<Partial<Note>>>([])
|
||||
@@ -564,23 +587,41 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
<Dialog open={true} onOpenChange={onClose}>
|
||||
<DialogContent
|
||||
className={cn(
|
||||
'!max-w-[min(95vw,1600px)] max-h-[90vh] overflow-y-auto',
|
||||
'!max-w-[min(95vw,1600px)] max-h-[90vh] overflow-hidden p-0 flex flex-row items-stretch',
|
||||
colorClasses.bg
|
||||
)}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="sr-only">{t('notes.edit')}</DialogTitle>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold">{readOnly ? t('notes.view') : t('notes.edit')}</h2>
|
||||
{readOnly && (
|
||||
<Badge variant="secondary" className="bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary-foreground">
|
||||
{t('notes.readOnly')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 min-w-0 flex flex-col overflow-y-auto space-y-4 px-6 py-6">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="sr-only">{t('notes.edit')}</DialogTitle>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-lg font-semibold">{readOnly ? t('notes.view') : t('notes.edit')}</h2>
|
||||
{/* AI Copilot Toggle Button next to title */}
|
||||
{note.type === 'text' && !readOnly && aiAssistantEnabled && (
|
||||
<Button
|
||||
variant="ghost" size="sm"
|
||||
className={cn(
|
||||
'h-8 gap-1.5 px-2 text-xs transition-colors ml-2',
|
||||
aiOpen && 'bg-primary/10 text-primary'
|
||||
)}
|
||||
onClick={() => setAiOpen(!aiOpen)}
|
||||
title="Toggle AI Copilot"
|
||||
>
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
<span className="hidden sm:inline">Assistant IA</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{readOnly && (
|
||||
<Badge variant="secondary" className="bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary-foreground">
|
||||
{t('notes.readOnly')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
{/* Title */}
|
||||
<div className="relative">
|
||||
<Input
|
||||
@@ -650,47 +691,10 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
{/* Content or Checklist */}
|
||||
{note.type === 'text' ? (
|
||||
<div className="space-y-2">
|
||||
{/* Markdown controls */}
|
||||
<div className="flex items-center justify-between gap-2 pb-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsMarkdown(!isMarkdown)
|
||||
if (isMarkdown) setShowMarkdownPreview(false)
|
||||
}}
|
||||
className={cn("h-7 text-xs", isMarkdown && "text-primary")}
|
||||
>
|
||||
<FileText className="h-3 w-3 mr-1" />
|
||||
{isMarkdown ? t('notes.markdownOn') : t('notes.markdownOff')}
|
||||
</Button>
|
||||
|
||||
{isMarkdown && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowMarkdownPreview(!showMarkdownPreview)}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
{showMarkdownPreview ? (
|
||||
<>
|
||||
<FileText className="h-3 w-3 mr-1" />
|
||||
{t('general.edit')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Eye className="h-3 w-3 mr-1" />
|
||||
{t('notes.preview')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showMarkdownPreview && isMarkdown ? (
|
||||
<MarkdownContent
|
||||
content={content || t('notes.noContent')}
|
||||
className="min-h-[200px] p-3 rounded-md border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50"
|
||||
className="min-h-[200px] p-3 rounded-md border border-border/40 bg-muted/20"
|
||||
/>
|
||||
) : (
|
||||
<Textarea
|
||||
@@ -699,13 +703,11 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
disabled={readOnly}
|
||||
className={cn(
|
||||
"min-h-[200px] border-0 focus-visible:ring-0 px-0 bg-transparent resize-none",
|
||||
"min-h-[200px] border-0 focus-visible:ring-0 px-0 bg-transparent resize-none text-sm leading-relaxed",
|
||||
readOnly && "cursor-default"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* AI Auto-tagging Suggestions */}
|
||||
<GhostTags
|
||||
suggestions={filteredSuggestions}
|
||||
addedTags={labels}
|
||||
@@ -713,19 +715,6 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
onSelectTag={handleSelectGhostTag}
|
||||
onDismissTag={handleDismissGhostTag}
|
||||
/>
|
||||
|
||||
{/* AI Assistant ActionBar */}
|
||||
{!readOnly && (
|
||||
<AIAssistantActionBar
|
||||
onClarify={handleClarifyDirect}
|
||||
onShorten={handleShortenDirect}
|
||||
onImprove={handleImproveDirect}
|
||||
onTransformMarkdown={handleTransformMarkdown}
|
||||
isMarkdownMode={isMarkdown}
|
||||
disabled={isProcessingAI || !content}
|
||||
className="mt-3"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
@@ -742,22 +731,13 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
placeholder={t('notes.listItem')}
|
||||
className="flex-1 border-0 focus-visible:ring-0 px-0 bg-transparent"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="opacity-0 group-hover:opacity-100 h-8 w-8 p-0"
|
||||
onClick={() => handleRemoveCheckItem(item.id)}
|
||||
>
|
||||
<Button variant="ghost" size="sm" className="opacity-0 group-hover:opacity-100 h-8 w-8 p-0"
|
||||
onClick={() => handleRemoveCheckItem(item.id)}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleAddCheckItem}
|
||||
className="text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
<Button variant="ghost" size="sm" onClick={handleAddCheckItem} className="text-gray-600 dark:text-gray-400">
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
{t('notes.addItem')}
|
||||
</Button>
|
||||
@@ -837,111 +817,69 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
)}
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center justify-between pt-4 border-t">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center justify-between pt-3 border-t border-border/30">
|
||||
<div className="flex items-center gap-0.5">
|
||||
{!readOnly && (
|
||||
<>
|
||||
{/* Reminder Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowReminderDialog(true)}
|
||||
title={t('notes.setReminder')}
|
||||
className={currentReminder ? "text-primary" : ""}
|
||||
>
|
||||
{/* Reminder */}
|
||||
<Button variant="ghost" size="icon" className={cn('h-8 w-8', currentReminder && 'text-primary')}
|
||||
onClick={() => setShowReminderDialog(true)} title={t('notes.setReminder')}>
|
||||
<Bell className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{/* Add Image Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
title={t('notes.addImage')}
|
||||
>
|
||||
{/* Add Image */}
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8"
|
||||
onClick={() => fileInputRef.current?.click()} title={t('notes.addImage')}>
|
||||
<ImageIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{/* Add Link Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowLinkDialog(true)}
|
||||
title={t('notes.addLink')}
|
||||
>
|
||||
{/* Add Link */}
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8"
|
||||
onClick={() => setShowLinkDialog(true)} title={t('notes.addLink')}>
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{/* AI Assistant Button */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title={t('ai.assistant')}
|
||||
className="text-purple-600 hover:text-purple-700 dark:text-purple-400"
|
||||
>
|
||||
<Wand2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={handleGenerateTitles} disabled={isGeneratingTitles}>
|
||||
<Sparkles className="h-4 w-4 mr-2" />
|
||||
{isGeneratingTitles ? t('ai.generating') : t('ai.generateTitles')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Wand2 className="h-4 w-4 mr-2" />
|
||||
{t('ai.reformulateText')}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleReformulate('clarify')}
|
||||
disabled={isReformulating}
|
||||
>
|
||||
<Sparkles className="h-4 w-4 mr-2" />
|
||||
{isReformulating ? t('ai.reformulating') : t('ai.clarify')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleReformulate('shorten')}
|
||||
disabled={isReformulating}
|
||||
>
|
||||
<Sparkles className="h-4 w-4 mr-2" />
|
||||
{isReformulating ? t('ai.reformulating') : t('ai.shorten')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleReformulate('improve')}
|
||||
disabled={isReformulating}
|
||||
>
|
||||
<Sparkles className="h-4 w-4 mr-2" />
|
||||
{isReformulating ? t('ai.reformulating') : t('ai.improveStyle')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{/* Markdown toggle */}
|
||||
{note.type === 'text' && (
|
||||
<Button variant="ghost" size="icon"
|
||||
className={cn('h-8 w-8', isMarkdown && 'text-primary bg-primary/10')}
|
||||
onClick={() => { setIsMarkdown(!isMarkdown); if (isMarkdown) setShowMarkdownPreview(false) }}
|
||||
title="Markdown">
|
||||
<FileText className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Markdown preview toggle */}
|
||||
{isMarkdown && (
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8"
|
||||
onClick={() => setShowMarkdownPreview(!showMarkdownPreview)}
|
||||
title={showMarkdownPreview ? t('general.edit') : t('notes.preview')}>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* AI Copilot */}
|
||||
{note.type === 'text' && aiAssistantEnabled && (
|
||||
<Button variant="ghost" size="sm"
|
||||
className={cn('h-8 gap-1.5 px-2 text-xs font-medium transition-colors', aiOpen && 'bg-primary/10 text-primary')}
|
||||
onClick={() => setAiOpen(!aiOpen)} title="Assistant IA">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
<span className="hidden sm:inline">Assistant IA</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Size Selector */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" title={t('notes.changeSize')}>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" title={t('notes.changeSize')}>
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<div className="flex flex-col gap-1 p-1">
|
||||
{['small', 'medium', 'large'].map((s) => (
|
||||
<Button
|
||||
key={s}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSize(s as NoteSize)}
|
||||
className={cn(
|
||||
"justify-start capitalize",
|
||||
size === s && "bg-accent"
|
||||
)}
|
||||
>
|
||||
{(['small', 'medium', 'large'] as const).map((s) => (
|
||||
<Button key={s} variant="ghost" size="sm"
|
||||
onClick={() => setSize(s)}
|
||||
className={cn('justify-start capitalize', size === s && 'bg-accent')}>
|
||||
{s}
|
||||
</Button>
|
||||
))}
|
||||
@@ -952,34 +890,24 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
{/* Color Picker */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" title={t('notes.changeColor')}>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" title={t('notes.changeColor')}>
|
||||
<Palette className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<div className="grid grid-cols-5 gap-2 p-2">
|
||||
{Object.entries(NOTE_COLORS).map(([colorName, classes]) => (
|
||||
<button
|
||||
key={colorName}
|
||||
className={cn(
|
||||
'h-8 w-8 rounded-full border-2 transition-transform hover:scale-110',
|
||||
classes.bg,
|
||||
color === colorName ? 'border-gray-900 dark:border-gray-100' : 'border-gray-300 dark:border-gray-700'
|
||||
)}
|
||||
onClick={() => setColor(colorName)}
|
||||
title={colorName}
|
||||
/>
|
||||
<button key={colorName}
|
||||
className={cn('h-7 w-7 rounded-full border-2 transition-transform hover:scale-110', classes.bg,
|
||||
color === colorName ? 'border-gray-900 dark:border-gray-100' : 'border-gray-300 dark:border-gray-700')}
|
||||
onClick={() => setColor(colorName)} title={colorName} />
|
||||
))}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Label Manager */}
|
||||
<LabelManager
|
||||
existingLabels={labels}
|
||||
notebookId={note.notebookId}
|
||||
onUpdate={setLabels}
|
||||
/>
|
||||
<LabelManager existingLabels={labels} notebookId={note.notebookId} onUpdate={setLabels} />
|
||||
</>
|
||||
)}
|
||||
{readOnly && (
|
||||
@@ -1034,14 +962,34 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleImageUpload}
|
||||
/>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleImageUpload}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ── AI Copilot Side Panel ── */}
|
||||
{aiOpen && (
|
||||
<ContextualAIChat
|
||||
onClose={() => setAiOpen(false)}
|
||||
noteTitle={title}
|
||||
noteContent={content}
|
||||
onApplyToNote={(newContent) => {
|
||||
setPreviousContentForCopilot(content)
|
||||
setContent(newContent)
|
||||
}}
|
||||
onUndoLastAction={previousContentForCopilot !== null ? () => {
|
||||
setContent(previousContentForCopilot)
|
||||
setPreviousContentForCopilot(null)
|
||||
} : undefined}
|
||||
lastActionApplied={previousContentForCopilot !== null}
|
||||
notebooks={notebooks.map(nb => ({ id: nb.id, name: nb.name }))}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
<ReminderDialog
|
||||
|
||||
Reference in New Issue
Block a user