diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 013f484..5758755 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,9 +8,11 @@ import { useThemeStore } from '@/store/themeStore'; import { ErrorBoundary } from '@/components/common/ErrorBoundary'; import { useToast } from '@/components/ui/Toast'; import { useOnlineStatus } from '@/hooks/useOnlineStatus'; +import { useTranslation } from 'react-i18next'; function QueryClientWrapper({ children }: { children: React.ReactNode }) { + const { t } = useTranslation(); const toast = useToast(); const [queryClient] = React.useState(() => new QueryClient({ @@ -40,18 +42,20 @@ function QueryClientWrapper({ children }: { children: React.ReactNode }) { } function OfflineBanner() { + const { t } = useTranslation(); const isOnline = useOnlineStatus(); if (isOnline) return null; return (
Laden...
} {!isLoading && signals.length === 0 && ( -Keine Signale. Klicken Sie auf „Sammeln" um zu starten.
+{t('improvementPanel.keinesignaleklickensieaufsammeln')}
)} {signals.map(s => ( @@ -92,6 +95,7 @@ function SignalsView() { } function PatternsView() { + const { t } = useTranslation(); const { data, isLoading } = usePatterns(1, 20); const detectMut = useDetectPatterns(); const patterns = data?.items ?? []; @@ -104,7 +108,7 @@ function PatternsView() { onClick={() => detectMut.mutate({ min_occurrences: 2 })} disabled={detectMut.isPending} className="inline-flex items-center gap-1 px-2 py-1 rounded text-xs font-medium text-primary-600 hover:bg-primary-50 disabled:opacity-50 min-h-touch" - aria-label="Muster erkennen" + aria-label={t('improvementPanel.mustererkennen')} > Erkennen @@ -113,7 +117,7 @@ function PatternsView() { {isLoading &&Laden...
} {!isLoading && patterns.length === 0 && ( -Keine Muster. Sammeln Sie zuerst Signale und klicken Sie dann auf „Erkennen".
+{t('improvementPanel.keinemustersammelnsiezuerstsignale')}
)} {patterns.map(p => ( @@ -143,6 +147,7 @@ function PatternsView() { } function ProposalsView() { + const { t } = useTranslation(); const { data, isLoading } = useProposals(1, 20); const evalMut = useEvaluateProposal(); const activateMut = useActivateProposal(); @@ -168,7 +173,7 @@ function ProposalsView() { {isLoading &&Laden...
} {!isLoading && proposals.length === 0 && ( -Keine Vorschläge.
+{t('improvementPanel.keinevorschläge')}
)} {proposals.map(p => ( @@ -191,7 +196,7 @@ function ProposalsView() { onClick={() => evalMut.mutate(p.id)} disabled={evalMut.isPending} className="inline-flex items-center gap-1 px-2 py-1 rounded text-[11px] font-medium text-blue-600 hover:bg-blue-50 disabled:opacity-50 min-h-touch" - aria-label="Evaluieren" + aria-label={t('improvementPanel.evaluieren')} > Evaluieren @@ -202,7 +207,7 @@ function ProposalsView() { onClick={() => activateMut.mutate(p.id)} disabled={activateMut.isPending} className="inline-flex items-center gap-1 px-2 py-1 rounded text-[11px] font-medium text-green-600 hover:bg-green-50 disabled:opacity-50 min-h-touch" - aria-label="Aktivieren" + aria-label={t('improvementPanel.aktivieren')} > Aktivieren @@ -214,7 +219,7 @@ function ProposalsView() { onClick={() => rollbackMut.mutate({ proposalId: p.id, reason: 'Manual rollback' })} disabled={rollbackMut.isPending} className="inline-flex items-center gap-1 px-2 py-1 rounded text-[11px] font-medium text-danger-600 hover:bg-danger-50 disabled:opacity-50 min-h-touch" - aria-label="Rollback" + aria-label={t('improvementPanel.rollback')} > Rollback @@ -223,7 +228,7 @@ function ProposalsView() { onClick={() => measureMut.mutate(p.id)} disabled={measureMut.isPending} className="inline-flex items-center gap-1 px-2 py-1 rounded text-[11px] font-medium text-purple-600 hover:bg-purple-50 disabled:opacity-50 min-h-touch" - aria-label="Impact messen" + aria-label={t('improvementPanel.impactmessen')} > Messen diff --git a/frontend/src/components/ai/SuggestionBadge.tsx b/frontend/src/components/ai/SuggestionBadge.tsx index c5abe9a..980027d 100644 --- a/frontend/src/components/ai/SuggestionBadge.tsx +++ b/frontend/src/components/ai/SuggestionBadge.tsx @@ -1,11 +1,13 @@ import { useState, useEffect } from 'react'; import { apiClient } from '@/api/client'; +import { useTranslation } from 'react-i18next'; interface SuggestionBadgeProps { onClick: () => void; } export function SuggestionBadge({ onClick }: SuggestionBadgeProps) { + const { t } = useTranslation(); const [count, setCount] = useState(0); const [pulsing, setPulsing] = useState(false); @@ -31,7 +33,7 @@ export function SuggestionBadge({ onClick }: SuggestionBadgeProps) { diff --git a/frontend/src/components/ai/SuggestionCard.tsx b/frontend/src/components/ai/SuggestionCard.tsx index 97501b5..e8e2af1 100644 --- a/frontend/src/components/ai/SuggestionCard.tsx +++ b/frontend/src/components/ai/SuggestionCard.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import type { Suggestion } from '@/api/aiProactive'; +import { useTranslation } from 'react-i18next'; const typeConfig = { info: { icon: '💡', color: 'blue', bg: 'bg-blue-50', border: 'border-blue-200', text: 'text-blue-700', bar: 'bg-blue-500' }, @@ -15,6 +16,7 @@ interface SuggestionCardProps { } export function SuggestionCard({ suggestion, onDismiss, onAct }: SuggestionCardProps) { + const { t } = useTranslation(); const [expanded, setExpanded] = useState(false); const config = typeConfig[suggestion.suggestion_type] || typeConfig.info; const confidencePercent = Math.round(suggestion.confidence * 100); @@ -32,7 +34,7 @@ export function SuggestionCard({ suggestion, onDismiss, onAct }: SuggestionCardP @@ -86,7 +88,7 @@ export function SuggestionCard({ suggestion, onDismiss, onAct }: SuggestionCardP {/* Acted upon badge */} {suggestion.is_acted_upon && (Keine Vorschläge vorhanden
-Die KI analysiert deinen Kontext...
+{t('suggestionSidebar.keinevorschlägevorhanden')}
+{t('suggestionSidebar.diekianalysiertdeinenkontext')}
{contactName}
-Kontakt anzeigen
+{t('contactCardBlock.kontaktanzeigen')}
Keine Konfiguration
+{t('miniAppBlock.keinekonfiguration')}
)}- Keine aktiven Filterkriterien + {t('saveFilterDialog.keineaktivenfilterkriterien')}
); } @@ -85,6 +86,7 @@ export function SaveFilterDialog({ ); const handleSave = async () => { + const { t } = useTranslation(); const trimmed = name.trim(); if (!trimmed) return; setSubmitting(true); diff --git a/frontend/src/components/common/ShareDialog.tsx b/frontend/src/components/common/ShareDialog.tsx index 52269c5..c8e17ed 100644 --- a/frontend/src/components/common/ShareDialog.tsx +++ b/frontend/src/components/common/ShareDialog.tsx @@ -25,6 +25,7 @@ import { import { useUsers } from '@/api/users'; import { useGroups } from '@/api/groups'; import type { EntityPermission, PermissionLevel } from '@/api/entityPermissions'; +import { useTranslation } from 'react-i18next'; interface ShareDialogProps { entityType: string; @@ -51,6 +52,7 @@ function permLabel(level: string) { } export function ShareDialog({ entityType, entityId, entityName, onClose }: ShareDialogProps) { + const { t } = useTranslation(); const { data: permData, isLoading } = useEntityPermissions(entityType, entityId); const { data: accessData } = useEntityAccess(entityType, entityId); const { data: usersData } = useUsers(1, 100); @@ -133,7 +135,7 @@ export function ShareDialog({ entityType, entityId, entityName, onClose }: Share @@ -143,10 +145,9 @@ export function ShareDialog({ entityType, entityId, entityName, onClose }: ShareElement teilen
+{t('shareDialog.elementteilen')}
- Gewähre Benutzern oder Gruppen Zugriff auf dieses Element. Die Berechtigungsstufe bestimmt, - welche Aktionen durchgeführt werden können. + {t('shareDialog.gewährebenutzernodergruppenzugriffauf')}
- {selectedContactIds?.size || 0} Kontakt(e) wirklich löschen? + {selectedContactIds?.size || 0} {t('contactList.kontaktewirklichlöschen')}