fix: comprehensive i18n — replace hardcoded French/English strings with t() calls
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:
2026-04-26 21:14:45 +02:00
parent e358171c45
commit 153c921960
60 changed files with 4125 additions and 1677 deletions

View File

@@ -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