feat: Complete internationalization and code cleanup

## Translation Files
- Add 11 new language files (es, de, pt, ru, zh, ja, ko, ar, hi, nl, pl)
- Add 100+ missing translation keys across all 15 languages
- New sections: notebook, pagination, ai.batchOrganization, ai.autoLabels
- Update nav section with workspace, quickAccess, myLibrary keys

## Component Updates
- Update 15+ components to use translation keys instead of hardcoded text
- Components: notebook dialogs, sidebar, header, note-input, ghost-tags, etc.
- Replace 80+ hardcoded English/French strings with t() calls
- Ensure consistent UI across all supported languages

## Code Quality
- Remove 77+ console.log statements from codebase
- Clean up API routes, components, hooks, and services
- Keep only essential error handling (no debugging logs)

## UI/UX Improvements
- Update Keep logo to yellow post-it style (from-yellow-400 to-amber-500)
- Change selection colors to #FEF3C6 (notebooks) and #EFB162 (nav items)
- Make "+" button permanently visible in notebooks section
- Fix grammar and syntax errors in multiple components

## Bug Fixes
- Fix JSON syntax errors in it.json, nl.json, pl.json, zh.json
- Fix syntax errors in notebook-suggestion-toast.tsx
- Fix syntax errors in use-auto-tagging.ts
- Fix syntax errors in paragraph-refactor.service.ts
- Fix duplicate "fusion" section in nl.json

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Ou une version plus courte si vous préférez :

feat(i18n): Add 15 languages, remove logs, update UI components

- Create 11 new translation files (es, de, pt, ru, zh, ja, ko, ar, hi, nl, pl)
- Add 100+ translation keys: notebook, pagination, AI features
- Update 15+ components to use translations (80+ strings)
- Remove 77+ console.log statements from codebase
- Fix JSON syntax errors in 4 translation files
- Fix component syntax errors (toast, hooks, services)
- Update logo to yellow post-it style
- Change selection colors (#FEF3C6, #EFB162)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-11 22:26:13 +01:00
parent fc2c40249e
commit 7fb486c9a4
183 changed files with 48288 additions and 1290 deletions

View File

@@ -0,0 +1,62 @@
import { useState, useEffect } from 'react'
import { useSearchParams } from 'next/navigation'
import { useSession } from 'next-auth/react'
/**
* Hook to check if auto label suggestions should be shown for the current notebook
* Triggers when notebook has 15+ notes (IA4)
*/
export function useAutoLabelSuggestion() {
const { data: session } = useSession()
const searchParams = useSearchParams()
const [shouldSuggest, setShouldSuggest] = useState(false)
const [notebookId, setNotebookId] = useState<string | null>(searchParams.get('notebook'))
const [hasChecked, setHasChecked] = useState(false)
useEffect(() => {
const currentNotebookId = searchParams.get('notebook')
// Reset when notebook changes
if (currentNotebookId !== notebookId) {
setNotebookId(currentNotebookId)
setHasChecked(false)
setShouldSuggest(false)
// Check if we should suggest labels for this notebook
if (currentNotebookId && session?.user?.id) {
checkNotebookForSuggestions(currentNotebookId)
}
}
}, [searchParams, notebookId, session])
const checkNotebookForSuggestions = async (nbId: string) => {
try {
// Check if notebook has 15+ notes
const response = await fetch('/api/ai/auto-labels', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ notebookId: nbId }),
})
const data = await response.json()
// Show suggestions if available
if (data.success && data.data) {
setShouldSuggest(true)
}
setHasChecked(true)
} catch (error) {
console.error('Failed to check for label suggestions:', error)
setHasChecked(true)
}
}
return {
shouldSuggest,
notebookId,
hasChecked,
dismiss: () => setShouldSuggest(false),
}
}

View File

