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

@@ -1,20 +1,26 @@
'use client'
import { useState, useEffect, useRef, useCallback, memo } from 'react';
import { useState, useEffect, useRef, useCallback, memo, useMemo } from 'react';
import { Note } from '@/lib/types';
import { NoteCard } from './note-card';
import { NoteEditor } from './note-editor';
import { updateFullOrder } from '@/app/actions/notes';
import { updateFullOrderWithoutRevalidation } from '@/app/actions/notes';
import { useResizeObserver } from '@/hooks/use-resize-observer';
import { useNotebookDrag } from '@/context/notebook-drag-context';
import { useLanguage } from '@/lib/i18n';
interface MasonryGridProps {
notes: Note[];
onEdit?: (note: Note, readOnly?: boolean) => void;
}
interface MasonryItemProps {
note: Note;
onEdit: (note: Note, readOnly?: boolean) => void;
onResize: () => void;
onDragStart?: (noteId: string) => void;
onDragEnd?: () => void;
isDragging?: boolean;
}
function getSizeClasses(size: string = 'small') {
@@ -29,62 +35,97 @@ function getSizeClasses(size: string = 'small') {
}
}
const MasonryItem = memo(function MasonryItem({ note, onEdit, onResize }: MasonryItemProps) {
const resizeRef = useResizeObserver(() => {
onResize();
});
const MasonryItem = memo(function MasonryItem({ note, onEdit, onResize, onDragStart, onDragEnd, isDragging }: MasonryItemProps) {
const resizeRef = useResizeObserver(onResize);
const sizeClasses = getSizeClasses(note.size);
return (
<div
<div
className={`masonry-item absolute p-2 ${sizeClasses}`}
data-id={note.id}
ref={resizeRef as any}
>
<div className="masonry-item-content relative">
<NoteCard note={note} onEdit={onEdit} />
<NoteCard
note={note}
onEdit={onEdit}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
isDragging={isDragging}
/>
</div>
</div>
);
}, (prev, next) => {
// Custom comparison to avoid re-render on function prop changes if note data is same
return prev.note === next.note;
return prev.note.id === next.note.id && prev.note.order === next.note.order && prev.isDragging === next.isDragging;
});
export function MasonryGrid({ notes }: MasonryGridProps) {
export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
const { t } = useLanguage();
const [editingNote, setEditingNote] = useState<{ note: Note; readOnly?: boolean } | null>(null);
const { startDrag, endDrag, draggedNoteId } = useNotebookDrag();
// Use external onEdit if provided, otherwise use internal state
const handleEdit = useCallback((note: Note, readOnly?: boolean) => {
if (onEdit) {
onEdit(note, readOnly);
} else {
setEditingNote({ note, readOnly });
}
}, [onEdit]);
const pinnedGridRef = useRef<HTMLDivElement>(null);
const othersGridRef = useRef<HTMLDivElement>(null);
const pinnedMuuri = useRef<any>(null);
const othersMuuri = useRef<any>(null);
const isDraggingRef = useRef(false);
const pinnedNotes = notes.filter(n => n.isPinned).sort((a, b) => a.order - b.order);
const othersNotes = notes.filter(n => !n.isPinned).sort((a, b) => a.order - b.order);
// Memoize filtered and sorted notes to avoid recalculation on every render
const pinnedNotes = useMemo(
() => notes.filter(n => n.isPinned).sort((a, b) => a.order - b.order),
[notes]
);
const othersNotes = useMemo(
() => notes.filter(n => !n.isPinned).sort((a, b) => a.order - b.order),
[notes]
);
const handleDragEnd = async (grid: any) => {
// CRITICAL: Sync editingNote when underlying note changes (e.g., after moving to notebook)
// This ensures the NoteEditor gets the updated note with the new notebookId
useEffect(() => {
if (!editingNote) return;
// Find the updated version of the currently edited note in the notes array
const updatedNote = notes.find(n => n.id === editingNote.note.id);
if (updatedNote) {
// Check if any key properties changed (especially notebookId)
const notebookIdChanged = updatedNote.notebookId !== editingNote.note.notebookId;
if (notebookIdChanged) {
// Update the editingNote with the new data
setEditingNote(prev => prev ? { ...prev, note: updatedNote } : null);
}
}
}, [notes, editingNote]);
const handleDragEnd = useCallback(async (grid: any) => {
if (!grid) return;
// Prevent layout refresh during server update
isDraggingRef.current = true;
const items = grid.getItems();
const ids = items
.map((item: any) => item.getElement()?.getAttribute('data-id'))
.filter((id: any): id is string => !!id);
try {
await updateFullOrder(ids);
// Save order to database WITHOUT revalidating the page
// Muuri has already updated the visual layout, so we don't need to reload
await updateFullOrderWithoutRevalidation(ids);
} catch (error) {
console.error('Failed to persist order:', error);
} finally {
// Reset after animation/server roundtrip
setTimeout(() => {
isDraggingRef.current = false;
}, 1000);
}
};
}, []);
const refreshLayout = useCallback(() => {
// Use requestAnimationFrame for smoother updates
@@ -98,10 +139,16 @@ export function MasonryGrid({ notes }: MasonryGridProps) {
});
}, []);
// Initialize Muuri grids once on mount and sync when needed
useEffect(() => {
let isMounted = true;
let muuriInitialized = false;
const initMuuri = async () => {
// Prevent duplicate initialization
if (muuriInitialized) return;
muuriInitialized = true;
// Import web-animations-js polyfill
await import('web-animations-js');
// Dynamic import of Muuri to avoid SSR window error
@@ -114,8 +161,8 @@ export function MasonryGrid({ notes }: MasonryGridProps) {
const layoutOptions = {
dragEnabled: true,
// On mobile, restrict drag to handle to allow scrolling. On desktop, allow drag from anywhere.
dragHandle: isMobile ? '.drag-handle' : undefined,
// Always use specific drag handle to avoid conflicts
dragHandle: '.muuri-drag-handle',
dragContainer: document.body,
dragStartPredicate: {
distance: 10,
@@ -137,12 +184,14 @@ export function MasonryGrid({ notes }: MasonryGridProps) {
},
};
if (pinnedGridRef.current && !pinnedMuuri.current && pinnedNotes.length > 0) {
// Initialize pinned grid
if (pinnedGridRef.current && !pinnedMuuri.current) {
pinnedMuuri.current = new MuuriClass(pinnedGridRef.current, layoutOptions)
.on('dragEnd', () => handleDragEnd(pinnedMuuri.current));
}
if (othersGridRef.current && !othersMuuri.current && othersNotes.length > 0) {
// Initialize others grid
if (othersGridRef.current && !othersMuuri.current) {
othersMuuri.current = new MuuriClass(othersGridRef.current, layoutOptions)
.on('dragEnd', () => handleDragEnd(othersMuuri.current));
}
@@ -157,32 +206,37 @@ export function MasonryGrid({ notes }: MasonryGridProps) {
pinnedMuuri.current = null;
othersMuuri.current = null;
};
}, [pinnedNotes.length > 0, othersNotes.length > 0]);
// Only run once on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Synchronize items when notes change (e.g. searching, adding)
useEffect(() => {
if (isDraggingRef.current) return;
if (pinnedMuuri.current) {
pinnedMuuri.current.refreshItems().layout();
}
if (othersMuuri.current) {
othersMuuri.current.refreshItems().layout();
}
requestAnimationFrame(() => {
if (pinnedMuuri.current) {
pinnedMuuri.current.refreshItems().layout();
}
if (othersMuuri.current) {
othersMuuri.current.refreshItems().layout();
}
});
}, [notes]);
return (
<div className="masonry-container">
{pinnedNotes.length > 0 && (
<div className="mb-8">
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3 px-2">Pinned</h2>
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3 px-2">{t('notes.pinned')}</h2>
<div ref={pinnedGridRef} className="relative min-h-[100px]">
{pinnedNotes.map(note => (
<MasonryItem
key={note.id}
note={note}
onEdit={(note, readOnly) => setEditingNote({ note, readOnly })}
onEdit={handleEdit}
onResize={refreshLayout}
onDragStart={startDrag}
onDragEnd={endDrag}
isDragging={draggedNoteId === note.id}
/>
))}
</div>
@@ -192,15 +246,18 @@ export function MasonryGrid({ notes }: MasonryGridProps) {
{othersNotes.length > 0 && (
<div>
{pinnedNotes.length > 0 && (
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3 px-2">Others</h2>
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3 px-2">{t('notes.others')}</h2>
)}
<div ref={othersGridRef} className="relative min-h-[100px]">
{othersNotes.map(note => (
<MasonryItem
key={note.id}
note={note}
onEdit={(note, readOnly) => setEditingNote({ note, readOnly })}
onEdit={handleEdit}
onResize={refreshLayout}
onDragStart={startDrag}
onDragEnd={endDrag}
isDragging={draggedNoteId === note.id}
/>
))}
</div>