feat(i18n): migrate hardcoded German strings to t() across 104 components/pages — AST-based batch with re-parse gate, 423 new de.json keys; tsc clean; vitest failures byte-identical to clean-tree baseline (pre-existing)

This commit is contained in:
Agent Zero
2026-08-27 01:43:06 +02:00
parent 5680179260
commit 4cb5298768
105 changed files with 1204 additions and 459 deletions
@@ -237,7 +237,7 @@ export function ContactEditForm({ onClose, contact, onSaved }: ContactEditFormPr
error={errors.name?.message}
required
data-testid="contact-name-input"
placeholder="TechCorp GmbH"
placeholder={t('contactEditForm.techcorpgmbh')}
/>
) : (
<div className="grid grid-cols-2 gap-3">
@@ -257,7 +257,7 @@ export function ContactEditForm({ onClose, contact, onSaved }: ContactEditFormPr
)}
{/* Code */}
<Input label={t('contacts.code')} {...register('code')} placeholder="K-00123" />
<Input label={t('contacts.code')} {...register('code')} placeholder={t('contactEditForm.k00123')} />
{/* Communication */}
<div className="border border-secondary-200 rounded-lg p-3">
@@ -299,7 +299,7 @@ export function ContactEditForm({ onClose, contact, onSaved }: ContactEditFormPr
<div className="border border-secondary-200 rounded-lg p-3">
<h3 className="text-sm font-semibold text-secondary-700 mb-2">{t('contacts.notes')}</h3>
<div className="space-y-3">
<Input label={t('contacts.tags')} {...register('tags')} placeholder="tag1, tag2" />
<Input label={t('contacts.tags')} {...register('tags')} placeholder={t('contactEditForm.tag1tag2')} />
<div>
<label className="block text-sm font-medium text-secondary-700 mb-1">{t('contacts.projectnote')}</label>
<textarea
@@ -162,6 +162,7 @@ function FolderTreeItem({
multiSelectedFolders?: string[];
onToggleMultiSelect?: (folderId: string) => void;
}) {
const { t } = useTranslation();
const [expanded, setExpanded] = useState(true);
const folderKey = `folder:${node.id}` as ContactFilter;
const isActive = selectedFilter === folderKey;
@@ -228,8 +229,8 @@ function FolderTreeItem({
type="button"
onClick={(e) => { e.stopPropagation(); e.preventDefault(); onMoreClick(e, node.id); }}
className="flex-shrink-0 text-secondary-400 hover:text-primary-600 p-1.5 rounded hover:bg-secondary-100 transition-colors touch-manipulation"
title="Optionen"
aria-label="Optionen"
title={t('contactFolderTree.optionen')}
aria-label={t('contactFolderTree.optionen')}
>
{icon(ICONS.more, 'w-4 h-4')}
</button>
@@ -446,8 +447,8 @@ export function ContactFolderTree({
<button
onClick={handleToggleMultiSelectMode}
className={`text-secondary-400 hover:text-primary-600 p-0.5 ${multiSelectMode ? 'text-primary-600 bg-primary-50 rounded' : ''}`}
title="Mehrere Ordner auswählen"
aria-label="Mehrere Ordner auswählen"
title={t('contactFolderTree.mehrereordnerauswählen')}
aria-label={t('contactFolderTree.mehrereordnerauswählen')}
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
<rect x="3" y="3" width="7" height="7" rx="1" />
@@ -461,8 +462,8 @@ export function ContactFolderTree({
<button
onClick={handleNewFolder}
className="text-secondary-400 hover:text-primary-600 p-0.5"
title="Neuer Ordner"
aria-label="Neuer Ordner"
title={t('contactFolderTree.neuerordner')}
aria-label={t('contactFolderTree.neuerordner')}
>
{icon(ICONS.plus, 'w-3.5 h-3.5')}
</button>
@@ -512,7 +513,7 @@ export function ContactFolderTree({
)}
{tree.length === 0 && !foldersLoading && !loading && (
<div className="px-2 py-1 text-xs text-secondary-400">Keine Ordner vorhanden</div>
<div className="px-2 py-1 text-xs text-secondary-400">{t('contactFolderTree.keineordnervorhanden')}</div>
)}
</div>
@@ -574,7 +575,7 @@ export function ContactFolderTree({
<>
<div className="fixed inset-0 z-[9998]" onClick={() => setColorPicker(null)} />
<div className="fixed z-[9999] bg-white border border-secondary-200 rounded-lg shadow-lg p-3 min-w-[200px]" style={{ top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}>
<div className="text-sm font-semibold text-secondary-700 mb-2">Farbe wählen</div>
<div className="text-sm font-semibold text-secondary-700 mb-2">{t('contactFolderTree.farbewählen')}</div>
<div className="grid grid-cols-6 gap-1.5 mb-3">
{['#ef4444', '#f97316', '#f59e0b', '#eab308', '#84cc16', '#22c55e', '#10b981', '#14b8a6', '#06b6d4', '#3b82f6', '#6366f1', '#8b5cf6', '#a855f7', '#d946ef', '#ec4899', '#f43f5e', '#64748b', '#475569'].map((c) => (
<button
@@ -969,7 +969,7 @@ export function ContactList({
onClick={() => setBulkFolderOpen(!bulkFolderOpen)}
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium bg-white/20 hover:bg-white/30 rounded transition-colors"
>
Ordner zuweisen
{t('contactList.ordnerzuweisen')}
</button>
{bulkFolderOpen && (
<div className="absolute right-0 top-full mt-1 bg-white text-secondary-800 rounded-lg shadow-lg border border-secondary-200 max-h-60 overflow-y-auto min-w-[200px] z-30">
@@ -987,7 +987,7 @@ export function ContactList({
</button>
))}
{folderList.length === 0 && (
<div className="px-3 py-2 text-xs text-secondary-400">Keine Ordner</div>
<div className="px-3 py-2 text-xs text-secondary-400">{t('contactList.keineordner')}</div>
)}
</div>
)}
@@ -999,7 +999,7 @@ export function ContactList({
onClick={() => setBulkTagsOpen(!bulkTagsOpen)}
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium bg-white/20 hover:bg-white/30 rounded transition-colors"
>
Tags hinzufügen
{t('contactList.tagshinzufügen')}
</button>
{bulkTagsOpen && (
<div className="absolute right-0 top-full mt-1 bg-white text-secondary-800 rounded-lg shadow-lg border border-secondary-200 p-2 z-30">
@@ -1007,7 +1007,7 @@ export function ContactList({
type="text"
value={bulkTagsInput}
onChange={(e) => setBulkTagsInput(e.target.value)}
placeholder="tag1, tag2, ..."
placeholder={t('contactList.tag1tag2')}
className="w-48 px-2 py-1 text-xs border border-secondary-200 rounded focus:outline-none focus:border-primary-400"
/>
<button
@@ -1032,7 +1032,7 @@ export function ContactList({
onClick={clearSelection}
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium bg-white/20 hover:bg-white/30 rounded transition-colors"
>
Auswahl aufheben
{t('contactList.auswahlaufheben')}
</button>
</div>
)}
@@ -1041,9 +1041,9 @@ export function ContactList({
{bulkDeleteConfirm && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white rounded-lg shadow-xl p-6 max-w-sm">
<h3 className="text-lg font-semibold text-secondary-900 mb-2">Löschen bestätigen</h3>
<h3 className="text-lg font-semibold text-secondary-900 mb-2">{t('contactList.löschenbestätigen')}</h3>
<p className="text-sm text-secondary-600 mb-4">
{selectedContactIds?.size || 0} Kontakt(e) wirklich löschen?
{selectedContactIds?.size || 0} {t('contactList.kontaktewirklichlöschen')}
</p>
<div className="flex justify-end gap-2">
<button
@@ -1073,7 +1073,7 @@ export function ContactList({
{isCustomSortActive && customOrder.length > 0 && (
<div className="flex items-center gap-1.5 px-3 py-1 bg-amber-50 border-b border-amber-200 text-xs text-amber-700">
<Info className="w-3 h-3" />
<span>Custom Sortierung aktiv Drag-and-Drop zum Umsortieren</span>
<span>{t('contactList.customsortierungaktivdraganddrop')}</span>
</div>
)}
@@ -1138,15 +1138,15 @@ export function ContactList({
<div ref={colMenuRef} className="relative">
<button
onClick={() => setColMenuOpen(!colMenuOpen)}
title="Spalten verwalten"
aria-label="Spalten verwalten"
title={t('contactList.spaltenverwalten')}
aria-label={t('contactList.spaltenverwalten')}
className="p-1 rounded hover:bg-secondary-100 text-secondary-500 hover:text-secondary-700 transition-colors"
>
<Settings className="w-3.5 h-3.5" />
</button>
{colMenuOpen && (
<div className="absolute right-0 top-full mt-1 bg-white rounded-lg shadow-lg border border-secondary-200 max-h-80 overflow-y-auto min-w-[200px] z-30">
<div className="px-3 py-2 text-xs font-semibold text-secondary-700 border-b border-secondary-100">Spalten verwalten</div>
<div className="px-3 py-2 text-xs font-semibold text-secondary-700 border-b border-secondary-100">{t('contactList.spaltenverwalten')}</div>
{ALL_COLUMNS.map((col) => (
<label
key={col.key}
@@ -8,6 +8,7 @@ import React, { useState, useRef, useEffect, useMemo } from 'react';
import { Filter, Plus, X, ChevronDown, Bookmark } from 'lucide-react';
import type { UnifiedContact } from '@/api/unifiedContacts';
import { useCustomFieldDefinitions, type CustomFieldDefinition } from '@/api/customFieldDefinitions';
import { useTranslation } from 'react-i18next';
// ─── Field definitions ───────────────────────────────────────────────────────
@@ -245,6 +246,7 @@ function newConditionId() {
}
export function FilterPanel({ filters, onFiltersChange, contactType, savedFilters = [], onSaveFilter, onLoadFilter, onDeleteFilter }: FilterPanelProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const btnRef = useRef<HTMLDivElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
@@ -373,8 +375,8 @@ export function FilterPanel({ filters, onFiltersChange, contactType, savedFilter
<div ref={btnRef} className="relative flex-shrink-0">
<button
onClick={handleToggle}
title="Filter"
aria-label="Filter"
title={t('filterPanel.filter')}
aria-label={t('filterPanel.filter')}
className={`
inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium
transition-colors duration-100 cursor-pointer relative
@@ -415,7 +417,7 @@ export function FilterPanel({ filters, onFiltersChange, contactType, savedFilter
onClick={clearAll}
className="text-xs text-secondary-500 hover:text-red-600 transition-colors"
>
Alle löschen
{t('filterPanel.allelöschen')}
</button>
)}
<button
@@ -430,7 +432,7 @@ export function FilterPanel({ filters, onFiltersChange, contactType, savedFilter
{/* Logic toggle */}
{activeCount > 0 && (
<div className="flex items-center gap-2 px-4 py-2 border-b border-secondary-100">
<span className="text-xs text-secondary-500">Bedingungen verknüpfen:</span>
<span className="text-xs text-secondary-500">{t('filterPanel.bedingungenverknüpfen')}</span>
<button
onClick={toggleLogic}
className={`
@@ -483,7 +485,7 @@ export function FilterPanel({ filters, onFiltersChange, contactType, savedFilter
{/* Saved filters */}
{savedFilters.length > 0 && (
<div className="px-4 py-2 border-b border-secondary-100">
<div className="text-[10px] font-semibold uppercase tracking-wide text-secondary-400 mb-1.5">Gespeicherte Filter</div>
<div className="text-[10px] font-semibold uppercase tracking-wide text-secondary-400 mb-1.5">{t('filterPanel.gespeichertefilter')}</div>
<div className="space-y-0.5">
{savedFilters.map((sf) => (
<div key={sf.id} className="flex items-center gap-1 group">
@@ -510,7 +512,7 @@ export function FilterPanel({ filters, onFiltersChange, contactType, savedFilter
<div className="px-4 py-3 space-y-2">
{filters.conditions.length === 0 && (
<div className="text-center py-6 text-xs text-secondary-400">
Keine Filter aktiv. Klicke unten um eine Bedingung hinzuzufügen.
{t('filterPanel.keinefilteraktivklickeuntenum')}
</div>
)}
{filters.conditions.map((cond, idx) => {
@@ -570,7 +572,7 @@ export function FilterPanel({ filters, onFiltersChange, contactType, savedFilter
onChange={(e) => updateCondition(cond.id, { value: e.target.value })}
className="px-2 py-1.5 text-xs border border-secondary-200 rounded bg-white cursor-pointer focus:outline-none focus:border-primary-400"
>
<option value=""> wählen </option>
<option value="">{t('filterPanel.wählen')}</option>
{def.options?.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
@@ -587,7 +589,7 @@ export function FilterPanel({ filters, onFiltersChange, contactType, savedFilter
type="text"
value={cond.value}
onChange={(e) => updateCondition(cond.id, { value: e.target.value })}
placeholder="Wert…"
placeholder={t('filterPanel.wert')}
className="w-24 px-2 py-1.5 text-xs border border-secondary-200 rounded focus:outline-none focus:border-primary-400"
/>
)
@@ -615,7 +617,7 @@ export function FilterPanel({ filters, onFiltersChange, contactType, savedFilter
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors"
>
<Plus className="w-3.5 h-3.5" />
Bedingung hinzufügen
{t('filterPanel.bedingunghinzufügen')}
</button>
{activeCount > 0 && onSaveFilter && (
<button
@@ -623,7 +625,7 @@ export function FilterPanel({ filters, onFiltersChange, contactType, savedFilter
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors"
>
<Bookmark className="w-3.5 h-3.5" />
Filter speichern
{t('filterPanel.filterspeichern')}
</button>
)}
</div>
@@ -11,6 +11,7 @@ import {
import { useUsers } from '@/api/users';
import { useGroups } from '@/api/groups';
import type { FolderPermission } from '@/api/contactFolders';
import { useTranslation } from 'react-i18next';
interface FolderPermissionDialogProps {
folderId: string;
@@ -36,6 +37,7 @@ function permLabel(level: string) {
}
export function FolderPermissionDialog({ folderId, folderName, onClose }: FolderPermissionDialogProps) {
const { t } = useTranslation();
const { data: permData, isLoading } = useFolderPermissions(folderId);
const { data: usersData } = useUsers(1, 100);
const { data: groupsData } = useGroups();
@@ -109,7 +111,7 @@ export function FolderPermissionDialog({ folderId, folderName, onClose }: Folder
<button
onClick={onClose}
className="text-secondary-400 hover:text-secondary-600 p-1 rounded-md hover:bg-secondary-100"
aria-label="Schließen"
aria-label={t('folderPermissionDialog.schließen')}
>
<X className="w-5 h-5" strokeWidth={2} />
</button>
@@ -119,9 +121,9 @@ export function FolderPermissionDialog({ folderId, folderName, onClose }: Folder
<div className="flex-1 overflow-y-auto px-5 py-4">
{/* Info banner */}
<div className="mb-4 p-3 bg-primary-50 border border-primary-200 rounded-lg text-sm text-primary-700">
<p className="font-medium mb-1">Ordner teilen</p>
<p className="font-medium mb-1">{t('folderPermissionDialog.ordnerteilen')}</p>
<p className="text-primary-600">
Gewähre Benutzern oder Gruppen Zugriff auf diesen Ordner. Mit Vererben" gelten die Rechte auch für alle Unterordner.
{t('folderPermissionDialog.gewährebenutzernodergruppenzugriffauf')}
</p>
</div>
@@ -130,7 +132,7 @@ export function FolderPermissionDialog({ folderId, folderName, onClose }: Folder
<div className="text-sm text-secondary-400 py-4 text-center">Laden</div>
) : permissions.length === 0 ? (
<div className="text-sm text-secondary-400 py-4 text-center">
Noch keine Berechtigungen vergeben. Dieser Ordner ist nur für den Besitzer sichtbar.
{t('folderPermissionDialog.nochkeineberechtigungenvergebendieserord')}
</div>
) : (
<div className="space-y-2">
@@ -176,8 +178,8 @@ export function FolderPermissionDialog({ folderId, folderName, onClose }: Folder
<button
onClick={() => handleDelete(perm)}
className="text-secondary-400 hover:text-red-600 p-1.5 rounded-md hover:bg-red-50 flex-shrink-0"
title="Entfernen"
aria-label="Berechtigung entfernen"
title={t('folderPermissionDialog.entfernen')}
aria-label={t('folderPermissionDialog.berechtigungentfernen')}
>
<Trash2 className="w-4 h-4" strokeWidth={2} />
</button>
@@ -192,7 +194,7 @@ export function FolderPermissionDialog({ folderId, folderName, onClose }: Folder
<div className="mt-4 p-4 border-2 border-primary-200 rounded-lg bg-primary-50/50">
<div className="flex items-center gap-2 mb-3">
<Plus className="w-4 h-4 text-primary-600" strokeWidth={2} />
<span className="text-sm font-medium text-secondary-700">Neue Berechtigung</span>
<span className="text-sm font-medium text-secondary-700">{t('folderPermissionDialog.neueberechtigung')}</span>
</div>
{/* Type toggle */}
@@ -264,7 +266,7 @@ export function FolderPermissionDialog({ folderId, folderName, onClose }: Folder
onChange={(e) => setAddInherit(e.target.checked)}
className="w-4 h-4 rounded border-secondary-300 text-primary-600 focus:ring-primary-500"
/>
Auf Unterordner vererben
{t('folderPermissionDialog.aufunterordnervererben')}
</label>
{/* Actions */}
@@ -290,7 +292,7 @@ export function FolderPermissionDialog({ folderId, folderName, onClose }: Folder
className="mt-4 w-full flex items-center justify-center gap-2 py-2.5 text-sm text-primary-600 border-2 border-dashed border-primary-200 rounded-lg hover:bg-primary-50 hover:border-primary-300 transition-colors"
>
<Plus className="w-4 h-4" strokeWidth={2} />
Berechtigung hinzufügen
{t('folderPermissionDialog.berechtigunghinzufügen')}
</button>
)}
</div>
@@ -8,6 +8,7 @@ import React, { useState, useRef, useEffect, useMemo } from 'react';
import { Group as GroupIcon, Plus, X } from 'lucide-react';
import type { UnifiedContact } from '@/api/unifiedContacts';
import { useCustomFieldDefinitions } from '@/api/customFieldDefinitions';
import { useTranslation } from 'react-i18next';
// ─── Field definitions ────────────────────────────────────────────────────────
@@ -172,6 +173,7 @@ function newGroupId() {
}
export function GroupPanel({ groupState, onGroupChange, contactType }: GroupPanelProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const btnRef = useRef<HTMLDivElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
@@ -289,8 +291,8 @@ export function GroupPanel({ groupState, onGroupChange, contactType }: GroupPane
<div ref={btnRef} className="relative flex-shrink-0">
<button
onClick={handleToggle}
title="Gruppierung"
aria-label="Gruppierung"
title={t('groupPanel.gruppierung')}
aria-label={t('groupPanel.gruppierung')}
className={`
inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium
transition-colors duration-100 cursor-pointer relative
@@ -331,7 +333,7 @@ export function GroupPanel({ groupState, onGroupChange, contactType }: GroupPane
onClick={clearAll}
className="text-xs text-secondary-500 hover:text-red-600 transition-colors"
>
Alle löschen
{t('groupPanel.allelöschen')}
</button>
)}
<button
@@ -347,7 +349,7 @@ export function GroupPanel({ groupState, onGroupChange, contactType }: GroupPane
{activeCount === 0 && (
<div className="px-4 py-3 border-b border-secondary-100">
<div className="text-center py-4 text-xs text-secondary-400">
Keine Gruppierung aktiv. Alle Datensätze werden in einer flachen Liste angezeigt.
{t('groupPanel.keinegruppierungaktivalledatensätzewerde')}
</div>
</div>
)}
@@ -435,7 +437,7 @@ export function GroupPanel({ groupState, onGroupChange, contactType }: GroupPane
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors"
>
<Plus className="w-3.5 h-3.5" />
Gruppierung hinzufügen
{t('groupPanel.gruppierunghinzufügen')}
</button>
<button
onClick={() => setOpen(false)}
@@ -4,6 +4,7 @@
import React, { useState } from 'react';
import { Bookmark, Check, Folder, Filter, Group as GroupIcon, ArrowDownAZ, LayoutGrid } from 'lucide-react';
import { useTranslation } from 'react-i18next';
export interface SaveViewSelection {
folder: boolean;
@@ -32,6 +33,7 @@ const defaultSelection: SaveViewSelection = {
};
export function SaveViewDialog({ open, onClose, onSave, hasFilter, hasGroup, hasSort, hasFolder }: SaveViewDialogProps) {
const { t } = useTranslation();
const [name, setName] = useState('');
const [selection, setSelection] = useState<SaveViewSelection>(defaultSelection);
@@ -66,19 +68,19 @@ export function SaveViewDialog({ open, onClose, onSave, hasFilter, hasGroup, has
{/* Header */}
<div className="flex items-center gap-2 px-5 py-4 border-b border-secondary-100">
<Bookmark className="w-5 h-5 text-primary-600" strokeWidth={2} />
<h2 className="text-base font-semibold text-secondary-800">Ansicht speichern</h2>
<h2 className="text-base font-semibold text-secondary-800">{t('saveViewDialog.ansichtspeichern')}</h2>
</div>
{/* Body */}
<div className="px-5 py-4 space-y-4">
{/* Name input */}
<div>
<label className="block text-xs font-medium text-secondary-600 mb-1">Name der Ansicht</label>
<label className="block text-xs font-medium text-secondary-600 mb-1">{t('saveViewDialog.namederansicht')}</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="z.B. Meine Firmen-Kontakte"
placeholder={t('saveViewDialog.zbmeinefirmenkontakte')}
autoFocus
onKeyDown={(e) => { if (e.key === 'Enter') handleSave(); }}
className="w-full px-3 py-2 text-sm border border-secondary-200 rounded-md focus:outline-none focus:border-primary-400 focus:ring-1 focus:ring-primary-300"
@@ -87,7 +89,7 @@ export function SaveViewDialog({ open, onClose, onSave, hasFilter, hasGroup, has
{/* Component selection */}
<div>
<label className="block text-xs font-medium text-secondary-600 mb-2">Was soll gespeichert werden?</label>
<label className="block text-xs font-medium text-secondary-600 mb-2">{t('saveViewDialog.wassollgespeichertwerden')}</label>
<div className="space-y-1.5">
{options.map((opt) => (
<label
@@ -112,7 +114,7 @@ export function SaveViewDialog({ open, onClose, onSave, hasFilter, hasGroup, has
</div>
</span>
{!opt.available && (
<span className="text-[10px] text-secondary-400 italic">nicht aktiv</span>
<span className="text-[10px] text-secondary-400 italic">{t('saveViewDialog.nichtaktiv')}</span>
)}
</label>
))}
@@ -8,6 +8,7 @@ import React, { useState, useRef, useEffect, useMemo } from 'react';
import { ArrowDownAZ, ArrowUpZA, Plus, X } from 'lucide-react';
import type { UnifiedContact } from '@/api/unifiedContacts';
import { useCustomFieldDefinitions } from '@/api/customFieldDefinitions';
import { useTranslation } from 'react-i18next';
// ─── Field definitions (reuse from FilterPanel) ────────────────────────────────
@@ -164,6 +165,7 @@ function newSortId() {
}
export function SortPanel({ sortState, onSortChange, contactType }: SortPanelProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const btnRef = useRef<HTMLDivElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
@@ -282,8 +284,8 @@ export function SortPanel({ sortState, onSortChange, contactType }: SortPanelPro
<div ref={btnRef} className="relative flex-shrink-0">
<button
onClick={handleToggle}
title="Sortieren"
aria-label="Sortieren"
title={t('sortPanel.sortieren')}
aria-label={t('sortPanel.sortieren')}
className={`
inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium
transition-colors duration-100 cursor-pointer relative
@@ -324,7 +326,7 @@ export function SortPanel({ sortState, onSortChange, contactType }: SortPanelPro
onClick={clearAll}
className="text-xs text-secondary-500 hover:text-red-600 transition-colors"
>
Alle löschen
{t('sortPanel.allelöschen')}
</button>
)}
<button
@@ -340,7 +342,7 @@ export function SortPanel({ sortState, onSortChange, contactType }: SortPanelPro
{activeCount === 0 && (
<div className="px-4 py-3 border-b border-secondary-100">
<div className="text-center py-4 text-xs text-secondary-400">
Keine Sortierung aktiv. Datensätze können per Drag-and-Drop umsortiert werden.
{t('sortPanel.keinesortierungaktivdatensätzekönnenper')}
</div>
</div>
)}
@@ -448,7 +450,7 @@ export function SortPanel({ sortState, onSortChange, contactType }: SortPanelPro
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors"
>
<Plus className="w-3.5 h-3.5" />
Sortierung hinzufügen
{t('sortPanel.sortierunghinzufügen')}
</button>
<button
onClick={() => setOpen(false)}