@@ -1,13 +1,14 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { useDebounce } from './use-debounce';
import { TagSuggestion } from '@/lib/ai/types';
interface UseAutoTaggingProps {
content: string;
notebookId?: string | null;
enabled?: boolean;
}
export function useAutoTagging({ content, enabled = true }: UseAutoTaggingProps) {
export function useAutoTagging({ content, notebookId, enabled = true }: UseAutoTaggingProps) {
const [suggestions, setSuggestions] = useState<TagSuggestion[]>([]);
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -15,40 +16,74 @@ export function useAutoTagging({ content, enabled = true }: UseAutoTaggingProps)
// Debounce le contenu de 1.5s
const debouncedContent = useDebounce(content, 1500);
useEffect(() => {
if (!enabled || !debouncedContent || debouncedContent.length < 10) {
// Track previous notebookId to detect when note is moved to a notebook
const previousNotebookId = useRef<string | null | undefined>(notebookId);
const analyzeContent = async (contentToAnalyze: string) => {
// CRITICAL: Don't suggest labels in "Notes générales" (notebookId is null)
// Labels should ONLY appear within notebooks, not in the general notes section
if (!notebookId) {
setSuggestions([]);
return;
}
const analyzeContent = async () => {
setIsAnalyzing(true);
setError(null);
if (!contentToAnalyze || contentToAnalyze.length < 10) {
setSuggestions([]);
return;
}
try {
const response = await fetch('/api/ai/tags', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: debouncedContent }),
});
setIsAnalyzing(true);
setError(null);
if (!response.ok) {
throw new Error('Erreur lors de l\'analyse');
}
try {
const response = await fetch('/api/ai/tags', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: contentToAnalyze,
notebookId: notebookId || undefined, // Pass notebookId for contextual suggestions (IA2)
}),
});
const data = await response.json();
setSuggestions(data.tags || []);
} catch (err) {
console.error('❌ Auto-tagging error:', err);
setError('Impossible de générer des suggestions');
} finally {
setIsAnalyzing(false);
if (!response.ok) {
throw new Error('Erreur lors de l\'analyse');
}
};
analyzeContent();
const data = await response.json();
setSuggestions(data.tags || []);
} catch (err) {
setError('Impossible de générer des suggestions');
} finally {
setIsAnalyzing(false);
}
};
// Trigger on content change
useEffect(() => {
if (!enabled) {
setSuggestions([]);
return;
}
analyzeContent(debouncedContent);
}, [debouncedContent, enabled]);
// CRITICAL: Also trigger when notebookId changes from null/undefined to a value (note moved to notebook)
useEffect(() => {
if (!enabled) return;
const prev = previousNotebookId.current;
previousNotebookId.current = notebookId;
// Detect when note is moved FROM "Notes générales" (null) TO a notebook
const wasMovedToNotebook = (prev === null || prev === undefined) && notebookId;
if (wasMovedToNotebook && content && content.length >= 10) {
// Use current content immediately (no debounce) when moving to notebook
analyzeContent(content);
}
}, [notebookId, content, enabled]);
return {
suggestions,
isAnalyzing,

View File

@@ -0,0 +1,44 @@
'use client'
import { useState, useEffect } from 'react'
import { getNoteById } from '@/app/actions/notes'
import { Note } from '@/lib/types'
export function useConnectionsCompare(noteIds: string[] | null) {
const [notes, setNotes] = useState<Note[]>([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
// Early return if no noteIds or empty array
if (!noteIds || noteIds.length === 0) {
setNotes([])
return
}
const fetchNotes = async () => {
setIsLoading(true)
setError(null)
try {
const fetchedNotes = await Promise.all(
noteIds.map(id => getNoteById(id))
)
// Filter out null/undefined notes
const validNotes = fetchedNotes.filter((note): note is Note => note !== null && note !== undefined)
setNotes(validNotes)
} catch (err) {
console.error('[useConnectionsCompare] Failed to fetch notes:', err)
setError('Failed to load notes')
} finally {
setIsLoading(false)
}
}
fetchNotes()
}, [noteIds])
return { notes, isLoading, error }
}

View File

@@ -0,0 +1,36 @@
'use client'
import { useState, useCallback } from 'react'
export type DragState = 'idle' | 'dragging' | 'drag-over'
export function useNoteDrag() {
const [draggedNoteId, setDraggedNoteId] = useState<string | null>(null)
const [dragOverNotebookId, setDragOverNotebookId] = useState<string | null>(null)
const startDrag = useCallback((noteId: string) => {
setDraggedNoteId(noteId)
}, [])
const endDrag = useCallback(() => {
setDraggedNoteId(null)
setDragOverNotebookId(null)
}, [])
const dragOver = useCallback((notebookId: string | null) => {
setDragOverNotebookId(notebookId)
}, [])
const isDragging = draggedNoteId !== null
const isDragOver = dragOverNotebookId !== null
return {
draggedNoteId,
dragOverNotebookId,
startDrag,
endDrag,
dragOver,
isDragging,
isDragOver,
}
}

View File

@@ -10,27 +10,27 @@ export function useReminderCheck(notes: Note[]) {
useEffect(() => {
const checkReminders = () => {
const now = new Date();
const dueReminders: string[] = [];
// First pass: collect which reminders are due
notes.forEach(note => {
if (!note.reminder) return;
const reminderDate = new Date(note.reminder);
// Check if reminder is due (within last minute or future)
// We only notify if it's due now or just passed, not old overdue ones (unless we want to catch up)
// Let's say: notify if reminder time is passed AND we haven't notified yet.
if (reminderDate <= now && !notifiedReminders.has(note.id)) {
// Play sound (optional)
// const audio = new Audio('/notification.mp3');
// audio.play().catch(e => console.log('Audio play failed', e));
dueReminders.push(note.id);
toast.info("🔔 Reminder: " + (note.title || "Untitled Note"));
// Mark as notified in local state
setNotifiedReminders(prev => new Set(prev).add(note.id));
}
});
// Second pass: update state only once with all due reminders
if (dueReminders.length > 0) {
setNotifiedReminders(prev => {
const newSet = new Set(prev);
dueReminders.forEach(id => newSet.add(id));
return newSet;
});
}
};
// Check immediately
@@ -40,5 +40,5 @@ export function useReminderCheck(notes: Note[]) {
const interval = setInterval(checkReminders, 30000);
return () => clearInterval(interval);
}, [notes, notifiedReminders]);
}, [notes]);
}

