From 8daf50ac3f0594b096b38562d9931ca9e951daa5 Mon Sep 17 00:00:00 2001 From: Sepehr Ramezani Date: Sun, 29 Mar 2026 22:14:05 +0200 Subject: [PATCH] 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 --- keep-notes/app/(auth)/layout.tsx | 2 +- keep-notes/app/(main)/page.tsx | 45 +- keep-notes/components/connections-badge.tsx | 75 +- .../components/create-notebook-dialog.tsx | 26 +- keep-notes/components/markdown-content.tsx | 7 +- keep-notes/components/masonry-grid.tsx | 4 +- keep-notes/components/note-actions.tsx | 15 + keep-notes/components/note-card.tsx | 123 ++-- keep-notes/components/notebooks-list.tsx | 2 +- .../components/recent-notes-section.tsx | 182 ++++- keep-notes/lib/i18n/LanguageProvider.tsx | 51 +- keep-notes/lib/i18n/detect-user-language.ts | 2 +- keep-notes/locales/ar.json | 60 +- keep-notes/locales/de.json | 62 +- keep-notes/locales/en.json | 28 - keep-notes/locales/es.json | 61 +- keep-notes/locales/fa.json | 60 +- keep-notes/locales/fr.json | 643 +++++++++--------- keep-notes/locales/hi.json | 56 +- keep-notes/locales/it.json | 92 +-- keep-notes/locales/ja.json | 62 +- keep-notes/locales/ko.json | 62 +- keep-notes/locales/nl.json | 92 +-- keep-notes/locales/pl.json | 92 +-- keep-notes/locales/pt.json | 92 +-- keep-notes/locales/ru.json | 92 +-- keep-notes/locales/zh.json | 58 +- 27 files changed, 1210 insertions(+), 936 deletions(-) diff --git a/keep-notes/app/(auth)/layout.tsx b/keep-notes/app/(auth)/layout.tsx index 50304a5..a70d6b8 100644 --- a/keep-notes/app/(auth)/layout.tsx +++ b/keep-notes/app/(auth)/layout.tsx @@ -8,7 +8,7 @@ export default function AuthLayout({ children: React.ReactNode; }>) { return ( - +
{children} diff --git a/keep-notes/app/(main)/page.tsx b/keep-notes/app/(main)/page.tsx index 2aa5e5b..6e3a93f 100644 --- a/keep-notes/app/(main)/page.tsx +++ b/keep-notes/app/(main)/page.tsx @@ -3,7 +3,7 @@ import { useState, useEffect, useCallback } from 'react' import { useSearchParams, useRouter } from 'next/navigation' import { Note } from '@/lib/types' -import { getAllNotes, getPinnedNotes, getRecentNotes, searchNotes } from '@/app/actions/notes' +import { getAllNotes, searchNotes } from '@/app/actions/notes' import { getAISettings } from '@/app/actions/ai-settings' import { NoteInput } from '@/components/note-input' import { MasonryGrid } from '@/components/masonry-grid' @@ -35,7 +35,7 @@ export default function HomePage() { const [notes, setNotes] = useState([]) const [pinnedNotes, setPinnedNotes] = useState([]) const [recentNotes, setRecentNotes] = useState([]) - const [showRecentNotes, setShowRecentNotes] = useState(false) + const [showRecentNotes, setShowRecentNotes] = useState(true) const [editingNote, setEditingNote] = useState<{ note: Note; readOnly?: boolean } | null>(null) const [isLoading, setIsLoading] = useState(true) const [notebookSuggestion, setNotebookSuggestion] = useState<{ noteId: string; content: string } | null>(null) @@ -137,8 +137,9 @@ export default function HomePage() { } - // Refresh in background to ensure data consistency (non-blocking) - router.refresh() + // Note: revalidatePath('/') is already called in the server action, + // and the optimistic update above already adds the note to state. + // No additional router.refresh() needed — avoids visible re-render flash. }, [searchParams, labels, router]) const handleOpenNote = (noteId: string) => { @@ -156,9 +157,11 @@ export default function HomePage() { const loadSettings = async () => { try { const settings = await getAISettings() - setShowRecentNotes(settings.showRecentNotes === true) + // Default to true if setting is undefined or null + setShowRecentNotes(settings?.showRecentNotes !== false) } catch (error) { - setShowRecentNotes(false) + // Default to true on error + setShowRecentNotes(true) } } loadSettings() @@ -203,19 +206,31 @@ export default function HomePage() { ) } - // Load pinned notes separately (shown in favorites section) - const pinned = await getPinnedNotes(notebookFilter || undefined) + // Derive pinned notes from already-fetched allNotes (no extra server call) + const pinnedFilter = notebookFilter + ? allNotes.filter((note: any) => note.isPinned && note.notebookId === notebookFilter) + : allNotes.filter((note: any) => note.isPinned && !note.notebookId) - setPinnedNotes(pinned) + setPinnedNotes(pinnedFilter) - // Load recent notes only if enabled in settings + // Derive recent notes from already-fetched allNotes (no extra server call) if (showRecentNotes) { - const recent = await getRecentNotes(3) - // Filter recent notes by current filters + const sevenDaysAgo = new Date() + sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7) + sevenDaysAgo.setHours(0, 0, 0, 0) + + const recentFiltered = allNotes + .filter((note: any) => { + return !note.isArchived && !note.dismissedFromRecent && note.contentUpdatedAt >= sevenDaysAgo + }) + .sort((a: any, b: any) => new Date(b.contentUpdatedAt).getTime() - new Date(a.createdAt).getTime()) + .slice(0, 3) + if (notebookFilter) { - setRecentNotes(recent.filter((note: any) => note.notebookId === notebookFilter)) + setRecentNotes(recentFiltered.filter((note: any) => note.notebookId === notebookFilter)) } else { - setRecentNotes(recent.filter((note: any) => !note.notebookId)) + // Show ALL recent notes when in inbox (not just notes without notebooks) + setRecentNotes(recentFiltered) } } else { setRecentNotes([]) @@ -392,7 +407,7 @@ export default function HomePage() { {/* Recent Notes Section - Only shown if enabled in settings */} {showRecentNotes && ( !note.isPinned)} + recentNotes={recentNotes} onEdit={(note, readOnly) => setEditingNote({ note, readOnly })} /> )} diff --git a/keep-notes/components/connections-badge.tsx b/keep-notes/components/connections-badge.tsx index 2b36152..16ca2a5 100644 --- a/keep-notes/components/connections-badge.tsx +++ b/keep-notes/components/connections-badge.tsx @@ -1,9 +1,10 @@ 'use client' -import { useState, useEffect } from 'react' +import { memo, useState, useEffect } from 'react' import { Sparkles } from 'lucide-react' import { cn } from '@/lib/utils' import { useLanguage } from '@/lib/i18n/LanguageProvider' +import { getConnectionsCount } from '@/lib/connections-cache' interface ConnectionsBadgeProps { noteId: string @@ -11,54 +12,44 @@ interface ConnectionsBadgeProps { className?: string } -interface ConnectionData { - noteId: string - title: string | null - content: string - createdAt: Date - similarity: number - daysApart: number -} - -interface ConnectionsResponse { - connections: ConnectionData[] - pagination: { - total: number - page: number - limit: number - totalPages: number - hasNext: boolean - hasPrev: boolean - } -} - -export function ConnectionsBadge({ noteId, onClick, className }: ConnectionsBadgeProps) { +export const ConnectionsBadge = memo(function ConnectionsBadge({ noteId, onClick, className }: ConnectionsBadgeProps) { const { t } = useLanguage() const [connectionCount, setConnectionCount] = useState(0) const [isLoading, setIsLoading] = useState(false) const [isHovered, setIsHovered] = useState(false) + const [fetchAttempted, setFetchAttempted] = useState(false) useEffect(() => { + if (fetchAttempted) return + setFetchAttempted(true) + + let isMounted = true + const fetchConnections = async () => { setIsLoading(true) try { - const res = await fetch(`/api/ai/echo/connections?noteId=${noteId}&limit=1`) - if (!res.ok) { - throw new Error('Failed to fetch connections') + const count = await getConnectionsCount(noteId) + if (isMounted) { + setConnectionCount(count) } - - const data: ConnectionsResponse = await res.json() - setConnectionCount(data.pagination.total || 0) } catch (error) { console.error('[ConnectionsBadge] Failed to fetch connections:', error) - setConnectionCount(0) + if (isMounted) { + setConnectionCount(0) + } } finally { - setIsLoading(false) + if (isMounted) { + setIsLoading(false) + } } } fetchConnections() - }, [noteId]) + + return () => { + isMounted = false + } + }, [noteId]) // eslint-disable-line react-hooks/exhaustive-deps // Don't render if no connections or still loading if (connectionCount === 0 || isLoading) { @@ -69,19 +60,11 @@ export function ConnectionsBadge({ noteId, onClick, className }: ConnectionsBadg const badgeText = t('memoryEcho.connectionsBadge', { count: connectionCount, plural }) return ( -
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} @@ -91,4 +74,4 @@ export function ConnectionsBadge({ noteId, onClick, className }: ConnectionsBadg {badgeText}
) -} +}) diff --git a/keep-notes/components/create-notebook-dialog.tsx b/keep-notes/components/create-notebook-dialog.tsx index 7237888..b1e8e5b 100644 --- a/keep-notes/components/create-notebook-dialog.tsx +++ b/keep-notes/components/create-notebook-dialog.tsx @@ -14,6 +14,7 @@ import { import { Input } from '@/components/ui/input' import { cn } from '@/lib/utils' import { useLanguage } from '@/lib/i18n' +import { useNotebooks } from '@/context/notebooks-context' const NOTEBOOK_ICONS = [ { icon: Folder, name: 'folder' }, @@ -48,6 +49,7 @@ interface CreateNotebookDialogProps { export function CreateNotebookDialog({ open, onOpenChange }: CreateNotebookDialogProps) { const router = useRouter() const { t } = useLanguage() + const { createNotebookOptimistic } = useNotebooks() const [name, setName] = useState('') const [selectedIcon, setSelectedIcon] = useState('folder') const [selectedColor, setSelectedColor] = useState('#3B82F6') @@ -61,24 +63,14 @@ export function CreateNotebookDialog({ open, onOpenChange }: CreateNotebookDialo setIsSubmitting(true) try { - const response = await fetch('/api/notebooks', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name: name.trim(), - icon: selectedIcon, - color: selectedColor, - }), + await createNotebookOptimistic({ + name: name.trim(), + icon: selectedIcon, + color: selectedColor, }) - - if (response.ok) { - // Close dialog and reload - onOpenChange?.(false) - window.location.reload() - } else { - const error = await response.json() - console.error('Failed to create notebook:', error) - } + // Close dialog — no full page reload needed + onOpenChange?.(false) + router.refresh() } catch (error) { console.error('Failed to create notebook:', error) } finally { diff --git a/keep-notes/components/markdown-content.tsx b/keep-notes/components/markdown-content.tsx index d4cb0fc..30e7716 100644 --- a/keep-notes/components/markdown-content.tsx +++ b/keep-notes/components/markdown-content.tsx @@ -1,5 +1,6 @@ 'use client' +import { memo } from 'react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' @@ -11,7 +12,7 @@ interface MarkdownContentProps { className?: string } -export function MarkdownContent({ content, className = '' }: MarkdownContentProps) { +export const MarkdownContent = memo(function MarkdownContent({ content, className }: MarkdownContentProps) { return ( ) -} +}) diff --git a/keep-notes/components/masonry-grid.tsx b/keep-notes/components/masonry-grid.tsx index ff45cd2..597f9d3 100644 --- a/keep-notes/components/masonry-grid.tsx +++ b/keep-notes/components/masonry-grid.tsx @@ -26,7 +26,7 @@ interface MasonryItemProps { isDragging?: boolean; } -const MasonryItem = function MasonryItem({ note, onEdit, onResize, onNoteSizeChange, onDragStart, onDragEnd, isDragging }: MasonryItemProps) { +const MasonryItem = memo(function MasonryItem({ note, onEdit, onResize, onNoteSizeChange, onDragStart, onDragEnd, isDragging }: MasonryItemProps) { const resizeRef = useResizeObserver(onResize); useEffect(() => { @@ -55,7 +55,7 @@ const MasonryItem = function MasonryItem({ note, onEdit, onResize, onNoteSizeCha
); -}; +}) export function MasonryGrid({ notes, onEdit }: MasonryGridProps) { const { t } = useLanguage(); diff --git a/keep-notes/components/note-actions.tsx b/keep-notes/components/note-actions.tsx index 1a954e8..9f12790 100644 --- a/keep-notes/components/note-actions.tsx +++ b/keep-notes/components/note-actions.tsx @@ -87,6 +87,21 @@ export function NoteActions({ + {/* Pin/Unpin Option */} + + {isPinned ? ( + <> + + {t('notes.unpin')} + + ) : ( + <> + + {t('notes.pin')} + + )} + + {isArchived ? ( <> diff --git a/keep-notes/components/note-card.tsx b/keep-notes/components/note-card.tsx index eba1fd5..ba9af78 100644 --- a/keep-notes/components/note-card.tsx +++ b/keep-notes/components/note-card.tsx @@ -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 = { - 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([]) @@ -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 && (
- {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 ( - - - {label} - - ) - })} + {optimisticNote.labels.map((label) => ( + + ))}
)} @@ -655,4 +644,4 @@ export function NoteCard({ )} ) -} \ No newline at end of file +}) \ No newline at end of file diff --git a/keep-notes/components/notebooks-list.tsx b/keep-notes/components/notebooks-list.tsx index b85ab49..4dbbc21 100644 --- a/keep-notes/components/notebooks-list.tsx +++ b/keep-notes/components/notebooks-list.tsx @@ -234,7 +234,7 @@ export function NotebooksList() { )} > - {notebook.name} + {notebook.name} {notebook.notesCount > 0 && ( ({notebook.notesCount}) )} diff --git a/keep-notes/components/recent-notes-section.tsx b/keep-notes/components/recent-notes-section.tsx index 22d7c33..6f46e74 100644 --- a/keep-notes/components/recent-notes-section.tsx +++ b/keep-notes/components/recent-notes-section.tsx @@ -1,9 +1,18 @@ 'use client' +import { useState, useTransition, useOptimistic } from 'react' import { Note } from '@/lib/types' -import { Clock } from 'lucide-react' +import { Clock, Pin, FolderOpen, Trash2, Folder, X } from 'lucide-react' import { cn } from '@/lib/utils' import { useLanguage } from '@/lib/i18n' +import { Button } from './ui/button' +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from './ui/dropdown-menu' +import { togglePin, deleteNote, dismissFromRecent } from '@/app/actions/notes' +import { useRouter } from 'next/navigation' +import { useNotebooks } from '@/context/notebooks-context' +import { useNoteRefresh } from '@/context/NoteRefreshContext' +import { toast } from 'sonner' +import { StickyNote } from 'lucide-react' interface RecentNotesSectionProps { recentNotes: Note[] @@ -53,16 +62,89 @@ function CompactCard({ onEdit?: (note: Note, readOnly?: boolean) => void }) { const { t } = useLanguage() - const timeAgo = getCompactTime(note.contentUpdatedAt || note.updatedAt, t) + const router = useRouter() + const { notebooks, moveNoteToNotebookOptimistic } = useNotebooks() + const { triggerRefresh } = useNoteRefresh() + const [isDeleting, setIsDeleting] = useState(false) + const [showNotebookMenu, setShowNotebookMenu] = useState(false) + const [, startTransition] = useTransition() + + // Optimistic UI state + const [optimisticNote, addOptimisticNote] = useOptimistic( + note, + (state: Note, newProps: Partial) => ({ ...state, ...newProps }) + ) + + const timeAgo = getCompactTime(optimisticNote.contentUpdatedAt || optimisticNote.updatedAt, t) const isFirstNote = index === 0 + const handleTogglePin = async (e: React.MouseEvent) => { + e.stopPropagation() + startTransition(async () => { + const newPinnedState = !optimisticNote.isPinned + addOptimisticNote({ isPinned: newPinnedState }) + await togglePin(note.id, newPinnedState) + + // Trigger global refresh to update lists + triggerRefresh() + router.refresh() + + if (newPinnedState) { + toast.success(t('notes.pinned') || 'Note pinned') + } else { + toast.info(t('notes.unpinned') || 'Note unpinned') + } + }) + } + + const handleMoveToNotebook = async (notebookId: string | null) => { + await moveNoteToNotebookOptimistic(note.id, notebookId) + setShowNotebookMenu(false) + triggerRefresh() + } + + const handleDelete = async (e: React.MouseEvent) => { + e.stopPropagation() + if (confirm(t('notes.confirmDelete'))) { + setIsDeleting(true) + try { + await deleteNote(note.id) + triggerRefresh() + router.refresh() + } catch (error) { + console.error('Failed to delete note:', error) + setIsDeleting(false) + } + } + } + + const handleDismiss = async (e: React.MouseEvent) => { + e.stopPropagation() + // Optimistic removal + setIsDeleting(true) + try { + await dismissFromRecent(note.id) + // Don't refresh list to prevent immediate replacement + // triggerRefresh() + // router.refresh() + toast.success(t('notes.dismissed') || 'Note dismissed from recent') + } catch (error) { + console.error('Failed to dismiss note:', error) + setIsDeleting(false) + toast.error(t('common.error') || 'Failed to dismiss') + } + } + + if (isDeleting) return null + return ( - + + {/* Move to Notebook */} + + + + + e.stopPropagation()}> +
+ {t('notebookSuggestion.moveToNotebook')} +
+ handleMoveToNotebook(null)}> + + {t('notebookSuggestion.generalNotes')} + + {notebooks.map((notebook: any) => ( + handleMoveToNotebook(notebook.id)} + > + + {notebook.name} + + ))} +
+
+ + {/* Dismiss Button */} + + + {/* Delete Button */} + + +

