fix: i18n system overhaul and sidebar UI bugs
- Fix LanguageProvider: add RTL support (ar/fa), translation caching, prevent blank flash during load, browser language detection - Fix detect-user-language: extend whitelist from 5 to all 15 languages - Remove hardcoded initialLanguage="fr" from auth layout - Complete fr.json translation (all sections translated from English) - Add missing admin.ai keys to all 13 non-English locales - Translate ai.autoLabels, ai.batchOrganization, memoryEcho sections for all locales - Remove duplicate top-level autoLabels/batchOrganization from en.json - Fix notebook creation: replace window.location.reload() with createNotebookOptimistic + router.refresh() - Fix notebook name truncation in sidebar with min-w-0 - Remove redundant router.refresh() after note creation in page.tsx Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,14 +10,28 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Pin, Bell, GripVertical, X, Link2, FolderOpen, StickyNote, LucideIcon, Folder, Briefcase, FileText, Zap, BarChart3, Globe, Sparkles, Book, Heart, Crown, Music, Building2, Tag } from 'lucide-react'
|
||||
import { useState, useEffect, useTransition, useOptimistic } from 'react'
|
||||
import { Pin, Bell, GripVertical, X, Link2, FolderOpen, StickyNote, LucideIcon, Folder, Briefcase, FileText, Zap, BarChart3, Globe, Sparkles, Book, Heart, Crown, Music, Building2 } from 'lucide-react'
|
||||
import { useState, useEffect, useTransition, useOptimistic, memo } from 'react'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { deleteNote, toggleArchive, togglePin, updateColor, updateNote, updateSize, getNoteAllUsers, leaveSharedNote, removeFusedBadge } from '@/app/actions/notes'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { formatDistanceToNow, Locale } from 'date-fns'
|
||||
import * as dateFnsLocales from 'date-fns/locale'
|
||||
import { enUS } from 'date-fns/locale/en-US'
|
||||
import { fr } from 'date-fns/locale/fr'
|
||||
import { es } from 'date-fns/locale/es'
|
||||
import { de } from 'date-fns/locale/de'
|
||||
import { faIR } from 'date-fns/locale/fa-IR'
|
||||
import { it } from 'date-fns/locale/it'
|
||||
import { pt } from 'date-fns/locale/pt'
|
||||
import { ru } from 'date-fns/locale/ru'
|
||||
import { zhCN } from 'date-fns/locale/zh-CN'
|
||||
import { ja } from 'date-fns/locale/ja'
|
||||
import { ko } from 'date-fns/locale/ko'
|
||||
import { ar } from 'date-fns/locale/ar'
|
||||
import { hi } from 'date-fns/locale/hi'
|
||||
import { nl } from 'date-fns/locale/nl'
|
||||
import { pl } from 'date-fns/locale/pl'
|
||||
import { MarkdownContent } from './markdown-content'
|
||||
import { LabelBadge } from './label-badge'
|
||||
import { NoteImages } from './note-images'
|
||||
@@ -36,25 +50,25 @@ import { toast } from 'sonner'
|
||||
|
||||
// Mapping of supported languages to date-fns locales
|
||||
const localeMap: Record<string, Locale> = {
|
||||
en: dateFnsLocales.enUS,
|
||||
fr: dateFnsLocales.fr,
|
||||
es: dateFnsLocales.es,
|
||||
de: dateFnsLocales.de,
|
||||
fa: dateFnsLocales.faIR,
|
||||
it: dateFnsLocales.it,
|
||||
pt: dateFnsLocales.pt,
|
||||
ru: dateFnsLocales.ru,
|
||||
zh: dateFnsLocales.zhCN,
|
||||
ja: dateFnsLocales.ja,
|
||||
ko: dateFnsLocales.ko,
|
||||
ar: dateFnsLocales.ar,
|
||||
hi: dateFnsLocales.hi,
|
||||
nl: dateFnsLocales.nl,
|
||||
pl: dateFnsLocales.pl,
|
||||
en: enUS,
|
||||
fr: fr,
|
||||
es: es,
|
||||
de: de,
|
||||
fa: faIR,
|
||||
it: it,
|
||||
pt: pt,
|
||||
ru: ru,
|
||||
zh: zhCN,
|
||||
ja: ja,
|
||||
ko: ko,
|
||||
ar: ar,
|
||||
hi: hi,
|
||||
nl: nl,
|
||||
pl: pl,
|
||||
}
|
||||
|
||||
function getDateLocale(language: string): Locale {
|
||||
return localeMap[language] || dateFnsLocales.enUS
|
||||
return localeMap[language] || enUS
|
||||
}
|
||||
|
||||
// Map icon names to lucide-react components
|
||||
@@ -118,7 +132,7 @@ function getAvatarColor(name: string): string {
|
||||
return colors[hash % colors.length]
|
||||
}
|
||||
|
||||
export function NoteCard({
|
||||
export const NoteCard = memo(function NoteCard({
|
||||
note,
|
||||
onEdit,
|
||||
onDragStart,
|
||||
@@ -133,7 +147,7 @@ export function NoteCard({
|
||||
const { data: session } = useSession()
|
||||
const { t, language } = useLanguage()
|
||||
const { notebooks, moveNoteToNotebookOptimistic } = useNotebooks()
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const [, startTransition] = useTransition()
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [showCollaboratorDialog, setShowCollaboratorDialog] = useState(false)
|
||||
const [collaborators, setCollaborators] = useState<any[]>([])
|
||||
@@ -172,23 +186,33 @@ export function NoteCard({
|
||||
|
||||
// Load collaborators when note changes
|
||||
useEffect(() => {
|
||||
let isMounted = true
|
||||
|
||||
const loadCollaborators = async () => {
|
||||
if (note.userId) {
|
||||
if (note.userId && isMounted) {
|
||||
try {
|
||||
const users = await getNoteAllUsers(note.id)
|
||||
setCollaborators(users)
|
||||
// Owner is always first in the list
|
||||
if (users.length > 0) {
|
||||
setOwner(users[0])
|
||||
if (isMounted) {
|
||||
setCollaborators(users)
|
||||
// Owner is always first in the list
|
||||
if (users.length > 0) {
|
||||
setOwner(users[0])
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load collaborators:', error)
|
||||
setCollaborators([])
|
||||
if (isMounted) {
|
||||
setCollaborators([])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadCollaborators()
|
||||
|
||||
return () => {
|
||||
isMounted = false
|
||||
}
|
||||
}, [note.id, note.userId])
|
||||
|
||||
const handleDelete = async () => {
|
||||
@@ -525,47 +549,12 @@ export function NoteCard({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Labels - ONLY show if note belongs to a notebook (labels are contextual per PRD) */}
|
||||
{/* Labels - using shared LabelBadge component */}
|
||||
{optimisticNote.notebookId && optimisticNote.labels && optimisticNote.labels.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-3">
|
||||
{optimisticNote.labels.map((label) => {
|
||||
// Map label names to Keep style colors
|
||||
const getLabelColor = (labelName: string) => {
|
||||
if (labelName.includes('hôtels') || labelName.includes('réservations')) {
|
||||
return 'bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300'
|
||||
} else if (labelName.includes('vols') || labelName.includes('flight')) {
|
||||
return 'bg-sky-50 dark:bg-sky-900/30 text-sky-700 dark:text-sky-300'
|
||||
} else if (labelName.includes('restos') || labelName.includes('restaurant')) {
|
||||
return 'bg-orange-50 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300'
|
||||
} else {
|
||||
return 'bg-emerald-50 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300'
|
||||
}
|
||||
}
|
||||
|
||||
// Map label names to Keep style icons
|
||||
const getLabelIcon = (labelName: string) => {
|
||||
if (labelName.includes('hôtels')) return 'label'
|
||||
else if (labelName.includes('vols')) return 'flight'
|
||||
else if (labelName.includes('restos')) return 'restaurant'
|
||||
else return 'label'
|
||||
}
|
||||
|
||||
const icon = getLabelIcon(label)
|
||||
const colorClass = getLabelColor(label)
|
||||
|
||||
return (
|
||||
<span
|
||||
key={label}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold",
|
||||
colorClass
|
||||
)}
|
||||
>
|
||||
<Tag className="w-3 h-3" />
|
||||
{label}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
{optimisticNote.labels.map((label) => (
|
||||
<LabelBadge key={label} label={label} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -655,4 +644,4 @@ export function NoteCard({
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user