View File

@@ -0,0 +1,74 @@
import { useState, useEffect } from 'react'
import { useDebounce } from './use-debounce'
export interface TitleSuggestion {
title: string
confidence: number
reasoning?: string
}
interface UseTitleSuggestionsProps {
content: string
enabled?: boolean
}
export function useTitleSuggestions({ content, enabled = true }: UseTitleSuggestionsProps) {
const [suggestions, setSuggestions] = useState<TitleSuggestion[]>([])
const [isAnalyzing, setIsAnalyzing] = useState(false)
const [error, setError] = useState<string | null>(null)
// Debounce le contenu de 2s pour éviter trop d'appels
const debouncedContent = useDebounce(content, 2000)
useEffect(() => {
if (!enabled || !debouncedContent) {
setSuggestions([])
return
}
const wordCount = debouncedContent.split(/\s+/).length
// Il faut au moins 10 mots
if (wordCount < 10) {
setSuggestions([])
return
}
const generateTitles = async () => {
setIsAnalyzing(true)
setError(null)
try {
const response = await fetch('/api/ai/title-suggestions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: debouncedContent }),
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || 'Erreur lors de la génération des titres')
}
const data = await response.json()
setSuggestions(data.suggestions || [])
} catch (err) {
console.error('❌ Title suggestions error:', err)
setError('Impossible de générer des suggestions de titres')
} finally {
setIsAnalyzing(false)
}
}
generateTitles()
}, [debouncedContent, enabled])
return {
suggestions,
isAnalyzing,
error,
clearSuggestions: () => setSuggestions([])
}
}