- {note.title || t('notes.untitled')} + {optimisticNote.title || t('notes.untitled')}

- {note.content?.substring(0, 80) || ''} - {note.content && note.content.length > 80 && '...'} + {(optimisticNote.content && typeof optimisticNote.content === 'string') ? optimisticNote.content.substring(0, 80) : ''} + {optimisticNote.content && typeof optimisticNote.content === 'string' && optimisticNote.content.length > 80 && '...'}

@@ -90,18 +241,19 @@ function CompactCard({
- {note.notebookId && ( + {optimisticNote.isPinned && ( + + )} + {optimisticNote.notebookId && (
)} - {note.labels && note.labels.length > 0 && ( -
+ {optimisticNote.labels && optimisticNote.labels.length > 0 && ( +
)}
- -
- +
) } diff --git a/keep-notes/lib/i18n/LanguageProvider.tsx b/keep-notes/lib/i18n/LanguageProvider.tsx index b19d1c2..ebb67eb 100644 --- a/keep-notes/lib/i18n/LanguageProvider.tsx +++ b/keep-notes/lib/i18n/LanguageProvider.tsx @@ -1,6 +1,6 @@ 'use client' -import { createContext, useContext, useEffect, useState } from 'react' +import { createContext, useContext, useEffect, useState, useCallback, useRef } from 'react' import type { ReactNode } from 'react' import { SupportedLanguage, loadTranslations, getTranslationValue, Translations } from './load-translations' @@ -13,18 +13,35 @@ type LanguageContextType = { const LanguageContext = createContext(undefined) +const RTL_LANGUAGES: SupportedLanguage[] = ['ar', 'fa'] + +function updateDocumentDirection(lang: SupportedLanguage) { + document.documentElement.lang = lang + document.documentElement.dir = RTL_LANGUAGES.includes(lang) ? 'rtl' : 'ltr' +} + export function LanguageProvider({ children, initialLanguage = 'en' }: { children: ReactNode initialLanguage?: SupportedLanguage }) { const [language, setLanguageState] = useState(initialLanguage) const [translations, setTranslations] = useState(null) + const cacheRef = useRef>(new Map()) - // Load translations when language changes + // Load translations when language changes (with caching) useEffect(() => { + const cached = cacheRef.current.get(language) + if (cached) { + setTranslations(cached) + updateDocumentDirection(language) + return + } + const loadLang = async () => { const loaded = await loadTranslations(language) + cacheRef.current.set(language, loaded) setTranslations(loaded) + updateDocumentDirection(language) } loadLang() }, [language]) @@ -34,7 +51,6 @@ export function LanguageProvider({ children, initialLanguage = 'en' }: { const saved = localStorage.getItem('user-language') as SupportedLanguage if (saved) { setLanguageState(saved) - document.documentElement.lang = saved } else { // Auto-detect from browser language const browserLang = navigator.language.split('-')[0] as SupportedLanguage @@ -43,35 +59,44 @@ export function LanguageProvider({ children, initialLanguage = 'en' }: { if (supportedLangs.includes(browserLang)) { setLanguageState(browserLang) localStorage.setItem('user-language', browserLang) - document.documentElement.lang = browserLang } } }, []) - const setLanguage = (lang: SupportedLanguage) => { + const setLanguage = useCallback((lang: SupportedLanguage) => { setLanguageState(lang) localStorage.setItem('user-language', lang) - // Update HTML lang attribute for font styling - document.documentElement.lang = lang - } + updateDocumentDirection(lang) + }, []) - const t = (key: string, params?: Record) => { + const t = useCallback((key: string, params?: Record) => { if (!translations) return key let value: any = getTranslationValue(translations, key) // Replace parameters like {count}, {percentage}, etc. - if (params) { + if (params && typeof value === 'string') { Object.entries(params).forEach(([param, paramValue]) => { value = value.replace(`{${param}}`, String(paramValue)) }) } - return value - } + return typeof value === 'string' ? value : key + }, [translations]) + // During initial load, show children with the initial language as fallback + // to prevent blank flash if (!translations) { - return null // Show loading state if needed + return ( + key, + translations: {} as Translations + }}> + {children} + + ) } return ( diff --git a/keep-notes/lib/i18n/detect-user-language.ts b/keep-notes/lib/i18n/detect-user-language.ts index 13adb38..5d91e02 100644 --- a/keep-notes/lib/i18n/detect-user-language.ts +++ b/keep-notes/lib/i18n/detect-user-language.ts @@ -49,7 +49,7 @@ export async function detectUserLanguage(): Promise { if (sortedLanguages.length > 0) { const topLanguage = sortedLanguages[0][0] as SupportedLanguage // Verify it's a supported language - if (['fr', 'en', 'es', 'de', 'fa'].includes(topLanguage)) { + if (['en', 'fr', 'es', 'de', 'fa', 'it', 'pt', 'ru', 'zh', 'ja', 'ko', 'ar', 'hi', 'nl', 'pl'].includes(topLanguage)) { return topLanguage } } diff --git a/keep-notes/locales/ar.json b/keep-notes/locales/ar.json index 592e24b..439e7b9 100644 --- a/keep-notes/locales/ar.json +++ b/keep-notes/locales/ar.json @@ -63,7 +63,13 @@ "tagsGenerationProvider": "مزود توليد الوسوم", "title": "تكوين الذكاء الاصطناعي", "updateFailed": "فشل تحديث إعدادات الذكاء الاصطناعي", - "updateSuccess": "تم تحديث إعدادات الذكاء الاصطناعي بنجاح" + "updateSuccess": "تم تحديث إعدادات الذكاء الاصطناعي بنجاح", + "bestValue": "أفضل قيمة", + "bestQuality": "أفضل جودة", + "providerOllamaOption": "🦙 Ollama (Local & Free)", + "providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)", + "providerCustomOption": "🔧 Custom OpenAI-Compatible", + "saved": "(تم الحفظ)" }, "aiTest": { "description": "اختبر مزودي الذكاء الاصطناعي لتوليد الوسوم وتضمينات البحث الدلالي", @@ -153,34 +159,36 @@ "analyzing": "الذكاء الاصطناعي يحلل...", "assistant": "مساعد الذكاء الاصطناعي", "autoLabels": { - "analyzing": "تحليل ملاحظاتك لاقتراحات التصنيفات...", - "create": "إنشاء", - "createNewLabel": "Create this new label and add it", - "created": "{count} labels created successfully", - "creating": "إنشاء التصنيفات...", - "description": "I've detected recurring themes in \"{notebookName}\" ({totalNotes} notes). Create labels for them?", - "error": "Failed to fetch label suggestions", + "error": "فشل في جلب اقتراحات التصنيفات", + "noLabelsSelected": "لم يتم اختيار تصنيفات", + "created": "تم إنشاء {count} تصنيفات بنجاح", + "analyzing": "تحليل ملاحظاتك...", + "title": "اقتراحات تصنيفات جديدة", + "description": "اكتشفت مواضيع متكررة في \"{notebookName}\" ({totalNotes} ملاحظات). هل تريد إنشاء تصنيفات لها؟", + "note": "ملاحظة", + "notes": "ملاحظات", + "typeContent": "اكتب محتوى للحصول على اقتراحات التصنيفات...", + "createNewLabel": "إنشاء هذا التصنيف الجديد وإضافته", "new": "(جديد)", - "noLabelsSelected": "No labels selected", - "note": "note", - "notes": "notes", - "title": "اقتراحات التصنيفات", - "typeContent": "Type content to get label suggestions..." + "create": "إنشاء", + "creating": "جارٍ إنشاء التصنيفات...", + "notesCount": "{count} ملاحظات", + "typeForSuggestions": "اكتب محتوى للحصول على اقتراحات التصنيفات..." }, "batchOrganization": { - "analyzing": "Analyzing your notes...", - "apply": "Apply", - "applyFailed": "Failed to apply organization plan", - "applying": "Applying...", - "description": "سيقوم الذكاء الاصطناعي بتحليل ملاحظاتك واقتراح تنظيمها في دفاتر.", - "error": "Failed to create organization plan", - "noNotebooks": "No notebooks available. Create notebooks first to organize your notes.", - "noNotesSelected": "No notes selected", - "noSuggestions": "AI could not find a good way to organize these notes.", - "selectAllIn": "Select all notes in {notebook}", - "selectNote": "Select note: {title}", - "success": "{count} notes moved successfully", - "title": "Organize with AI" + "title": "تنظيم بالذكاء الاصطناعي", + "description": "سيحلل الذكاء الاصطناعي ملاحظاتك ويقترح تنظيمها في دفاتر.", + "analyzing": "تحليل ملاحظاتك...", + "noNotebooks": "لا توجد دفاتر متاحة. أنشئ دفاتر أولاً.", + "noSuggestions": "لم يتمكن الذكاء الاصطناعي من إيجاد طريقة جيدة لتنظيم هذه الملاحظات.", + "apply": "تطبيق", + "applying": "جارٍ التطبيق...", + "success": "تم نقل {count} ملاحظات بنجاح", + "error": "فشل في إنشاء خطة التنظيم", + "noNotesSelected": "لم يتم اختيار ملاحظات", + "applyFailed": "فشل في تطبيق خطة التنظيم", + "selectAllIn": "تحديد جميع الملاحظات في {notebook}", + "selectNote": "تحديد ملاحظة: {title}" }, "clarify": "توضيح", "clickToAddTag": "انقر لإضافة هذا الوسم", diff --git a/keep-notes/locales/de.json b/keep-notes/locales/de.json index 621f733..1057132 100644 --- a/keep-notes/locales/de.json +++ b/keep-notes/locales/de.json @@ -63,7 +63,13 @@ "tagsGenerationProvider": "Tags-Generierungsanbieter", "title": "KI-Konfiguration", "updateFailed": "Fehler beim Aktualisieren der KI-Einstellungen", - "updateSuccess": "KI-Einstellungen erfolgreich aktualisiert" + "updateSuccess": "KI-Einstellungen erfolgreich aktualisiert", + "bestValue": "Bestes Preis-Leistungs-Verhältnis", + "bestQuality": "Beste Qualität", + "providerOllamaOption": "🦙 Ollama (Local & Free)", + "providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)", + "providerCustomOption": "🔧 Custom OpenAI-Compatible", + "saved": "(Gespeichert)" }, "aiTest": { "description": "Testen Sie Ihre KI-Anbieter für Tag-Generierung und semantische Such-Embeddings", @@ -153,34 +159,36 @@ "analyzing": "KI analysiert...", "assistant": "KI-Assistent", "autoLabels": { - "analyzing": "Analysiere Ihre Notizen für Etikettenvorschläge...", - "create": "Erstellen", - "createNewLabel": "Create this new label and add it", - "created": "{count} labels created successfully", - "creating": "Erstelle Etiketten...", - "description": "I've detected recurring themes in \"{notebookName}\" ({totalNotes} notes). Create labels for them?", - "error": "Failed to fetch label suggestions", + "error": "Fehler beim Abrufen der Etikettvorschläge", + "noLabelsSelected": "Keine Etiketten ausgewählt", + "created": "{count} Etiketten erfolgreich erstellt", + "analyzing": "Analysiere deine Notizen...", + "title": "Neue Etikettvorschläge", + "description": "Ich habe wiederkehrende Themen in \"{notebookName}\" ({totalNotes} Notizen) erkannt. Etiketten erstellen?", + "note": "Notiz", + "notes": "Notizen", + "typeContent": "Inhalt eingeben für Etikettvorschläge...", + "createNewLabel": "Dieses neue Etikett erstellen und hinzufügen", "new": "(neu)", - "noLabelsSelected": "No labels selected", - "note": "note", - "notes": "notes", - "title": "Etikettenvorschläge", - "typeContent": "Type content to get label suggestions..." + "create": "Erstellen", + "creating": "Etiketten werden erstellt...", + "notesCount": "{count} Notizen", + "typeForSuggestions": "Inhalt eingeben für Etikettvorschläge..." }, "batchOrganization": { - "analyzing": "Analyzing your notes...", - "apply": "Apply", - "applyFailed": "Failed to apply organization plan", - "applying": "Applying...", - "description": "Die KI analysiert Ihre Notizen und schlägt vor, sie in Notizbüchern zu organisieren.", - "error": "Failed to create organization plan", - "noNotebooks": "No notebooks available. Create notebooks first to organize your notes.", - "noNotesSelected": "No notes selected", - "noSuggestions": "AI could not find a good way to organize these notes.", - "selectAllIn": "Select all notes in {notebook}", - "selectNote": "Select note: {title}", - "success": "{count} notes moved successfully", - "title": "Organize with AI" + "title": "Mit KI organisieren", + "description": "Die KI analysiert deine Notizen und schlägt vor, sie in Notizbücher zu organisieren.", + "analyzing": "Analysiere deine Notizen...", + "noNotebooks": "Keine Notizbücher verfügbar. Erstelle zuerst Notizbücher.", + "noSuggestions": "Die KI konnte keine gute Möglichkeit finden, diese Notizen zu organisieren.", + "apply": "Anwenden", + "applying": "Wird angewendet...", + "success": "{count} Notizen erfolgreich verschoben", + "error": "Fehler beim Erstellen des Organisationsplans", + "noNotesSelected": "Keine Notizen ausgewählt", + "applyFailed": "Fehler beim Anwenden des Organisationsplans", + "selectAllIn": "Alle Notizen in {notebook} auswählen", + "selectNote": "Notiz auswählen: {title}" }, "clarify": "Klarstellen", "clickToAddTag": "Klicken Sie, um diesen Tag hinzuzufügen", @@ -193,7 +201,7 @@ "languageDetected": "Sprache erkannt", "notebookSummary": { "regenerate": "Zusammenfassung neu generieren", - "regenerating": "Generiere Zusammenfassung neu..." + "regenerating": "Zusammenfassung wird neu generiert..." }, "original": "Original", "poweredByAI": "Powered by KI", diff --git a/keep-notes/locales/en.json b/keep-notes/locales/en.json index 3cb3bd4..5042ad2 100644 --- a/keep-notes/locales/en.json +++ b/keep-notes/locales/en.json @@ -291,34 +291,6 @@ "regenerating": "Regenerating summary..." } }, - "batchOrganization": { - "error": "Failed to create organization plan", - "noNotesSelected": "No notes selected", - "title": "Organize with AI", - "description": "AI will analyze your notes and suggest organizing them into notebooks.", - "analyzing": "Analyzing your notes...", - "notesToOrganize": "{count} notes to organize", - "selected": "{count} selected", - "noNotebooks": "No notebooks available. Create notebooks first to organize your notes.", - "noSuggestions": "AI could not find a good way to organize these notes.", - "confidence": "confidence", - "unorganized": "{count} notes couldn't be categorized and will stay in General Notes.", - "applying": "Applying...", - "apply": "Apply ({count})" - }, - "autoLabels": { - "error": "Failed to fetch label suggestions", - "noLabelsSelected": "No labels selected", - "created": "{count} labels created successfully", - "analyzing": "Analyzing your notes...", - "title": "New Label Suggestions", - "description": "I've detected recurring themes in \"{notebookName}\" ({totalNotes} notes). Create labels for them?", - "note": "note", - "notes": "notes", - "typeContent": "Type content to get label suggestions...", - "createNewLabel": "Create this new label and add it", - "new": "(new)" - }, "titleSuggestions": { "available": "Title suggestions", "title": "AI suggestions", diff --git a/keep-notes/locales/es.json b/keep-notes/locales/es.json index 8f47c78..abdf565 100644 --- a/keep-notes/locales/es.json +++ b/keep-notes/locales/es.json @@ -63,7 +63,13 @@ "tagsGenerationProvider": "Proveedor de generación de etiquetas", "title": "Configuración de IA", "updateFailed": "Error al actualizar la configuración de IA", - "updateSuccess": "Configuración de IA actualizada correctamente" + "updateSuccess": "Configuración de IA actualizada correctamente", + "bestValue": "Mejor relación calidad/precio", + "bestQuality": "Mejor calidad", + "providerOllamaOption": "🦙 Ollama (Local & Free)", + "providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)", + "providerCustomOption": "🔧 Custom OpenAI-Compatible", + "saved": "(Guardado)" }, "aiTest": { "description": "Prueba tus proveedores de IA para generación de etiquetas y embeddings de búsqueda semántica", @@ -153,35 +159,36 @@ "analyzing": "IA analizando...", "assistant": "Asistente IA", "autoLabels": { - "analyzing": "Analizando tus notas para sugerencias de etiquetas...", - "create": "Crear", - "createNewLabel": "Crear nueva etiqueta", - "created": "{count} labels created successfully", - "creating": "Creando etiquetas...", - "description": "I've detected recurring themes in \"{notebookName}\" ({totalNotes} notes). Create labels for them?", - "error": "Failed to fetch label suggestions", + "error": "Error al obtener sugerencias de etiquetas", + "noLabelsSelected": "No se seleccionaron etiquetas", + "created": "{count} etiquetas creadas exitosamente", + "analyzing": "Analizando tus notas...", + "title": "Nuevas sugerencias de etiquetas", + "description": "He detectado temas recurrentes en \"{notebookName}\" ({totalNotes} notas). ¿Crear etiquetas para ellos?", + "note": "nota", + "notes": "notas", + "typeContent": "Escribe contenido para obtener sugerencias de etiquetas...", + "createNewLabel": "Crear esta nueva etiqueta y agregarla", "new": "(nuevo)", - "noLabelsSelected": "No labels selected", - "note": "note", - "notes": "notes", - "title": "Sugerencias de etiquetas", - "typeContent": "Type content to get label suggestions...", - "typeForSuggestions": "Escribe para obtener sugerencias" + "create": "Crear", + "creating": "Creando etiquetas...", + "notesCount": "{count} notas", + "typeForSuggestions": "Escribe contenido para obtener sugerencias de etiquetas..." }, "batchOrganization": { - "analyzing": "Analyzing your notes...", - "apply": "Apply", - "applyFailed": "Error al aplicar", - "applying": "Applying...", - "description": "La IA analizará tus notas y sugerirá organizarlas en libretas.", - "error": "Error al organizar", - "noNotebooks": "No notebooks available. Create notebooks first to organize your notes.", - "noNotesSelected": "Sin notas seleccionadas", - "noSuggestions": "AI could not find a good way to organize these notes.", - "selectAllIn": "Seleccionar todo en", - "selectNote": "Seleccionar nota", - "success": "Organización completada", - "title": "Organizar con IA" + "title": "Organizar con IA", + "description": "La IA analizará tus notas y sugerirá organizarlas en cuadernos.", + "analyzing": "Analizando tus notas...", + "noNotebooks": "No hay cuadernos disponibles. Crea cuadernos primero para organizar tus notas.", + "noSuggestions": "La IA no pudo encontrar una buena manera de organizar estas notas.", + "apply": "Aplicar", + "applying": "Aplicando...", + "success": "{count} notas movidas exitosamente", + "error": "Error al crear el plan de organización", + "noNotesSelected": "No se seleccionaron notas", + "applyFailed": "Error al aplicar el plan de organización", + "selectAllIn": "Seleccionar todas las notas en {notebook}", + "selectNote": "Seleccionar nota: {title}" }, "clarify": "Aclarar", "clickToAddTag": "Haz clic para agregar esta etiqueta", diff --git a/keep-notes/locales/fa.json b/keep-notes/locales/fa.json index 68ad58b..388411c 100644 --- a/keep-notes/locales/fa.json +++ b/keep-notes/locales/fa.json @@ -63,7 +63,13 @@ "tagsGenerationProvider": "ارائه‌دهنده تولید برچسب", "title": "پیکربندی هوش مصنوعی", "updateFailed": "شکست در به‌روزرسانی تنظیمات هوش مصنوعی", - "updateSuccess": "تنظیمات هوش مصنوعی با موفقیت به‌روزرسانی شد" + "updateSuccess": "تنظیمات هوش مصنوعی با موفقیت به‌روزرسانی شد", + "bestValue": "بهترین ارزش", + "bestQuality": "بهترین کیفیت", + "providerOllamaOption": "🦙 Ollama (Local & Free)", + "providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)", + "providerCustomOption": "🔧 Custom OpenAI-Compatible", + "saved": "(ذخیره شد)" }, "aiTest": { "description": "تست ارائه‌دهندگان هوش مصنوعی برای تولید برچسب و تعبیه‌های جستجوی معنایی", @@ -153,34 +159,36 @@ "analyzing": "در حال تحلیل هوش مصنوعی...", "assistant": "دستیار هوش مصنوعی", "autoLabels": { - "analyzing": "در حال تحلیل یادداشت‌های شما برای پیشنهادات برچسب...", + "analyzing": "تحلیل یادداشت‌های شما...", "create": "ایجاد", - "createNewLabel": "Create this new label and add it", - "created": "{count} labels created successfully", + "createNewLabel": "ایجاد این برچسب جدید و اضافه کردن آن", + "created": "{count} برچسب با موفقیت ایجاد شد", "creating": "در حال ایجاد برچسب‌ها...", - "description": "I've detected recurring themes in \"{notebookName}\" ({totalNotes} notes). Create labels for them?", - "error": "Failed to fetch label suggestions", + "description": "من موضوعات تکراری در \"{notebookName}\" ({totalNotes} یادداشت) تشخیص دادم. برچسب برای آنها ایجاد شود؟", + "error": "دریافت پیشنهادهای برچسب ناموفق", "new": "(جدید)", - "noLabelsSelected": "No labels selected", - "note": "note", - "notes": "notes", - "title": "پیشنهادات برچسب", - "typeContent": "Type content to get label suggestions..." + "noLabelsSelected": "برچسبی انتخاب نشده", + "note": "یادداشت", + "notes": "یادداشت", + "title": "پیشنهادهای برچسب جدید", + "typeContent": "برای دریافت پیشنهاد برچسب محتوا وارد کنید...", + "notesCount": "{count} یادداشت", + "typeForSuggestions": "برای دریافت پیشنهاد برچسب محتوا وارد کنید..." }, "batchOrganization": { - "analyzing": "Analyzing your notes...", - "apply": "Apply", - "applyFailed": "Failed to apply organization plan", - "applying": "Applying...", - "description": "هوش مصنوعی یادداشت‌های شما را تحلیل کرده و پیشنهاد می‌کند آن‌ها را در دفترچه‌ها سازماندهی کنید.", - "error": "Failed to create organization plan", - "noNotebooks": "No notebooks available. Create notebooks first to organize your notes.", - "noNotesSelected": "No notes selected", - "noSuggestions": "AI could not find a good way to organize these notes.", - "selectAllIn": "Select all notes in {notebook}", - "selectNote": "Select note: {title}", - "success": "{count} notes moved successfully", - "title": "Organize with AI" + "analyzing": "تحلیل یادداشت‌های شما...", + "apply": "اعمال", + "applyFailed": "اعمال طرح سازماندهی ناموفق", + "applying": "در حال اعمال...", + "description": "هوش مصنوعی یادداشت‌های شما را تحلیل و پیشنهاد سازماندهی در دفترچه‌ها را می‌دهد.", + "error": "ایجاد طرح سازماندهی ناموفق", + "noNotebooks": "دفترچه‌ای موجود نیست. ابتدا دفترچه ایجاد کنید.", + "noNotesSelected": "یادداشتی انتخاب نشده", + "noSuggestions": "هوش مصنوعی نتوانست روش خوبی برای سازماندهی این یادداشت‌ها پیدا کند.", + "selectAllIn": "انتخاب تمام یادداشت‌ها در {notebook}", + "selectNote": "انتخاب یادداشت: {title}", + "success": "{count} یادداشت با موفقیت جابجا شد", + "title": "سازماندهی با هوش مصنوعی" }, "clarify": "شفاف‌سازی", "clickToAddTag": "برای افزودن این برچسب کلیک کنید", @@ -192,8 +200,8 @@ "improveStyle": "بهبود سبک", "languageDetected": "زبان شناسایی شد", "notebookSummary": { - "regenerate": "تولید مجدد خلاصه", - "regenerating": "در حال تولید مجدد خلاصه..." + "regenerate": "بازسازی خلاصه", + "regenerating": "در حال بازسازی خلاصه..." }, "original": "اصلی", "poweredByAI": "قدرت گرفته از هوش مصنوعی", diff --git a/keep-notes/locales/fr.json b/keep-notes/locales/fr.json index 3367732..5258f1d 100644 --- a/keep-notes/locales/fr.json +++ b/keep-notes/locales/fr.json @@ -45,16 +45,22 @@ "baseUrl": "URL de base", "commonEmbeddingModels": "Modèles d'embeddings courants pour API compatibles OpenAI", "commonModelsDescription": "Modèles courants pour API compatibles OpenAI", - "description": "Configurez les fournisseurs IA pour l'étiquetage auto et la recherche sémantique.", + "description": "Configurez les fournisseurs IA pour l'étiquetage auto et la recherche sémantique. Utilisez différents fournisseurs pour des performances optimales.", "embeddingsDescription": "Fournisseur IA pour la recherche sémantique. Recommandé : OpenAI (meilleure qualité).", "embeddingsProvider": "Fournisseur d'embeddings", "model": "Modèle", - "modelRecommendations": "gpt-4o-mini = Meilleur prix • gpt-4o = Meilleure qualité", + "modelRecommendations": "gpt-4o-mini = Meilleur rapport qualité/prix • gpt-4o = Meilleure qualité", "openAIKeyDescription": "Votre clé API OpenAI depuis platform.openai.com", "openTestPanel": "Ouvrir le panneau de test IA", "provider": "Fournisseur", "providerEmbeddingRequired": "AI_PROVIDER_EMBEDDING est requis", "providerTagsRequired": "AI_PROVIDER_TAGS est requis", + "providerOllamaOption": "🦙 Ollama (Local & Gratuit)", + "providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)", + "providerCustomOption": "🔧 Custom Compatible OpenAI", + "bestValue": "Meilleur rapport qualité/prix", + "bestQuality": "Meilleure qualité", + "saved": "(Enregistré)", "saveSettings": "Enregistrer les paramètres IA", "saving": "Enregistrement...", "selectEmbeddingModel": "Sélectionnez un modèle d'embedding installé localement", @@ -63,83 +69,77 @@ "tagsGenerationProvider": "Fournisseur de génération d'étiquettes", "title": "Configuration IA", "updateFailed": "Échec de la mise à jour des paramètres IA", - "updateSuccess": "Paramètres IA mis à jour avec succès", - "providerOllamaOption": "🦙 Ollama (Local & Gratuit)", - "providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)", - "providerCustomOption": "🔧 Custom Compatible OpenAI", - "bestValue": "Meilleur rapport qualité/prix", - "bestQuality": "Meilleure qualité", - "saved": "(Enregistré)" + "updateSuccess": "Paramètres IA mis à jour avec succès" }, "aiTest": { - "description": "Test your AI providers for tag generation and semantic search embeddings", - "embeddingDimensions": "Embedding Dimensions:", - "embeddingsTestDescription": "Test the AI provider responsible for semantic search embeddings", - "embeddingsTestTitle": "Embeddings Test", - "error": "Error:", - "first5Values": "First 5 values:", - "generatedTags": "Generated Tags:", - "howItWorksTitle": "How Testing Works", - "model": "Model:", - "provider": "Provider:", - "responseTime": "Response time: {time}ms", - "runTest": "Run Test", - "tagsTestDescription": "Test the AI provider responsible for automatic tag suggestions", - "tagsTestTitle": "Tags Generation Test", - "testError": "Test Error: {error}", - "testFailed": "Test Failed", - "testPassed": "Test Passed", - "testing": "Testing...", - "tipDescription": "Use the AI Test Panel to diagnose configuration issues before testing.", - "tipTitle": "Tip:", - "title": "AI Provider Testing", - "vectorDimensions": "vector dimensions" + "description": "Testez vos fournisseurs IA pour la génération d'étiquettes et les embeddings de recherche sémantique", + "embeddingDimensions": "Dimensions de l'embedding :", + "embeddingsTestDescription": "Testez le fournisseur IA responsable des embeddings de recherche sémantique", + "embeddingsTestTitle": "Test d'embeddings", + "error": "Erreur :", + "first5Values": "5 premières valeurs :", + "generatedTags": "Étiquettes générées :", + "howItWorksTitle": "Fonctionnement des tests", + "model": "Modèle :", + "provider": "Fournisseur :", + "responseTime": "Temps de réponse : {time}ms", + "runTest": "Lancer le test", + "tagsTestDescription": "Testez le fournisseur IA responsable des suggestions d'étiquettes automatiques", + "tagsTestTitle": "Test de génération d'étiquettes", + "testError": "Erreur de test : {error}", + "testFailed": "Test échoué", + "testPassed": "Test réussi", + "testing": "Test en cours...", + "tipDescription": "Utilisez le panneau de test IA pour diagnostiquer les problèmes de configuration avant de tester.", + "tipTitle": "Astuce :", + "title": "Test des fournisseurs IA", + "vectorDimensions": "dimensions vectorielles" }, - "aiTesting": "AI Testing", + "aiTesting": "Test IA", "security": { - "allowPublicRegistration": "Allow Public Registration", - "allowPublicRegistrationDescription": "If disabled, new users can only be added by an Administrator via the User Management page.", - "description": "Manage access control and registration policies.", - "title": "Security Settings", - "updateFailed": "Failed to update security settings", - "updateSuccess": "Security Settings updated" + "allowPublicRegistration": "Autoriser l'inscription publique", + "allowPublicRegistrationDescription": "Si désactivé, les nouveaux utilisateurs ne peuvent être ajoutés que par un administrateur via la page de gestion des utilisateurs.", + "description": "Gérez le contrôle d'accès et les politiques d'inscription.", + "title": "Paramètres de sécurité", + "updateFailed": "Échec de la mise à jour des paramètres de sécurité", + "updateSuccess": "Paramètres de sécurité mis à jour" }, - "settings": "Admin Settings", + "settings": "Paramètres administrateur", "smtp": { - "description": "Configure email server for password resets.", - "forceSSL": "Force SSL/TLS (usually for port 465)", - "fromEmail": "From Email", - "host": "Host", - "ignoreCertErrors": "Ignore Certificate Errors (Self-hosted/Dev only)", - "password": "Password", + "description": "Configurez le serveur email pour les réinitialisations de mot de passe.", + "forceSSL": "Forcer SSL/TLS (généralement pour le port 465)", + "fromEmail": "Email d'expédition", + "host": "Hôte", + "ignoreCertErrors": "Ignorer les erreurs de certificat (Auto-hébergé/Dev uniquement)", + "password": "Mot de passe", "port": "Port", - "saveSettings": "Save SMTP Settings", - "sending": "Sending...", - "testEmail": "Test Email", - "testFailed": "Failed: {error}", - "testSuccess": "Test email sent successfully!", - "title": "SMTP Configuration", - "updateFailed": "Failed to update SMTP settings", - "updateSuccess": "SMTP Settings updated", - "username": "Username" + "saveSettings": "Enregistrer les paramètres SMTP", + "sending": "Envoi en cours...", + "testEmail": "Email de test", + "testFailed": "Échec : {error}", + "testSuccess": "Email de test envoyé avec succès !", + "title": "Configuration SMTP", + "updateFailed": "Échec de la mise à jour des paramètres SMTP", + "updateSuccess": "Paramètres SMTP mis à jour", + "username": "Nom d'utilisateur" }, - "title": "Admin Dashboard", - "userManagement": "User Management", + "title": "Tableau de bord Admin", + "userManagement": "Gestion des utilisateurs", "users": { - "addUser": "Add User", + "addUser": "Ajouter un utilisateur", "confirmDelete": "Êtes-vous sûr ? Cette action est irréversible.", - "createFailed": "Failed to create user", - "createSuccess": "User created successfully", - "createUser": "Create User", - "createUserDescription": "Add a new user to the system.", + "createFailed": "Échec de la création de l'utilisateur", + "createSuccess": "Utilisateur créé avec succès", + "createUser": "Créer un utilisateur", + "createUserDescription": "Ajouter un nouvel utilisateur au système.", "deleteFailed": "Échec de la suppression", "deleteSuccess": "Utilisateur supprimé", "demote": "Rétrograder en utilisateur", "email": "Email", - "name": "Name", - "password": "Password", + "name": "Nom", + "password": "Mot de passe", "promote": "Promouvoir en admin", - "role": "Role", + "role": "Rôle", "roleUpdateFailed": "Échec de la mise à jour du rôle", "roleUpdateSuccess": "Rôle de l'utilisateur mis à jour à {role}", "roles": { @@ -156,79 +156,80 @@ } }, "ai": { - "analyzing": "AI analyzing...", - "assistant": "AI Assistant", + "analyzing": "Analyse IA en cours...", + "assistant": "Assistant IA", "autoLabels": { "analyzing": "Analyse de vos notes pour les suggestions d'étiquettes...", "create": "Créer", "createNewLabel": "Créer cette nouvelle étiquette et l'ajouter", - "created": "{count} labels created successfully", + "created": "{count} étiquettes créées avec succès", "creating": "Création des étiquettes...", - "description": "I've detected recurring themes in \"{notebookName}\" ({totalNotes} notes). Create labels for them?", - "error": "Failed to fetch label suggestions", + "description": "J'ai détecté des thèmes récurrents dans \"{notebookName}\" ({totalNotes} notes). Créer des étiquettes pour eux ?", + "error": "Échec de la récupération des suggestions d'étiquettes", "new": "(nouveau)", - "noLabelsSelected": "No labels selected", + "noLabelsSelected": "Aucune étiquette sélectionnée", "note": "note", "notes": "notes", "title": "Suggestions d'étiquettes", - "typeContent": "Type content to get label suggestions...", - "typeForSuggestions": "Tapez du contenu pour obtenir des suggestions d'étiquettes..." + "typeContent": "Tapez du contenu pour obtenir des suggestions d'étiquettes...", + "typeForSuggestions": "Tapez du contenu pour obtenir des suggestions d'étiquettes...", + "notesCount": "{count} notes" }, "batchOrganization": { - "analyzing": "Analyzing your notes...", - "apply": "Apply", + "analyzing": "Analyse de vos notes...", + "apply": "Appliquer", "applyFailed": "Échec de l'application du plan d'organisation", - "applying": "Applying...", + "applying": "Application...", "description": "L'IA analysera vos notes et suggérera de les organiser dans des carnets.", "error": "Échec de la création du plan d'organisation", - "noNotebooks": "No notebooks available. Create notebooks first to organize your notes.", + "noNotebooks": "Aucun carnet disponible. Créez d'abord des carnets pour organiser vos notes.", "noNotesSelected": "Aucune note sélectionnée", - "noSuggestions": "AI could not find a good way to organize these notes.", + "noSuggestions": "L'IA n'a pas trouvé de bonne manière d'organiser ces notes.", "selectAllIn": "Sélectionner toutes les notes dans {notebook}", "selectNote": "Sélectionner la note : {title}", "success": "{count} notes déplacées avec succès", "title": "Organiser avec l'IA" }, - "clarify": "Clarify", - "clickToAddTag": "Click to add this tag", - "generateTitles": "Generate titles", - "generateTitlesTooltip": "Generate titles with AI", - "generating": "Generating...", - "generatingTitles": "Generating titles...", - "ignoreSuggestion": "Ignore this suggestion", - "improveStyle": "Improve style", - "languageDetected": "Language detected", + "clarify": "Clarifier", + "clickToAddTag": "Cliquer pour ajouter cette étiquette", + "generateTitles": "Générer des titres", + "generateTitlesTooltip": "Générer des titres avec l'IA", + "generating": "Génération...", + "generatingTitles": "Génération de titres...", + "ignoreSuggestion": "Ignorer cette suggestion", + "improveStyle": "Améliorer le style", + "languageDetected": "Langue détectée", "notebookSummary": { "regenerate": "Régénérer le résumé", "regenerating": "Régénération du résumé..." }, "original": "Original", - "poweredByAI": "Powered by AI", - "processing": "Processing...", - "reformulateText": "Reformulate text", - "reformulated": "Reformulated", - "reformulating": "Reformulating...", - "reformulationApplied": "Reformulated text applied!", - "reformulationComparison": "Reformulation Comparison", - "reformulationError": "Error during reformulation", - "reformulationFailed": "Failed to reformulate text", - "reformulationMaxWords": "Text must have maximum 500 words", - "reformulationMinWords": "Text must have at least 10 words (current: {count} words)", - "reformulationNoText": "Please select text or add content", - "reformulationSelectionTooShort": "Selection too short, using full content", - "shorten": "Shorten", - "tagAdded": "Tag \"{tag}\" added", - "titleApplied": "Title applied!", - "titleGenerateWithAI": "Generate titles with AI", - "titleGenerating": "Generating...", - "titleGenerationError": "Error generating titles", - "titleGenerationFailed": "Failed to generate titles", - "titleGenerationMinWords": "Content must have at least 10 words to generate titles (current: {count} words)", - "titlesGenerated": "💡 {count} titles generated!", - "transformError": "Error during transformation", - "transformMarkdown": "Transform to Markdown", - "transformSuccess": "Text transformed to Markdown successfully!", - "transforming": "Transforming..." + "poweredByAI": "Propulsé par l'IA", + "processing": "Traitement en cours...", + "reformulateText": "Reformuler le texte", + "reformulated": "Reformulé", + "reformulating": "Reformulation...", + "reformulationApplied": "Texte reformulé appliqué !", + "reformulationComparison": "Comparaison de reformulation", + "reformulationError": "Erreur lors de la reformulation", + "reformulationFailed": "Échec de la reformulation du texte", + "reformulationMaxWords": "Le texte doit avoir au maximum 500 mots", + "reformulationMinWords": "Le texte doit avoir au moins 10 mots (actuel : {count} mots)", + "reformulationNoText": "Veuillez sélectionner du texte ou ajouter du contenu", + "reformulationSelectionTooShort": "Sélection trop courte, utilisation du contenu complet", + "shorten": "Raccourcir", + "tagAdded": "Étiquette \"{tag}\" ajoutée", + "titleApplied": "Titre appliqué !", + "titleGenerateWithAI": "Générer des titres avec l'IA", + "titleGenerating": "Génération...", + "titleGenerationError": "Erreur lors de la génération des titres", + "titleGenerationFailed": "Échec de la génération des titres", + "titleGenerationMinWords": "Le contenu doit avoir au moins 10 mots pour générer des titres (actuel : {count} mots)", + "titlesGenerated": "💡 {count} titres générés !", + "transformError": "Erreur lors de la transformation", + "transformMarkdown": "Transformer en Markdown", + "transformSuccess": "Texte transformé en Markdown avec succès !", + "transforming": "Transformation..." }, "aiSettings": { "description": "Configurez vos fonctionnalités IA et préférences", @@ -282,7 +283,7 @@ "sending": "Envoi en cours...", "signIn": "Connexion", "signInToAccount": "Connectez-vous à votre compte", - "signOut": "Sign out", + "signOut": "Déconnexion", "signUp": "S'inscrire" }, "autoLabels": { @@ -387,51 +388,51 @@ }, "dataManagement": { "cleanup": { - "button": "Cleanup", - "description": "Remove labels and connections that reference deleted notes.", - "failed": "Error during cleanup", - "title": "Cleanup Orphaned Data" + "button": "Nettoyer", + "description": "Supprimer les étiquettes et connexions qui référencent des notes supprimées.", + "failed": "Erreur lors du nettoyage", + "title": "Nettoyer les données orphelines" }, "cleanupComplete": "Nettoyage terminé : {created} créés, {deleted} supprimés", "cleanupError": "Erreur lors du nettoyage", "dangerZone": "Zone de danger", "dangerZoneDescription": "Supprimer définitivement vos données", "delete": { - "button": "Delete All Notes", - "confirm": "Are you sure? This will permanently delete all your notes.", - "description": "Permanently delete all your notes. This action cannot be undone.", - "failed": "Failed to delete notes", - "success": "All notes deleted", - "title": "Delete All Notes" + "button": "Supprimer toutes les notes", + "confirm": "Êtes-vous sûr ? Cette action supprimera définitivement toutes vos notes.", + "description": "Supprimer définitivement toutes vos notes. Cette action est irréversible.", + "failed": "Échec de la suppression des notes", + "success": "Toutes les notes ont été supprimées", + "title": "Supprimer toutes les notes" }, "deleting": "Suppression...", "export": { - "button": "Export Notes", - "description": "Download all your notes as a JSON file. This includes all content, labels, and metadata.", - "failed": "Failed to export notes", - "success": "Notes exported successfully", - "title": "Export All Notes" + "button": "Exporter les notes", + "description": "Télécharger toutes vos notes au format JSON. Inclut tout le contenu, les étiquettes et les métadonnées.", + "failed": "Échec de l'exportation des notes", + "success": "Notes exportées avec succès", + "title": "Exporter toutes les notes" }, "exporting": "Exportation...", "import": { - "button": "Import Notes", - "description": "Upload a JSON file to import notes. This will add to your existing notes, not replace them.", - "failed": "Failed to import notes", - "success": "Imported {count} notes", - "title": "Import Notes" + "button": "Importer des notes", + "description": "Téléchargez un fichier JSON pour importer des notes. Les notes seront ajoutées aux existantes, pas remplacées.", + "failed": "Échec de l'importation des notes", + "success": "{count} notes importées", + "title": "Importer des notes" }, "importing": "Importation...", "indexing": { - "button": "Rebuild Index", - "description": "Regenerate embeddings for all notes to improve semantic search.", - "failed": "Error during indexing", - "success": "Indexing complete: {count} notes processed", - "title": "Rebuild Search Index" + "button": "Reconstruire l'index", + "description": "Régénérer les embeddings pour toutes les notes afin d'améliorer la recherche sémantique.", + "failed": "Erreur lors de l'indexation", + "success": "Indexation terminée : {count} notes traitées", + "title": "Reconstruire l'index de recherche" }, "indexingComplete": "Indexation terminée : {count} notes traitées", "indexingError": "Erreur lors de l'indexation", - "title": "Data Management", - "toolsDescription": "Tools to maintain your database health" + "title": "Gestion des données", + "toolsDescription": "Outils pour maintenir la santé de votre base de données" }, "demoMode": { "activated": "Mode Démo activé ! Memory Echo fonctionnera maintenant instantanément.", @@ -501,53 +502,53 @@ "title": "Paramètres généraux" }, "labels": { - "addLabel": "Add label", - "allLabels": "All Labels", - "changeColor": "Change Color", - "changeColorTooltip": "Change color", - "clearAll": "Clear all", - "confirmDelete": "Are you sure you want to delete this label?", + "addLabel": "Ajouter une étiquette", + "allLabels": "Toutes les étiquettes", + "changeColor": "Changer la couleur", + "changeColorTooltip": "Changer la couleur", + "clearAll": "Tout effacer", + "confirmDelete": "Êtes-vous sûr de vouloir supprimer cette étiquette ?", "count": "{count} étiquettes", - "createLabel": "Create label", - "delete": "Delete", - "deleteTooltip": "Delete label", - "editLabels": "Edit Labels", - "editLabelsDescription": "Create, edit colors, or delete labels.", - "filter": "Filter by Label", - "filterByLabel": "Filter by label", - "labelColor": "Label color", - "labelName": "Label name", - "loading": "Loading...", - "manage": "Manage Labels", - "manageLabels": "Manage labels", - "manageLabelsDescription": "Add or remove labels for this note. Click on a label to change its color.", - "manageTooltip": "Manage Labels", - "namePlaceholder": "Enter label name", - "newLabelPlaceholder": "Create new label", + "createLabel": "Créer une étiquette", + "delete": "Supprimer", + "deleteTooltip": "Supprimer l'étiquette", + "editLabels": "Modifier les étiquettes", + "editLabelsDescription": "Créer, modifier les couleurs ou supprimer des étiquettes.", + "filter": "Filtrer par étiquette", + "filterByLabel": "Filtrer par étiquette", + "labelColor": "Couleur de l'étiquette", + "labelName": "Nom de l'étiquette", + "loading": "Chargement...", + "manage": "Gérer les étiquettes", + "manageLabels": "Gérer les étiquettes", + "manageLabelsDescription": "Ajouter ou supprimer des étiquettes pour cette note. Cliquez sur une étiquette pour changer sa couleur.", + "manageTooltip": "Gérer les étiquettes", + "namePlaceholder": "Entrez le nom de l'étiquette", + "newLabelPlaceholder": "Créer une nouvelle étiquette", "noLabels": "Aucune étiquette", - "noLabelsFound": "No labels found.", - "notebookRequired": "⚠️ Labels are only available in notebooks. Move this note to a notebook first.", - "selectedLabels": "Selected Labels", - "showLess": "Show less", - "showMore": "Show more", - "tagAdded": "Tag \"{tag}\" added", - "title": "Labels" + "noLabelsFound": "Aucune étiquette trouvée.", + "notebookRequired": "⚠️ Les étiquettes sont uniquement disponibles dans les carnets. Déplacez cette note dans un carnet d'abord.", + "selectedLabels": "Étiquettes sélectionnées", + "showLess": "Voir moins", + "showMore": "Voir plus", + "tagAdded": "Étiquette \"{tag}\" ajoutée", + "title": "Étiquettes" }, "memoryEcho": { "clickToView": "Cliquer pour voir la note →", "comparison": { - "clickToView": "Click to view note", - "helpful": "Helpful", - "helpfulQuestion": "Is this comparison helpful?", - "highSimilarityInsight": "These notes deal with the same topic with a high degree of similarity. They could be merged or consolidated.", - "notHelpful": "Not Helpful", - "similarityInfo": "These notes are connected by {similarity}% similarity", - "title": "💡 Note Comparison", - "untitled": "Untitled" + "clickToView": "Cliquer pour voir la note", + "helpful": "Utile", + "helpfulQuestion": "Cette comparaison est-elle utile ?", + "highSimilarityInsight": "Ces notes traitent du même sujet avec un fort degré de similarité. Elles pourraient être fusionnées ou consolidées.", + "notHelpful": "Pas utile", + "similarityInfo": "Ces notes sont connectées par {similarity}% de similarité", + "title": "💡 Comparaison de notes", + "untitled": "Sans titre" }, - "connection": "connection", - "connections": "Connections", - "connectionsBadge": "{count} connection{plural}", + "connection": "connexion", + "connections": "Connexions", + "connectionsBadge": "{count} connexion{plural}", "title": "💡 J'ai remarqué quelque chose...", "description": "Connexions proactives entre vos notes", "dailyInsight": "Aperçu quotidien de vos notes", @@ -568,8 +569,44 @@ "sortSimilarity": "Similarité", "sortOldest": "Plus ancien" }, - "thanksFeedback": "Thanks for your feedback!", - "thanksFeedbackImproving": "Thanks! We'll use this to improve." + "thanksFeedback": "Merci pour votre retour !", + "thanksFeedbackImproving": "Merci ! Nous l'utiliserons pour nous améliorer.", + "fused": "Fusionné", + "editorSection": { + "title": "⚡ Notes connectées ({count})", + "loading": "Chargement...", + "view": "Voir", + "compare": "Comparer", + "merge": "Fusionner", + "compareAll": "Tout comparer", + "mergeAll": "Tout fusionner", + "close": "Fermer" + }, + "fusion": { + "title": "🔗 Fusion intelligente", + "mergeNotes": "Fusionner {count} note(s)", + "notesToMerge": "📝 Notes à fusionner", + "optionalPrompt": "💬 Prompt de fusion (optionnel)", + "promptPlaceholder": "Instructions optionnelles pour l'IA (ex. : 'Garder le style formel de la note 1')...", + "generateFusion": "Générer la fusion", + "generating": "Génération...", + "previewTitle": "📝 Aperçu de la note fusionnée", + "edit": "Modifier", + "modify": "Modifier", + "finishEditing": "Terminer l'édition", + "optionsTitle": "Options de fusion", + "archiveOriginals": "Archiver les notes originales", + "keepAllTags": "Conserver toutes les étiquettes", + "useLatestTitle": "Utiliser la note la plus récente comme titre", + "createBacklinks": "Créer un lien vers les notes originales", + "cancel": "Annuler", + "confirmFusion": "Confirmer la fusion", + "success": "Notes fusionnées avec succès !", + "error": "Échec de la fusion des notes", + "generateError": "Échec de la génération de la fusion", + "noContentReturned": "Aucun contenu de fusion retourné par l'API", + "unknownDate": "Date inconnue" + } }, "nav": { "accountSettings": "Paramètres du compte", @@ -607,33 +644,33 @@ "workspace": "Espace de travail" }, "notebook": { - "cancel": "Cancel", - "create": "Create Notebook", - "createDescription": "Start a new collection to organize your notes, ideas, and projects efficiently.", - "createNew": "Create New Notebook", - "creating": "Creating...", - "delete": "Delete Notebook", - "deleteConfirm": "Delete", - "deleteWarning": "Are you sure you want to delete this notebook? Notes will be moved to General Notes.", - "edit": "Edit Notebook", - "editDescription": "Change the name, icon, and color of your notebook.", - "generating": "Generating summary...", + "cancel": "Annuler", + "create": "Créer un carnet", + "createDescription": "Commencez une nouvelle collection pour organiser vos notes, idées et projets efficacement.", + "createNew": "Créer un nouveau carnet", + "creating": "Création...", + "delete": "Supprimer le carnet", + "deleteConfirm": "Supprimer", + "deleteWarning": "Êtes-vous sûr de vouloir supprimer ce carnet ? Les notes seront déplacées dans les Notes générales.", + "edit": "Modifier le carnet", + "editDescription": "Changer le nom, l'icône et la couleur de votre carnet.", + "generating": "Génération du résumé...", "labels": "Étiquettes :", - "name": "Notebook Name", + "name": "Nom du carnet", "noLabels": "Aucune étiquette", - "selectColor": "Color", - "selectIcon": "Icon", - "summary": "Notebook Summary", - "summaryDescription": "Generate an AI-powered summary of all notes in this notebook.", - "summaryError": "Error generating summary" + "selectColor": "Couleur", + "selectIcon": "Icône", + "summary": "Résumé du carnet", + "summaryDescription": "Générer un résumé alimenté par l'IA de toutes les notes de ce carnet.", + "summaryError": "Erreur lors de la génération du résumé" }, "notebookSuggestion": { - "description": "Cette note semble appartenir à ce notebook", + "description": "Cette note semble appartenir à ce carnet", "dismiss": "Rejeter", "dismissIn": "Rejeter (ferme dans {timeLeft}s)", "generalNotes": "Notes générales", "move": "Déplacer", - "moveToNotebook": "Déplacer vers un notebook", + "moveToNotebook": "Déplacer vers un carnet", "title": "Déplacer vers {icon} {name} ?" }, "notebooks": { @@ -643,106 +680,106 @@ "noNotebooks": "Aucun carnet encore" }, "notes": { - "add": "Add", - "addCollaborators": "Add collaborators", - "addImage": "Add image", - "addItem": "Add item", - "addLink": "Add link", - "addListItem": "+ List item", + "add": "Ajouter", + "addCollaborators": "Ajouter des collaborateurs", + "addImage": "Ajouter une image", + "addItem": "Ajouter un élément", + "addLink": "Ajouter un lien", + "addListItem": "+ Élément de liste", "addNote": "Ajouter une note", - "adding": "Adding...", - "aiAssistant": "AI Assistant", - "archive": "Archive", - "backgroundOptions": "Background options", - "changeColor": "Change color", - "changeSize": "Change size", + "adding": "Ajout...", + "aiAssistant": "Assistant IA", + "archive": "Archiver", + "backgroundOptions": "Options d'arrière-plan", + "changeColor": "Changer la couleur", + "changeSize": "Changer la taille", "clarifyFailed": "Échec de la clarification du texte", - "close": "Close", - "color": "Color", - "confirmDelete": "Are you sure you want to delete this note?", - "confirmLeaveShare": "Are you sure you want to leave this shared note?", - "contentOrMediaRequired": "Please enter some content or add a link/image", - "copy": "Copy", - "copyFailed": "Failed to copy note", - "copySuccess": "Note copied successfully!", - "createFirstNote": "Create your first note", + "close": "Fermer", + "color": "Couleur", + "confirmDelete": "Êtes-vous sûr de vouloir supprimer cette note ?", + "confirmLeaveShare": "Êtes-vous sûr de vouloir quitter cette note partagée ?", + "contentOrMediaRequired": "Veuillez entrer du contenu ou ajouter un lien/image", + "copy": "Copier", + "copyFailed": "Échec de la copie de la note", + "copySuccess": "Note copiée avec succès !", + "createFirstNote": "Créez votre première note", "date": "Date", - "delete": "Delete", + "delete": "Supprimer", "dragToReorder": "Glisser pour réorganiser", - "duplicate": "Duplicate", - "edit": "Edit Note", + "duplicate": "Dupliquer", + "edit": "Modifier la note", "emptyState": "Aucune note encore. Créez votre première note !", - "fileTooLarge": "File too large: {fileName}. Maximum size is {maxSize}.", + "fileTooLarge": "Fichier trop volumineux : {fileName}. Taille maximale : {maxSize}.", "improveFailed": "Échec de l'amélioration du texte", "inNotebook": "Dans le carnet", - "invalidDateTime": "Invalid date or time", - "invalidFileType": "Invalid file type: {fileName}. Only JPEG, PNG, GIF, and WebP allowed.", - "itemOrMediaRequired": "Please add at least one item or media", - "large": "Large", - "leaveShare": "Leave", - "linkAddFailed": "Failed to add link", - "linkAdded": "Link added", - "linkMetadataFailed": "Could not fetch link metadata", - "listItem": "List item", - "makeCopy": "Make a copy", + "invalidDateTime": "Date ou heure invalide", + "invalidFileType": "Type de fichier invalide : {fileName}. Seuls JPEG, PNG, GIF et WebP sont autorisés.", + "itemOrMediaRequired": "Veuillez ajouter au moins un élément ou média", + "large": "Grande", + "leaveShare": "Quitter", + "linkAddFailed": "Échec de l'ajout du lien", + "linkAdded": "Lien ajouté", + "linkMetadataFailed": "Impossible de récupérer les métadonnées du lien", + "listItem": "Élément de liste", + "makeCopy": "Faire une copie", "markdown": "Markdown", "markdownMode": "Markdown", - "markdownOff": "Markdown OFF", - "markdownOn": "Markdown ON", - "markdownPlaceholder": "Take a note... (Markdown supported)", - "medium": "Medium", + "markdownOff": "Markdown DÉSACTIVÉ", + "markdownOn": "Markdown ACTIVÉ", + "markdownPlaceholder": "Prenez une note... (Markdown supporté)", + "medium": "Moyenne", "more": "Plus d'options", - "moreOptions": "More options", + "moreOptions": "Plus d'options", "moveFailed": "Échec du déplacement de la note. Veuillez réessayer.", - "newChecklist": "New checklist", - "newNote": "New note", - "noContent": "No content", - "noNotes": "No notes", - "noNotesFound": "No notes found", - "noteCreateFailed": "Failed to create note", - "noteCreated": "Note created successfully", - "others": "Others", - "pin": "Pin", - "pinned": "Pinned", + "newChecklist": "Nouvelle checklist", + "newNote": "Nouvelle note", + "noContent": "Pas de contenu", + "noNotes": "Aucune note", + "noNotesFound": "Aucune note trouvée", + "noteCreateFailed": "Échec de la création de la note", + "noteCreated": "Note créée avec succès", + "others": "Autres", + "pin": "Épingler", + "pinned": "Épinglées", "pinnedNotes": "Notes épinglées", - "placeholder": "Take a note...", - "preview": "Preview", - "readOnly": "Read Only", + "placeholder": "Prenez une note...", + "preview": "Aperçu", + "readOnly": "Lecture seule", "recent": "Récent", - "redo": "Redo (Ctrl+Y)", + "redo": "Rétablir (Ctrl+Y)", "redoShortcut": "Rétablir (Ctrl+Y)", - "remindMe": "Remind me", - "reminderDateTimeRequired": "Please enter date and time", - "reminderMustBeFuture": "Reminder must be in the future", - "reminderPastError": "Reminder must be in the future", - "reminderRemoved": "Reminder removed", - "reminderSet": "Reminder set for {datetime}", + "remindMe": "Me rappeler", + "reminderDateTimeRequired": "Veuillez entrer la date et l'heure", + "reminderMustBeFuture": "Le rappel doit être dans le futur", + "reminderPastError": "Le rappel doit être dans le futur", + "reminderRemoved": "Rappel supprimé", + "reminderSet": "Rappel défini pour {datetime}", "remove": "Supprimer", - "saving": "Saving...", - "setReminder": "Set reminder", - "setReminderButton": "Set Reminder", - "share": "Share", - "shareWithCollaborators": "Share with collaborators", - "sharedBy": "Shared by", - "sharedReadOnly": "This note is shared with you in read-only mode", + "saving": "Enregistrement...", + "setReminder": "Définir un rappel", + "setReminderButton": "Définir un rappel", + "share": "Partager", + "shareWithCollaborators": "Partager avec les collaborateurs", + "sharedBy": "Partagé par", + "sharedReadOnly": "Cette note est partagée avec vous en lecture seule", "shortenFailed": "Échec du raccourcissement du texte", - "showCollaborators": "Show collaborators", - "size": "Size", - "small": "Small", - "takeNote": "Take a note...", - "takeNoteMarkdown": "Take a note... (Markdown supported)", - "time": "Time", + "showCollaborators": "Voir les collaborateurs", + "size": "Taille", + "small": "Petite", + "takeNote": "Prenez une note...", + "takeNoteMarkdown": "Prenez une note... (Markdown supporté)", + "time": "Heure", "title": "Notes", - "titlePlaceholder": "Title", + "titlePlaceholder": "Titre", "transformFailed": "Échec de la transformation du texte", - "unarchive": "Unarchive", - "undo": "Undo (Ctrl+Z)", + "unarchive": "Désarchiver", + "undo": "Annuler (Ctrl+Z)", "undoShortcut": "Annuler (Ctrl+Z)", - "unpin": "Unpin", + "unpin": "Désépingler", "unpinned": "Désépinglées", - "untitled": "Untitled", + "untitled": "Sans titre", "uploadFailed": "Échec du téléchargement", - "view": "View Note" + "view": "Voir la note" }, "pagination": { "next": "→", @@ -842,37 +879,37 @@ "searching": "Recherche en cours..." }, "settings": { - "about": "About", - "account": "Account", - "appearance": "Appearance", - "cleanTags": "Clean Orphan Tags", - "cleanTagsDescription": "Remove tags that are no longer used by any notes", - "description": "Manage your settings and preferences", - "language": "Language", + "about": "À propos", + "account": "Compte", + "appearance": "Apparence", + "cleanTags": "Nettoyer les étiquettes orphelines", + "cleanTagsDescription": "Supprimer les étiquettes qui ne sont plus utilisées par aucune note", + "description": "Gérez vos paramètres et préférences", + "language": "Langue", "languageAuto": "Langue définie sur Auto", "maintenance": "Maintenance", - "maintenanceDescription": "Tools to maintain your database health", + "maintenanceDescription": "Outils pour maintenir la santé de votre base de données", "notifications": "Notifications", - "privacy": "Privacy", + "privacy": "Confidentialité", "profile": "Profil", "searchNoResults": "Aucun paramètre trouvé", - "security": "Security", - "selectLanguage": "Select language", - "semanticIndexing": "Semantic Indexing", - "semanticIndexingDescription": "Generate vectors for all notes to enable intent-based search", - "settingsError": "Error saving settings", - "settingsSaved": "Settings saved", - "theme": "Theme", - "themeDark": "Dark", - "themeLight": "Light", - "themeSystem": "System", - "title": "Settings", + "security": "Sécurité", + "selectLanguage": "Sélectionner une langue", + "semanticIndexing": "Indexation sémantique", + "semanticIndexingDescription": "Générer des vecteurs pour toutes les notes afin de permettre la recherche par intention", + "settingsError": "Erreur lors de la sauvegarde des paramètres", + "settingsSaved": "Paramètres enregistrés", + "theme": "Thème", + "themeDark": "Sombre", + "themeLight": "Clair", + "themeSystem": "Système", + "title": "Paramètres", "version": "Version" }, "sidebar": { "archive": "Archives", - "editLabels": "Modifier les libellés", - "labels": "Libellés", + "editLabels": "Modifier les étiquettes", + "labels": "Étiquettes", "notes": "Notes", "reminders": "Rappels", "trash": "Corbeille" @@ -956,4 +993,4 @@ "expand": "Développer", "open": "Ouvrir" } -} \ No newline at end of file +} diff --git a/keep-notes/locales/hi.json b/keep-notes/locales/hi.json index 87a8e14..96a4a82 100644 --- a/keep-notes/locales/hi.json +++ b/keep-notes/locales/hi.json @@ -63,7 +63,13 @@ "tagsGenerationProvider": "टैग जनरेशन प्रदाता", "title": "AI कॉन्फ़िगरेशन", "updateFailed": "AI सेटिंग्स अपडेट करने में विफल", - "updateSuccess": "AI सेटिंग्स सफलतापूर्वक अपडेट की गईं" + "updateSuccess": "AI सेटिंग्स सफलतापूर्वक अपडेट की गईं", + "bestValue": "सर्वोत्तम मूल्य", + "bestQuality": "सर्वोत्तम गुणवत्ता", + "providerOllamaOption": "🦙 Ollama (Local & Free)", + "providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)", + "providerCustomOption": "🔧 Custom OpenAI-Compatible", + "saved": "(सहेजा गया)" }, "aiTest": { "description": "टैग जनरेशन और सिमेंटिक खोज एम्बेडिंग्स के लिए अपने AI प्रदाताओं का परीक्षण करें", @@ -153,34 +159,36 @@ "analyzing": "AI विश्लेषण जारी है...", "assistant": "AI सहायक", "autoLabels": { - "analyzing": "लेबल सुझावों के लिए आपके नोट्स का विश्लेषण किया जा रहा है...", + "analyzing": "आपके नोट्स का विश्लेषण हो रहा है...", "create": "बनाएं", - "createNewLabel": "Create this new label and add it", - "created": "{count} labels created successfully", + "createNewLabel": "यह नया लेबल बनाएं और जोड़ें", + "created": "{count} लेबल सफलतापूर्वक बनाए गए", "creating": "लेबल बनाए जा रहे हैं...", - "description": "I've detected recurring themes in \"{notebookName}\" ({totalNotes} notes). Create labels for them?", - "error": "Failed to fetch label suggestions", + "description": "मैंने \"{notebookName}\" ({totalNotes} नोट्स) में बार-बार आने वाले विषयों का पता लगाया। उनके लिए लेबल बनाएं?", + "error": "लेबल सुझाव प्राप्त करने में विफल", "new": "(नया)", - "noLabelsSelected": "No labels selected", - "note": "note", - "notes": "notes", - "title": "लेबल सुझाव", - "typeContent": "Type content to get label suggestions..." + "noLabelsSelected": "कोई लेबल चयनित नहीं", + "note": "नोट", + "notes": "नोट्स", + "title": "नए लेबल सुझाव", + "typeContent": "लेबल सुझाव प्राप्त करने के लिए सामग्री लिखें...", + "notesCount": "{count} नोट्स", + "typeForSuggestions": "लेबल सुझाव प्राप्त करने के लिए सामग्री लिखें..." }, "batchOrganization": { - "analyzing": "Analyzing your notes...", - "apply": "Apply", - "applyFailed": "Failed to apply organization plan", - "applying": "Applying...", - "description": "AI आपके नोट्स का विश्लेषण करेगा और उन्हें नोटबुक में व्यवस्थित करने का सुझाव देगा।", - "error": "Failed to create organization plan", - "noNotebooks": "No notebooks available. Create notebooks first to organize your notes.", - "noNotesSelected": "No notes selected", - "noSuggestions": "AI could not find a good way to organize these notes.", - "selectAllIn": "Select all notes in {notebook}", - "selectNote": "Select note: {title}", - "success": "{count} notes moved successfully", - "title": "Organize with AI" + "analyzing": "आपके नोट्स का विश्लेषण हो रहा है...", + "apply": "लागू करें", + "applyFailed": "संगठन योजना लागू करने में विफल", + "applying": "लागू हो रहा है...", + "description": "AI आपके नोट्स का विश्लेषण करेगा और नोटबुक में व्यवस्थित करने का सुझाव देगा।", + "error": "संगठन योजना बनाने में विफल", + "noNotebooks": "कोई नोटबुक उपलब्ध नहीं। पहले नोटबुक बनाएं।", + "noNotesSelected": "कोई नोट चयनित नहीं", + "noSuggestions": "AI इन नोट्स को व्यवस्थित करने का अच्छा तरीका नहीं ढूंढ सका।", + "selectAllIn": "{notebook} में सभी नोट्स चुनें", + "selectNote": "नोट चुनें: {title}", + "success": "{count} नोट्स सफलतापूर्वक स्थानांतरित", + "title": "AI से व्यवस्थित करें" }, "clarify": "स्पष्ट करें", "clickToAddTag": "इस टैग को जोड़ने के लिए क्लिक करें", diff --git a/keep-notes/locales/it.json b/keep-notes/locales/it.json index 2af6e89..eedfa49 100644 --- a/keep-notes/locales/it.json +++ b/keep-notes/locales/it.json @@ -63,7 +63,13 @@ "tagsGenerationProvider": "Tags Generation Provider", "title": "AI Configuration", "updateFailed": "Failed to update AI settings", - "updateSuccess": "AI Settings updated successfully" + "updateSuccess": "AI Settings updated successfully", + "bestValue": "Miglior rapporto qualità/prezzo", + "bestQuality": "Miglior qualità", + "providerOllamaOption": "🦙 Ollama (Local & Free)", + "providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)", + "providerCustomOption": "🔧 Custom OpenAI-Compatible", + "saved": "(Salvato)" }, "aiTest": { "description": "Test your AI providers for tag generation and semantic search embeddings", @@ -156,9 +162,9 @@ "analyzing": "Analisi delle tue note per suggerimenti etichette...", "create": "Crea", "createNewLabel": "Crea nuova etichetta", - "created": "{count} labels created successfully", + "created": "{count} etichette create con successo", "creating": "Creazione etichette...", - "description": "I've detected recurring themes in \"{notebookName}\" ({totalNotes} notes). Create labels for them?", + "description": "Ho rilevato temi ricorrenti in \"{notebookName}\" ({totalNotes} note). Creare etichette per essi?", "error": "Failed to fetch label suggestions", "new": "(nuovo)", "noLabelsSelected": "No labels selected", @@ -580,14 +586,14 @@ "memoryEcho": { "clickToView": "Clicca per visualizzare", "comparison": { - "clickToView": "Click to view note", - "helpful": "Helpful", - "helpfulQuestion": "Is this comparison helpful?", - "highSimilarityInsight": "These notes deal with the same topic with a high degree of similarity. They could be merged or consolidated.", - "notHelpful": "Not Helpful", - "similarityInfo": "These notes are connected by {similarity}% similarity", - "title": "💡 Note Comparison", - "untitled": "Untitled" + "title": "💡 Confronto note", + "similarityInfo": "Queste note sono collegate da {similarity}% di similarità", + "highSimilarityInsight": "Queste note trattano lo stesso argomento con un alto grado di similarità. Potrebbero essere fuse o consolidate.", + "untitled": "Senza titolo", + "clickToView": "Clicca per visualizzare la nota", + "helpfulQuestion": "Questo confronto è utile?", + "helpful": "Utile", + "notHelpful": "Non utile" }, "connection": "connection", "connections": "Connections", @@ -596,40 +602,40 @@ "description": "Proactive connections between your notes", "dismiss": "Dismiss for now", "editorSection": { - "close": "Chiudi", - "compare": "Compare", - "compareAll": "Compare all", - "loading": "Loading...", - "merge": "Merge", - "mergeAll": "Merge all", - "title": "⚡ Connected Notes ({count})", - "view": "View" + "title": "⚡ Note connesse ({count})", + "loading": "Caricamento...", + "view": "Visualizza", + "compare": "Confronta", + "merge": "Unisci", + "compareAll": "Confronta tutto", + "mergeAll": "Unisci tutto", + "close": "Chiudi" }, - "fused": "Fused", + "fused": "Fuso", "fusion": { - "archiveOriginals": "Archive original notes", - "cancel": "Cancel", - "confirmFusion": "Confirm fusion", - "createBacklinks": "Create backlink to original notes", - "edit": "Edit", - "error": "Failed to merge notes", - "finishEditing": "Finish editing", - "generateError": "Failed to generate fusion", - "generateFusion": "Generate the fusion", - "generating": "Generating...", - "keepAllTags": "Keep all tags", - "mergeNotes": "Merge {count} note(s)", - "modify": "Modify", - "noContentReturned": "No fusion content returned from API", - "notesToMerge": "📝 Notes to merge", - "optionalPrompt": "💬 Fusion prompt (optional)", - "optionsTitle": "Fusion options", - "previewTitle": "📝 Preview of merged note", - "promptPlaceholder": "Optional instructions for AI (e.g., 'Keep the formal style of note 1')...", - "success": "Notes merged successfully!", - "title": "🔗 Intelligent Fusion", - "unknownDate": "Unknown date", - "useLatestTitle": "Use latest note as title" + "title": "🔗 Fusione intelligente", + "mergeNotes": "Unisci {count} nota/e", + "notesToMerge": "📝 Note da unire", + "optionalPrompt": "💬 Prompt di fusione (opzionale)", + "promptPlaceholder": "Istruzioni opzionali per l'IA (es. 'Mantieni lo stile formale della nota 1')...", + "generateFusion": "Genera la fusione", + "generating": "Generazione...", + "previewTitle": "📝 Anteprima della nota unita", + "edit": "Modifica", + "modify": "Modifica", + "finishEditing": "Termina modifica", + "optionsTitle": "Opzioni di fusione", + "archiveOriginals": "Archivia note originali", + "keepAllTags": "Mantieni tutti i tag", + "useLatestTitle": "Usa la nota più recente come titolo", + "createBacklinks": "Crea collegamento alle note originali", + "cancel": "Annulla", + "confirmFusion": "Conferma fusione", + "success": "Note unite con successo!", + "error": "Impossibile unire le note", + "generateError": "Impossibile generare la fusione", + "noContentReturned": "Nessun contenuto di fusione restituito dall'API", + "unknownDate": "Data sconosciuta" }, "helpful": "Helpful", "insightReady": "Your insight is ready!", diff --git a/keep-notes/locales/ja.json b/keep-notes/locales/ja.json index f5cc8be..ba57387 100644 --- a/keep-notes/locales/ja.json +++ b/keep-notes/locales/ja.json @@ -63,7 +63,13 @@ "tagsGenerationProvider": "タグ生成プロバイダー", "title": "AI設定", "updateFailed": "AI設定の更新に失敗しました", - "updateSuccess": "AI設定が正常に更新されました" + "updateSuccess": "AI設定が正常に更新されました", + "bestValue": "最もコスパが良い", + "bestQuality": "最高品質", + "providerOllamaOption": "🦙 Ollama (Local & Free)", + "providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)", + "providerCustomOption": "🔧 Custom OpenAI-Compatible", + "saved": "(保存済み)" }, "aiTest": { "description": "タグ生成とセマンティック検索埋め込みのAIプロバイダーをテストします", @@ -153,34 +159,36 @@ "analyzing": "AI分析中...", "assistant": "AIアシスタント", "autoLabels": { - "analyzing": "ラベル提案のためにノートを分析中...", + "analyzing": "ノートを分析中...", "create": "作成", - "createNewLabel": "Create this new label and add it", - "created": "{count} labels created successfully", + "createNewLabel": "この新しいラベルを作成して追加", + "created": "{count} 個のラベルを作成しました", "creating": "ラベルを作成中...", - "description": "I've detected recurring themes in \"{notebookName}\" ({totalNotes} notes). Create labels for them?", - "error": "Failed to fetch label suggestions", - "new": "(新規)", - "noLabelsSelected": "No labels selected", - "note": "note", - "notes": "notes", - "title": "ラベルの提案", - "typeContent": "Type content to get label suggestions..." + "description": "\"{notebookName}\"({totalNotes} 件のノート)で繰り返し出てくるテーマを検出しました。ラベルを作成しますか?", + "error": "ラベル候補の取得に失敗しました", + "new": "(新規)", + "noLabelsSelected": "ラベルが選択されていません", + "note": "件", + "notes": "件", + "title": "新しいラベル候補", + "typeContent": "ラベル候補を取得するにはコンテンツを入力...", + "notesCount": "{count} 件", + "typeForSuggestions": "ラベル候補を取得するにはコンテンツを入力..." }, "batchOrganization": { - "analyzing": "Analyzing your notes...", - "apply": "Apply", - "applyFailed": "Failed to apply organization plan", - "applying": "Applying...", - "description": "AIがノートを分析し、ノートブックに整理することを提案します。", - "error": "Failed to create organization plan", - "noNotebooks": "No notebooks available. Create notebooks first to organize your notes.", - "noNotesSelected": "No notes selected", - "noSuggestions": "AI could not find a good way to organize these notes.", - "selectAllIn": "Select all notes in {notebook}", - "selectNote": "Select note: {title}", - "success": "{count} notes moved successfully", - "title": "Organize with AI" + "analyzing": "ノートを分析中...", + "apply": "適用", + "applyFailed": "整理プランの適用に失敗しました", + "applying": "適用中...", + "description": "AI がノートを分析し、ノートブックに整理する方法を提案します。", + "error": "整理プランの作成に失敗しました", + "noNotebooks": "利用可能なノートブックがありません。先にノートブックを作成してください。", + "noNotesSelected": "ノートが選択されていません", + "noSuggestions": "AI はこれらのノートを整理する良い方法を見つけられませんでした。", + "selectAllIn": "{notebook} のすべてのノートを選択", + "selectNote": "ノートを選択:{title}", + "success": "{count} 件のノートを移動しました", + "title": "AI で整理" }, "clarify": "明確にする", "clickToAddTag": "クリックしてこのタグを追加", @@ -192,8 +200,8 @@ "improveStyle": "スタイルを改善", "languageDetected": "検出された言語", "notebookSummary": { - "regenerate": "概要を再生成", - "regenerating": "概要を再生成中..." + "regenerate": "要約を再生成", + "regenerating": "要約を再生成中..." }, "original": "元のテキスト", "poweredByAI": "AI powered", diff --git a/keep-notes/locales/ko.json b/keep-notes/locales/ko.json index 599ec54..08a519e 100644 --- a/keep-notes/locales/ko.json +++ b/keep-notes/locales/ko.json @@ -63,7 +63,13 @@ "tagsGenerationProvider": "태그 생성 공급자", "title": "AI 구성", "updateFailed": "AI 설정 업데이트 실패", - "updateSuccess": "AI 설정이 성공적으로 업데이트되었습니다" + "updateSuccess": "AI 설정이 성공적으로 업데이트되었습니다", + "bestValue": "최고 가성비", + "bestQuality": "최고 품질", + "providerOllamaOption": "🦙 Ollama (Local & Free)", + "providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)", + "providerCustomOption": "🔧 Custom OpenAI-Compatible", + "saved": "(저장됨)" }, "aiTest": { "description": "태그 생성 및 의미 검색 임베딩을 위한 AI 공급자 테스트", @@ -153,34 +159,36 @@ "analyzing": "AI 분석 중...", "assistant": "AI 도우미", "autoLabels": { - "analyzing": "라벨 제안을 위해 메모 분석 중...", - "create": "생성", - "createNewLabel": "Create this new label and add it", - "created": "{count} labels created successfully", - "creating": "라벨 생성 중...", - "description": "I've detected recurring themes in \"{notebookName}\" ({totalNotes} notes). Create labels for them?", - "error": "Failed to fetch label suggestions", - "new": "(새로운)", - "noLabelsSelected": "No labels selected", - "note": "note", - "notes": "notes", - "title": "라벨 제안", - "typeContent": "Type content to get label suggestions..." + "error": "라벨 제안을 가져오지 못했습니다", + "noLabelsSelected": "선택된 라벨이 없습니다", + "created": "{count}개의 라벨이 생성되었습니다", + "analyzing": "노트를 분석 중...", + "title": "새 라벨 제안", + "description": "\"{notebookName}\"({totalNotes}개의 노트)에서 반복되는 테마를 감지했습니다. 라벨을 만들까요?", + "note": "개", + "notes": "개", + "typeContent": "라벨 제안을 받으려면 내용을 입력하세요...", + "createNewLabel": "이 새 라벨을 만들고 추가", + "new": "(새)", + "create": "만들기", + "creating": "라벨 만드는 중...", + "notesCount": "{count}개", + "typeForSuggestions": "라벨 제안을 받으려면 내용을 입력하세요..." }, "batchOrganization": { - "analyzing": "Analyzing your notes...", - "apply": "Apply", - "applyFailed": "Failed to apply organization plan", - "applying": "Applying...", - "description": "AI가 메모를 분석하여 노트북으로 정리할 것을 제안합니다.", - "error": "Failed to create organization plan", - "noNotebooks": "No notebooks available. Create notebooks first to organize your notes.", - "noNotesSelected": "No notes selected", - "noSuggestions": "AI could not find a good way to organize these notes.", - "selectAllIn": "Select all notes in {notebook}", - "selectNote": "Select note: {title}", - "success": "{count} notes moved successfully", - "title": "Organize with AI" + "title": "AI로 정리", + "description": "AI가 노트를 분석하고 노트북으로 정리할 방법을 제안합니다.", + "analyzing": "노트를 분석 중...", + "noNotebooks": "사용 가능한 노트북이 없습니다. 먼저 노트북을 만드세요.", + "noSuggestions": "AI가 이 노트들을 정리할 좋은 방법을 찾지 못했습니다.", + "apply": "적용", + "applying": "적용 중...", + "success": "{count}개의 노트를 성공적으로 이동했습니다", + "error": "정리 계획 생성 실패", + "noNotesSelected": "선택된 노트가 없습니다", + "applyFailed": "정리 계획 적용 실패", + "selectAllIn": "{notebook}의 모든 노트 선택", + "selectNote": "노트 선택: {title}" }, "clarify": "명확히 하기", "clickToAddTag": "클릭하여 이 태그 추가", diff --git a/keep-notes/locales/nl.json b/keep-notes/locales/nl.json index e1dfce9..9d33127 100644 --- a/keep-notes/locales/nl.json +++ b/keep-notes/locales/nl.json @@ -63,7 +63,13 @@ "tagsGenerationProvider": "Tags Generation Provider", "title": "AI Configuration", "updateFailed": "Failed to update AI settings", - "updateSuccess": "AI Settings updated successfully" + "updateSuccess": "AI Settings updated successfully", + "bestValue": "Beste prijs-kwaliteit", + "bestQuality": "Beste kwaliteit", + "providerOllamaOption": "🦙 Ollama (Local & Free)", + "providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)", + "providerCustomOption": "🔧 Custom OpenAI-Compatible", + "saved": "(Opgeslagen)" }, "aiTest": { "description": "Test your AI providers for tag generation and semantic search embeddings", @@ -156,9 +162,9 @@ "analyzing": "Uw notities analyseren voor labelsuggesties...", "create": "Maken", "createNewLabel": "Nieuw label maken", - "created": "{count} labels created successfully", + "created": "{count} labels succesvol aangemaakt", "creating": "Labels maken...", - "description": "I've detected recurring themes in \"{notebookName}\" ({totalNotes} notes). Create labels for them?", + "description": "Ik heb terugkerende themas gedetecteerd in \"{notebookName}\" ({totalNotes} notities). Labels hiervoor maken?", "error": "Failed to fetch label suggestions", "new": "(nieuw)", "noLabelsSelected": "No labels selected", @@ -580,14 +586,14 @@ "memoryEcho": { "clickToView": "Klik om te bekijken", "comparison": { - "clickToView": "Click to view note", - "helpful": "Helpful", - "helpfulQuestion": "Is this comparison helpful?", - "highSimilarityInsight": "These notes deal with the same topic with a high degree of similarity. They could be merged or consolidated.", - "notHelpful": "Not Helpful", - "similarityInfo": "These notes are connected by {similarity}% similarity", - "title": "💡 Note Comparison", - "untitled": "Untitled" + "title": "💡 Notitie vergelijking", + "similarityInfo": "Deze notities zijn verbonden door {similarity}% overeenkomst", + "highSimilarityInsight": "Deze notities gaan over hetzelfde onderwerp met een hoge mate van overeenkomst. Ze kunnen worden samengevoegd.", + "untitled": "Naamloos", + "clickToView": "Klik om notitie te bekijken", + "helpfulQuestion": "Is deze vergelijking nuttig?", + "helpful": "Nuttig", + "notHelpful": "Niet nuttig" }, "connection": "connection", "connections": "Connections", @@ -596,40 +602,40 @@ "description": "Proactive connections between your notes", "dismiss": "Dismiss for now", "editorSection": { - "close": "Sluiten", - "compare": "Compare", - "compareAll": "Compare all", - "loading": "Loading...", - "merge": "Merge", - "mergeAll": "Merge all", - "title": "⚡ Connected Notes ({count})", - "view": "View" + "title": "⚡ Verbinde notities ({count})", + "loading": "Laden...", + "view": "Bekijken", + "compare": "Vergelijken", + "merge": "Samenvoegen", + "compareAll": "Alles vergelijken", + "mergeAll": "Alles samenvoegen", + "close": "Sluiten" }, - "fused": "Fused", + "fused": "Samengevoegd", "fusion": { - "archiveOriginals": "Archive original notes", - "cancel": "Cancel", - "confirmFusion": "Confirm fusion", - "createBacklinks": "Create backlink to original notes", - "edit": "Edit", - "error": "Failed to merge notes", - "finishEditing": "Finish editing", - "generateError": "Failed to generate fusion", - "generateFusion": "Generate the fusion", - "generating": "Generating...", - "keepAllTags": "Keep all tags", - "mergeNotes": "Merge {count} note(s)", - "modify": "Modify", - "noContentReturned": "No fusion content returned from API", - "notesToMerge": "📝 Notes to merge", - "optionalPrompt": "💬 Fusion prompt (optional)", - "optionsTitle": "Fusion options", - "previewTitle": "📝 Preview of merged note", - "promptPlaceholder": "Optional instructions for AI (e.g., 'Keep the formal style of note 1')...", - "success": "Notes merged successfully!", - "title": "🔗 Intelligent Fusion", - "unknownDate": "Unknown date", - "useLatestTitle": "Use latest note as title" + "title": "🔗 Intelligente fusie", + "mergeNotes": "Voeg {count} notitie(s) samen", + "notesToMerge": "📝 Te samenvoegen notities", + "optionalPrompt": "💬 Fusie prompt (optioneel)", + "promptPlaceholder": "Optionele instructies voor AI (bijv. 'Behoud de formele stijl van notitie 1')...", + "generateFusion": "Genereer fusie", + "generating": "Genereren...", + "previewTitle": "📝 Voorbeeld van samengevoegde notitie", + "edit": "Bewerken", + "modify": "Wijzigen", + "finishEditing": "Bewerken voltooid", + "optionsTitle": "Fusie-opties", + "archiveOriginals": "Archiveer originele notities", + "keepAllTags": "Bewaar alle tags", + "useLatestTitle": "Gebruik meest recente notitie als titel", + "createBacklinks": "Maak terugverwijzing naar originele notities", + "cancel": "Annuleren", + "confirmFusion": "Bevestig fusie", + "success": "Notities succesvol samengevoegd!", + "error": "Kan notities niet samenvoegen", + "generateError": "Kan fusie niet genereren", + "noContentReturned": "Geen fusie-inhoud ontvangen van API", + "unknownDate": "Onbekende datum" }, "helpful": "Helpful", "insightReady": "Your insight is ready!", diff --git a/keep-notes/locales/pl.json b/keep-notes/locales/pl.json index 097244e..e4dc674 100644 --- a/keep-notes/locales/pl.json +++ b/keep-notes/locales/pl.json @@ -63,7 +63,13 @@ "tagsGenerationProvider": "Tags Generation Provider", "title": "AI Configuration", "updateFailed": "Failed to update AI settings", - "updateSuccess": "AI Settings updated successfully" + "updateSuccess": "AI Settings updated successfully", + "bestValue": "Najlepszy stosunek jakości do ceny", + "bestQuality": "Najwyższa jakość", + "providerOllamaOption": "🦙 Ollama (Local & Free)", + "providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)", + "providerCustomOption": "🔧 Custom OpenAI-Compatible", + "saved": "(Zapisano)" }, "aiTest": { "description": "Test your AI providers for tag generation and semantic search embeddings", @@ -156,9 +162,9 @@ "analyzing": "Analizowanie Twoich notatek pod kątem sugestii etykiet...", "create": "Utwórz", "createNewLabel": "Utwórz nową etykietę", - "created": "{count} labels created successfully", + "created": "{count} etykiet utworzono pomyślnie", "creating": "Tworzenie etykiet...", - "description": "I've detected recurring themes in \"{notebookName}\" ({totalNotes} notes). Create labels for them?", + "description": "Wykryłem powtarzające się tematy w \"{notebookName}\" ({totalNotes} notatkach). Utworzyć dla nich etykiety?", "error": "Failed to fetch label suggestions", "new": "(nowa)", "noLabelsSelected": "No labels selected", @@ -602,14 +608,14 @@ "memoryEcho": { "clickToView": "Kliknij, aby wyświetlić", "comparison": { - "clickToView": "Click to view note", - "helpful": "Helpful", - "helpfulQuestion": "Is this comparison helpful?", - "highSimilarityInsight": "These notes deal with the same topic with a high degree of similarity. They could be merged or consolidated.", - "notHelpful": "Not Helpful", - "similarityInfo": "These notes are connected by {similarity}% similarity", - "title": "💡 Note Comparison", - "untitled": "Untitled" + "title": "💡 Porównanie notatek", + "similarityInfo": "Te notatki są połączone przez {similarity}% podobieństwa", + "highSimilarityInsight": "Te notatki dotyczą tego samego tematu z wysokim stopniem podobieństwa. Mogą zostać połączone.", + "untitled": "Bez tytułu", + "clickToView": "Kliknij aby wyświetlić notatkę", + "helpfulQuestion": "Czy to porównanie jest pomocne?", + "helpful": "Pomocne", + "notHelpful": "Nie pomocne" }, "connection": "connection", "connections": "Connections", @@ -618,40 +624,40 @@ "description": "Proactive connections between your notes", "dismiss": "Dismiss for now", "editorSection": { - "close": "Zamknij", - "compare": "Compare", - "compareAll": "Compare all", - "loading": "Loading...", - "merge": "Merge", - "mergeAll": "Merge all", - "title": "⚡ Connected Notes ({count})", - "view": "View" + "title": "⚡ Połączone notatki ({count})", + "loading": "Ładowanie...", + "view": "Wyświetl", + "compare": "Porównaj", + "merge": "Połącz", + "compareAll": "Porównaj wszystko", + "mergeAll": "Połącz wszystko", + "close": "Zamknij" }, - "fused": "Fused", + "fused": "Połączono", "fusion": { - "archiveOriginals": "Archive original notes", - "cancel": "Cancel", - "confirmFusion": "Confirm fusion", - "createBacklinks": "Create backlink to original notes", - "edit": "Edit", - "error": "Failed to merge notes", - "finishEditing": "Finish editing", - "generateError": "Failed to generate fusion", - "generateFusion": "Generate the fusion", - "generating": "Generating...", - "keepAllTags": "Keep all tags", - "mergeNotes": "Merge {count} note(s)", - "modify": "Modify", - "noContentReturned": "No fusion content returned from API", - "notesToMerge": "📝 Notes to merge", - "optionalPrompt": "💬 Fusion prompt (optional)", - "optionsTitle": "Fusion options", - "previewTitle": "📝 Preview of merged note", - "promptPlaceholder": "Optional instructions for AI (e.g., 'Keep the formal style of note 1')...", - "success": "Notes merged successfully!", - "title": "🔗 Intelligent Fusion", - "unknownDate": "Unknown date", - "useLatestTitle": "Use latest note as title" + "title": "🔗 Inteligentne fuzja", + "mergeNotes": "Połącz {count} notatkę/i", + "notesToMerge": "📝 Notatki do połączenia", + "optionalPrompt": "💬 Prompt fuzji (opcjonalny)", + "promptPlaceholder": "Opcjonalne instrukcje dla AI (np. 'Zachowaj formalny styl notatki 1')...", + "generateFusion": "Wygeneruj fuzję", + "generating": "Generowanie...", + "previewTitle": "📝 Podgląd połączonej notatki", + "edit": "Edytuj", + "modify": "Modyfikuj", + "finishEditing": "Zakończ edycję", + "optionsTitle": "Opcje fuzji", + "archiveOriginals": "Archiwizuj oryginalne notatki", + "keepAllTags": "Zachowaj wszystkie tagi", + "useLatestTitle": "Użyj najnowszej notatki jako tytułu", + "createBacklinks": "Utwórz link do oryginalnych notatek", + "cancel": "Anuluj", + "confirmFusion": "Potwierdź fuzję", + "success": "Notatki połączone pomyślnie!", + "error": "Nie udało się połączyć notatek", + "generateError": "Nie udało się wygenerować fuzji", + "noContentReturned": "Brak zawartości fuzji z API", + "unknownDate": "Nieznana data" }, "helpful": "Helpful", "insightReady": "Your insight is ready!", diff --git a/keep-notes/locales/pt.json b/keep-notes/locales/pt.json index e804084..d7b476c 100644 --- a/keep-notes/locales/pt.json +++ b/keep-notes/locales/pt.json @@ -63,7 +63,13 @@ "tagsGenerationProvider": "Tags Generation Provider", "title": "AI Configuration", "updateFailed": "Failed to update AI settings", - "updateSuccess": "AI Settings updated successfully" + "updateSuccess": "AI Settings updated successfully", + "bestValue": "Melhor custo-benefício", + "bestQuality": "Melhor qualidade", + "providerOllamaOption": "🦙 Ollama (Local & Free)", + "providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)", + "providerCustomOption": "🔧 Custom OpenAI-Compatible", + "saved": "(Salvo)" }, "aiTest": { "description": "Test your AI providers for tag generation and semantic search embeddings", @@ -156,9 +162,9 @@ "analyzing": "Analisando suas notas para sugestões de rótulos...", "create": "Criar", "createNewLabel": "Criar nova etiqueta", - "created": "{count} labels created successfully", + "created": "{count} etiquetas criadas com sucesso", "creating": "Criando rótulos...", - "description": "I've detected recurring themes in \"{notebookName}\" ({totalNotes} notes). Create labels for them?", + "description": "Detectei temas recorrentes em \"{notebookName}\" ({totalNotes} notas). Criar etiquetas para eles?", "error": "Failed to fetch label suggestions", "new": "(novo)", "noLabelsSelected": "No labels selected", @@ -530,14 +536,14 @@ "memoryEcho": { "clickToView": "Clique para visualizar", "comparison": { - "clickToView": "Click to view note", - "helpful": "Helpful", - "helpfulQuestion": "Is this comparison helpful?", - "highSimilarityInsight": "These notes deal with the same topic with a high degree of similarity. They could be merged or consolidated.", - "notHelpful": "Not Helpful", - "similarityInfo": "These notes are connected by {similarity}% similarity", - "title": "💡 Note Comparison", - "untitled": "Untitled" + "title": "💡 Comparação de notas", + "similarityInfo": "Estas notas estão conectadas por {similarity}% de similaridade", + "highSimilarityInsight": "Estas notas tratam do mesmo tema com alto grau de similaridade. Podem ser mescladas.", + "untitled": "Sem título", + "clickToView": "Clique para ver a nota", + "helpfulQuestion": "Esta comparação é útil?", + "helpful": "Útil", + "notHelpful": "Não útil" }, "connection": "connection", "connections": "Connections", @@ -546,40 +552,40 @@ "description": "Proactive connections between your notes", "dismiss": "Dismiss for now", "editorSection": { - "close": "Fechar", - "compare": "Compare", - "compareAll": "Compare all", - "loading": "Loading...", - "merge": "Merge", - "mergeAll": "Merge all", - "title": "⚡ Connected Notes ({count})", - "view": "View" + "title": "⚡ Notas conectadas ({count})", + "loading": "Carregando...", + "view": "Visualizar", + "compare": "Comparar", + "merge": "Mesclar", + "compareAll": "Comparar tudo", + "mergeAll": "Mesclar tudo", + "close": "Fechar" }, - "fused": "Fused", + "fused": "Mesclado", "fusion": { - "archiveOriginals": "Archive original notes", - "cancel": "Cancel", - "confirmFusion": "Confirm fusion", - "createBacklinks": "Create backlink to original notes", - "edit": "Edit", - "error": "Failed to merge notes", - "finishEditing": "Finish editing", - "generateError": "Failed to generate fusion", - "generateFusion": "Generate the fusion", - "generating": "Generating...", - "keepAllTags": "Keep all tags", - "mergeNotes": "Merge {count} note(s)", - "modify": "Modify", - "noContentReturned": "No fusion content returned from API", - "notesToMerge": "📝 Notes to merge", - "optionalPrompt": "💬 Fusion prompt (optional)", - "optionsTitle": "Fusion options", - "previewTitle": "📝 Preview of merged note", - "promptPlaceholder": "Optional instructions for AI (e.g., 'Keep the formal style of note 1')...", - "success": "Notes merged successfully!", - "title": "🔗 Intelligent Fusion", - "unknownDate": "Unknown date", - "useLatestTitle": "Use latest note as title" + "title": "🔗 Fusão inteligente", + "mergeNotes": "Mesclar {count} nota(s)", + "notesToMerge": "📝 Notas para mesclar", + "optionalPrompt": "💬 Prompt de fusão (opcional)", + "promptPlaceholder": "Instruções opcionais para IA (ex: 'Manter o estilo formal da nota 1')...", + "generateFusion": "Gerar fusão", + "generating": "Gerando...", + "previewTitle": "📝 Prévia da nota mesclada", + "edit": "Editar", + "modify": "Modificar", + "finishEditing": "Concluir edição", + "optionsTitle": "Opções de fusão", + "archiveOriginals": "Arquivar notas originais", + "keepAllTags": "Manter todas as tags", + "useLatestTitle": "Usar nota mais recente como título", + "createBacklinks": "Criar link para notas originais", + "cancel": "Cancelar", + "confirmFusion": "Confirmar fusão", + "success": "Notas mescladas com sucesso!", + "error": "Falha ao mesclar notas", + "generateError": "Falha ao gerar fusão", + "noContentReturned": "Nenhum conteúdo de fusão retornado pela API", + "unknownDate": "Data desconhecida" }, "helpful": "Helpful", "insightReady": "Your insight is ready!", diff --git a/keep-notes/locales/ru.json b/keep-notes/locales/ru.json index 25335ba..b60b256 100644 --- a/keep-notes/locales/ru.json +++ b/keep-notes/locales/ru.json @@ -63,7 +63,13 @@ "tagsGenerationProvider": "Tags Generation Provider", "title": "AI Configuration", "updateFailed": "Failed to update AI settings", - "updateSuccess": "AI Settings updated successfully" + "updateSuccess": "AI Settings updated successfully", + "bestValue": "Лучшее соотношение цена/качество", + "bestQuality": "Лучшее качество", + "providerOllamaOption": "🦙 Ollama (Local & Free)", + "providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)", + "providerCustomOption": "🔧 Custom OpenAI-Compatible", + "saved": "(Сохранено)" }, "aiTest": { "description": "Test your AI providers for tag generation and semantic search embeddings", @@ -156,9 +162,9 @@ "analyzing": "Анализ ваших заметок для предложений меток...", "create": "Создать", "createNewLabel": "Создать новую метку", - "created": "{count} labels created successfully", + "created": "{count} тегов успешно создано", "creating": "Создание меток...", - "description": "I've detected recurring themes in \"{notebookName}\" ({totalNotes} notes). Create labels for them?", + "description": "Я обнаружил повторяющиеся темы в \"{notebookName}\" ({totalNotes} заметках). Создать для них теги?", "error": "Failed to fetch label suggestions", "new": "(новая)", "noLabelsSelected": "No labels selected", @@ -530,14 +536,14 @@ "memoryEcho": { "clickToView": "Нажмите для просмотра", "comparison": { - "clickToView": "Click to view note", - "helpful": "Helpful", - "helpfulQuestion": "Is this comparison helpful?", - "highSimilarityInsight": "These notes deal with the same topic with a high degree of similarity. They could be merged or consolidated.", - "notHelpful": "Not Helpful", - "similarityInfo": "These notes are connected by {similarity}% similarity", - "title": "💡 Note Comparison", - "untitled": "Untitled" + "title": "💡 Сравнение заметок", + "similarityInfo": "Эти заметки связаны на {similarity}% подобия", + "highSimilarityInsight": "Эти заметки относятся к одной теме с высокой степенью подобия. Их можно объединить.", + "untitled": "Без названия", + "clickToView": "Нажмите для просмотра заметки", + "helpfulQuestion": "Полезно ли это сравнение?", + "helpful": "Полезно", + "notHelpful": "Не полезно" }, "connection": "connection", "connections": "Connections", @@ -546,40 +552,40 @@ "description": "Proactive connections between your notes", "dismiss": "Dismiss for now", "editorSection": { - "close": "Закрыть", - "compare": "Compare", - "compareAll": "Compare all", - "loading": "Loading...", - "merge": "Merge", - "mergeAll": "Merge all", - "title": "⚡ Connected Notes ({count})", - "view": "View" + "title": "⚡ Связанные заметки ({count})", + "loading": "Загрузка...", + "view": "Просмотр", + "compare": "Сравнить", + "merge": "Объединить", + "compareAll": "Сравнить всё", + "mergeAll": "Объединить всё", + "close": "Закрыть" }, - "fused": "Fused", + "fused": "Объединено", "fusion": { - "archiveOriginals": "Archive original notes", - "cancel": "Cancel", - "confirmFusion": "Confirm fusion", - "createBacklinks": "Create backlink to original notes", - "edit": "Edit", - "error": "Failed to merge notes", - "finishEditing": "Finish editing", - "generateError": "Failed to generate fusion", - "generateFusion": "Generate the fusion", - "generating": "Generating...", - "keepAllTags": "Keep all tags", - "mergeNotes": "Merge {count} note(s)", - "modify": "Modify", - "noContentReturned": "No fusion content returned from API", - "notesToMerge": "📝 Notes to merge", - "optionalPrompt": "💬 Fusion prompt (optional)", - "optionsTitle": "Fusion options", - "previewTitle": "📝 Preview of merged note", - "promptPlaceholder": "Optional instructions for AI (e.g., 'Keep the formal style of note 1')...", - "success": "Notes merged successfully!", - "title": "🔗 Intelligent Fusion", - "unknownDate": "Unknown date", - "useLatestTitle": "Use latest note as title" + "title": "🔗 Умное слияние", + "mergeNotes": "Объединить {count} заметку/и", + "notesToMerge": "📝 Заметки для объединения", + "optionalPrompt": "💬 Промпт слияния (необязательно)", + "promptPlaceholder": "Необязательные инструкции для ИИ (напр. 'Сохранить формальный стиль заметки 1')...", + "generateFusion": "Сгенерировать слияние", + "generating": "Генерация...", + "previewTitle": "📝 Предпросмотр объединённой заметки", + "edit": "Редактировать", + "modify": "Изменить", + "finishEditing": "Завершить редактирование", + "optionsTitle": "Параметры слияния", + "archiveOriginals": "Архивировать оригинальные заметки", + "keepAllTags": "Сохранить все теги", + "useLatestTitle": "Использовать последнюю заметку как заголовок", + "createBacklinks": "Создать обратную ссылку на оригинальные заметки", + "cancel": "Отмена", + "confirmFusion": "Подтвердить слияние", + "success": "Заметки успешно объединены!", + "error": "Не удалось объединить заметки", + "generateError": "Не удалось создать слияние", + "noContentReturned": "API не вернул содержимого слияния", + "unknownDate": "Неизвестная дата" }, "helpful": "Helpful", "insightReady": "Your insight is ready!", diff --git a/keep-notes/locales/zh.json b/keep-notes/locales/zh.json index 367da71..070be89 100644 --- a/keep-notes/locales/zh.json +++ b/keep-notes/locales/zh.json @@ -63,7 +63,13 @@ "tagsGenerationProvider": "标签生成提供商", "title": "AI 配置", "updateFailed": "更新 AI 设置失败", - "updateSuccess": "AI 设置更新成功" + "updateSuccess": "AI 设置更新成功", + "bestValue": "最佳性价比", + "bestQuality": "最佳质量", + "providerOllamaOption": "🦙 Ollama (Local & Free)", + "providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)", + "providerCustomOption": "🔧 Custom OpenAI-Compatible", + "saved": "(已保存)" }, "aiTest": { "description": "测试您的 AI 提供商的标签生成和语义搜索嵌入", @@ -153,34 +159,36 @@ "analyzing": "AI 分析中...", "assistant": "AI 助手", "autoLabels": { - "analyzing": "正在分析您的笔记以获取标签建议...", + "error": "获取标签建议失败", + "noLabelsSelected": "未选择标签", + "created": "成功创建 {count} 个标签", + "analyzing": "正在分析您的笔记...", + "title": "新标签建议", + "description": "我在\"{notebookName}\"({totalNotes} 条笔记)中检测到了重复主题。要为它们创建标签吗?", + "note": "条笔记", + "notes": "条笔记", + "typeContent": "输入内容以获取标签建议...", + "createNewLabel": "创建此新标签并添加", + "new": "(新)", "create": "创建", - "createNewLabel": "Create this new label and add it", - "created": "{count} labels created successfully", "creating": "正在创建标签...", - "description": "I've detected recurring themes in \"{notebookName}\" ({totalNotes} notes). Create labels for them?", - "error": "Failed to fetch label suggestions", - "new": "(新建)", - "noLabelsSelected": "No labels selected", - "note": "note", - "notes": "notes", - "title": "标签建议", - "typeContent": "Type content to get label suggestions..." + "notesCount": "{count} 条笔记", + "typeForSuggestions": "输入内容以获取标签建议..." }, "batchOrganization": { - "analyzing": "Analyzing your notes...", - "apply": "Apply", - "applyFailed": "Failed to apply organization plan", - "applying": "Applying...", - "description": "AI将分析您的笔记并建议将它们组织到笔记本中。", - "error": "Failed to create organization plan", - "noNotebooks": "No notebooks available. Create notebooks first to organize your notes.", - "noNotesSelected": "No notes selected", - "noSuggestions": "AI could not find a good way to organize these notes.", - "selectAllIn": "Select all notes in {notebook}", - "selectNote": "Select note: {title}", - "success": "{count} notes moved successfully", - "title": "Organize with AI" + "title": "使用 AI 整理", + "description": "AI 将分析您的笔记并建议将它们整理到笔记本中。", + "analyzing": "正在分析您的笔记...", + "noNotebooks": "没有可用的笔记本。请先创建笔记本。", + "noSuggestions": "AI 无法找到整理这些笔记的好方法。", + "apply": "应用", + "applying": "正在应用...", + "success": "成功移动 {count} 条笔记", + "error": "创建整理计划失败", + "noNotesSelected": "未选择笔记", + "applyFailed": "应用整理计划失败", + "selectAllIn": "选择 {notebook} 中的所有笔记", + "selectNote": "选择笔记:{title}" }, "clarify": "澄清", "clickToAddTag": "点击添加此标签",