From d1f0b3c3d6ea6c3ccc1dfe0c1f92d2dbd27ff77f Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Sat, 11 Apr 2026 19:44:52 +0200 Subject: [PATCH] fixed language logic items --- src/api/attributesApi.ts | 2 +- src/api/promptApi.ts | 2 +- src/api/userApi.ts | 2 +- .../AccessRules/AccessRulesEditor.tsx | 69 ++++++---- .../AccessRules/AccessRulesTable.tsx | 38 +++--- src/components/Chat/ChatInput.tsx | 9 +- src/components/Chat/ChatMessageList.tsx | 9 +- .../ContentPreview/ContentPreview.tsx | 8 +- .../ContentPreview/UrlContentPreview.tsx | 22 +-- .../ContentPreview/renderers/JsonRenderer.tsx | 4 +- .../renderers/PdfJsRenderer.tsx | 8 +- .../editor/Automation2FlowEditor.tsx | 51 +++---- .../FlowEditor/editor/CanvasHeader.tsx | 42 +++--- .../FlowEditor/editor/EditorChatPanel.tsx | 16 +-- .../FlowEditor/editor/FlowCanvas.tsx | 21 ++- .../FlowEditor/editor/NodeConfigPanel.tsx | 2 +- .../FlowEditor/editor/NodeSidebar.tsx | 2 +- .../FlowEditor/editor/RunTracingPanel.tsx | 17 ++- .../FlowEditor/editor/TemplatePicker.tsx | 41 +++--- .../editor/WorkflowConfigurationModal.tsx | 17 +-- .../FlowEditor/nodes/form/FormNodeConfig.tsx | 49 +++---- .../nodes/frontendTypeRenderers/index.tsx | 45 ++++--- .../nodes/ifElse/IfElseNodeConfig.tsx | 4 +- .../FlowEditor/nodes/shared/DataPicker.tsx | 16 ++- .../nodes/shared/LoopItemsSelect.tsx | 32 ++--- .../nodes/shared/RefSourceSelect.tsx | 40 +++--- .../nodes/start/FormStartNodeConfig.tsx | 23 ++-- .../nodes/start/ScheduleStartNodeConfig.tsx | 29 ++-- .../nodes/start/StartNodeConfig.tsx | 16 ++- .../nodes/switch/SwitchNodeConfig.tsx | 10 +- src/components/FolderTree/FolderTree.tsx | 27 ++-- .../FolderTree/SharepointBrowseTree.tsx | 16 ++- .../DeleteActionButton/DeleteActionButton.tsx | 4 +- .../ViewActionButton/ViewActionButton.tsx | 4 +- .../FormGeneratorForm/FormGeneratorForm.tsx | 48 +++---- .../FormGeneratorReport.tsx | 110 ++++++++------- src/components/Navigation/UserSection.tsx | 16 +-- .../NotificationBell/NotificationBell.tsx | 46 +++---- src/components/OnboardingAssistant.tsx | 29 ++-- src/components/OnboardingWizard.tsx | 12 +- .../ProviderSelector/ProviderSelector.tsx | 38 +++--- .../RbacExportImport/RbacExportImport.tsx | 54 ++++---- .../AddressAutocomplete.tsx | 6 +- .../BauvorschriftenSection.tsx | 10 +- .../Button/CreateButton/CreateButton.tsx | 4 +- .../Button/UploadButton/UploadButton.tsx | 3 +- .../ConnectedFilesList/ConnectedFilesList.tsx | 4 +- .../DropdownSelect/DropdownSelect.tsx | 16 ++- .../LocationInput/LocationInput.tsx | 10 +- src/components/UiComponents/Log/Log.tsx | 13 +- .../Log/LogMessage/LogMessage.tsx | 4 +- .../UiComponents/MapView/MapViewLeaflet.tsx | 8 +- .../Messages/ChatMessages/ChatMessage.tsx | 2 +- .../UiComponents/Messages/Messages.tsx | 7 +- .../OerebSection/OerebSection.tsx | 20 +-- .../ParcelInfoPanel/ParcelInfoPanel.tsx | 40 +++--- .../WorkflowStatus/WorkflowStatus.tsx | 17 ++- src/components/UnifiedDataBar/ChatsTab.tsx | 24 ++-- src/components/UnifiedDataBar/FilesTab.tsx | 12 +- src/components/UnifiedDataBar/SourcesTab.tsx | 69 +++++----- .../UnifiedDataBar/UnifiedDataBar.tsx | 2 +- src/core/PageManager/SidebarProvider.tsx | 4 +- src/core/PageManager/pageInterface.ts | 11 +- src/hooks/useAccessRules.tsx | 10 +- src/hooks/useAdminMandates.ts | 52 +++----- src/hooks/useAdminRbacRoles.ts | 52 +++----- src/hooks/useAdminRbacRules.ts | 52 +++----- src/hooks/useFeatureAccess.ts | 6 +- src/hooks/useMandateRoles.ts | 6 +- src/hooks/useMandates.ts | 2 +- src/hooks/usePrompts.ts | 28 ++-- src/hooks/useRbacExportImport.ts | 2 +- src/hooks/useRealEstate.ts | 8 +- src/hooks/useTrustee.ts | 36 ++--- src/hooks/useTrusteeAccess.ts | 13 +- src/hooks/useTrusteeContracts.ts | 13 +- src/hooks/useTrusteeDocuments.ts | 13 +- src/hooks/useTrusteeOrganisations.ts | 26 ++-- src/hooks/useTrusteePositionDocuments.ts | 13 +- src/hooks/useTrusteePositions.ts | 13 +- src/hooks/useTrusteeRoles.ts | 13 +- src/hooks/useUserMandates.ts | 4 +- src/hooks/useUsers.ts | 44 ++---- src/hooks/useWorkflows.ts | 24 +--- src/layouts/FeatureLayout.tsx | 35 +++-- src/layouts/MainLayout.tsx | 4 +- src/pages/AutomationsDashboardPage.tsx | 4 +- src/pages/Dashboard.tsx | 7 +- src/pages/FeatureView.tsx | 4 +- src/pages/GDPR.tsx | 46 ++++--- src/pages/InvitePage.tsx | 2 +- src/pages/Login.tsx | 16 +-- src/pages/PasswordResetRequest.tsx | 14 +- src/pages/Register.tsx | 30 ++--- src/pages/Reset.tsx | 28 ++-- src/pages/Settings.tsx | 14 +- src/pages/admin/AccessManagementHub.tsx | 77 ++++++----- src/pages/admin/AdminFeatureAccessPage.tsx | 76 ++++++----- .../admin/AdminFeatureInstanceUsersPage.tsx | 43 +++--- src/pages/admin/AdminFeatureRolesPage.tsx | 71 +++++----- src/pages/admin/AdminInvitationsPage.tsx | 49 ++++--- src/pages/admin/AdminLogsPage.tsx | 22 ++- .../admin/AdminMandateRolePermissionsPage.tsx | 76 +++++------ src/pages/admin/AdminMandateRolesPage.tsx | 101 ++++++-------- src/pages/admin/AdminMandatesPage.tsx | 49 +++---- .../admin/AdminUserAccessOverviewPage.tsx | 79 +++++------ src/pages/admin/AdminUserMandatesPage.tsx | 42 +++--- src/pages/admin/AdminUsersPage.tsx | 14 +- src/pages/admin/ChatbotConfigSection.tsx | 20 ++- src/pages/admin/InstanceDetailModal.tsx | 56 ++++---- src/pages/admin/InstanceHierarchyView.tsx | 30 +++-- src/pages/admin/PermissionMatrix.tsx | 12 +- .../wizards/AdminInvitationWizardPage.tsx | 85 ++++++------ .../admin/wizards/AdminMandateWizardPage.tsx | 125 +++++++++--------- .../admin/wizards/FeatureInstanceWizard.tsx | 42 +++--- src/pages/basedata/ConnectionsPage.tsx | 4 +- src/pages/basedata/FilesPage.tsx | 24 ++-- src/pages/basedata/PromptsPage.tsx | 12 +- src/pages/billing/BillingAdmin.tsx | 15 +-- src/pages/billing/BillingDashboard.tsx | 11 +- src/pages/billing/BillingMandateView.tsx | 16 ++- src/pages/billing/BillingNav.tsx | 8 +- src/pages/billing/BillingUserView.tsx | 16 +-- src/pages/billing/SubscriptionTab.tsx | 82 ++++++------ .../chatbot/ChatbotConversationsView.tsx | 24 ++-- .../commcoach/CommcoachDashboardView.tsx | 73 ++++++---- .../views/commcoach/CommcoachDossierView.tsx | 12 +- .../views/commcoach/CommcoachSettingsView.tsx | 14 +- .../GraphicalEditorTemplatesPage.tsx | 78 ++++++----- .../GraphicalEditorWorkflowsPage.tsx | 40 +++--- .../GraphicalEditorWorkflowsTasksPage.tsx | 69 +++++----- .../neutralization/NeutralizationView.tsx | 119 +++++++++-------- .../views/realestate/pek/PekLocationInput.tsx | 4 +- src/pages/views/realestate/pek/PekMapView.tsx | 2 +- .../views/teamsbot/TeamsbotDashboardView.tsx | 18 +-- .../views/teamsbot/TeamsbotSessionView.tsx | 50 +++---- .../views/teamsbot/TeamsbotSettingsView.tsx | 6 +- .../views/trustee/TrusteeAbschlussView.tsx | 40 +++--- .../trustee/TrusteeAccountingSettingsView.tsx | 6 +- .../views/trustee/TrusteeAnalyseView.tsx | 38 +++--- .../trustee/TrusteeExpenseImportView.tsx | 72 +++++----- .../trustee/TrusteeInstanceRolesView.tsx | 11 +- .../views/trustee/TrusteeScanUploadView.tsx | 2 +- src/pages/views/workspace/FilePreview.tsx | 4 +- .../views/workspace/NeutralizationPanel.tsx | 16 +-- src/pages/views/workspace/ToolActivityLog.tsx | 87 ++++++------ .../views/workspace/WorkspaceEditorPage.tsx | 22 +-- .../workspace/WorkspaceGeneralSettings.tsx | 12 +- src/pages/views/workspace/WorkspacePage.tsx | 20 +-- .../workspace/WorkspaceRagInsightsPage.tsx | 34 ++--- .../views/workspace/WorkspaceSettingsPage.tsx | 6 +- src/providers/auth/AuthProvider.tsx | 4 +- src/providers/auth/ProtectedRoute.tsx | 4 +- src/stores/featureStore.tsx | 6 +- src/types/mandate.ts | 28 ---- 155 files changed, 2091 insertions(+), 2057 deletions(-) diff --git a/src/api/attributesApi.ts b/src/api/attributesApi.ts index 4805aa8..1776fb9 100644 --- a/src/api/attributesApi.ts +++ b/src/api/attributesApi.ts @@ -18,7 +18,7 @@ export interface AttributeDefinition { description?: string; required?: boolean; default?: any; - options?: Array<{ value: string | number; label: string | { [key: string]: string } }> | string; + options?: Array<{ value: string | number; label: string }> | string; validation?: any; ui?: any; readonly?: boolean; diff --git a/src/api/promptApi.ts b/src/api/promptApi.ts index 094dcdc..00f1be7 100644 --- a/src/api/promptApi.ts +++ b/src/api/promptApi.ts @@ -16,7 +16,7 @@ export interface Prompt { export interface AttributeOption { value: string | number; - label: string | { [key: string]: string }; + label: string; } export interface AttributeDefinition { diff --git a/src/api/userApi.ts b/src/api/userApi.ts index 6df4dbd..80207c0 100644 --- a/src/api/userApi.ts +++ b/src/api/userApi.ts @@ -28,7 +28,7 @@ export interface AttributeDefinition { description?: string; required?: boolean; default?: any; - options?: Array<{ value: string | number; label: string | { [key: string]: string } }> | string; + options?: Array<{ value: string | number; label: string }> | string; validation?: any; sortable?: boolean; filterable?: boolean; diff --git a/src/components/AccessRules/AccessRulesEditor.tsx b/src/components/AccessRules/AccessRulesEditor.tsx index b652019..12619de 100644 --- a/src/components/AccessRules/AccessRulesEditor.tsx +++ b/src/components/AccessRules/AccessRulesEditor.tsx @@ -86,7 +86,7 @@ const RuleCard: React.FC = ({ rule, readOnly, onUpdate, onDelete @@ -97,7 +97,7 @@ const RuleCard: React.FC = ({ rule, readOnly, onUpdate, onDelete
{/* View Toggle */}
- View + {t('Ansicht')}
= ({ rule, readOnly, onUpdate, onDelete {isDataRule ? ( <>
- Read + {t('Lesen')} onUpdate(rule.id, { read: value })} @@ -122,7 +122,7 @@ const RuleCard: React.FC = ({ rule, readOnly, onUpdate, onDelete />
- Create + {t('Erstellen')} onUpdate(rule.id, { create: value })} @@ -131,7 +131,7 @@ const RuleCard: React.FC = ({ rule, readOnly, onUpdate, onDelete />
- Update + {t('Bearbeiten')} onUpdate(rule.id, { update: value })} @@ -140,7 +140,7 @@ const RuleCard: React.FC = ({ rule, readOnly, onUpdate, onDelete />
- {t('delete')} + {t('Löschen')} onUpdate(rule.id, { delete: value })} @@ -214,7 +214,7 @@ const AddRuleForm: React.FC = ({ context, availableObjects, on }; const getLabel = (obj: CatalogObject): string => { - return obj.label.de || obj.label.en || obj.objectKey; + return (typeof obj.label === 'string' ? obj.label : '') || obj.objectKey; }; return ( @@ -260,7 +260,9 @@ const AddRuleForm: React.FC = ({ context, availableObjects, on )} - Leer lassen für globale Regel. Längster Match gewinnt bei Wildcards (z.B. data.feature.trustee.*). + {t( + 'Leer lassen für globale Regel. Längster Match gewinnt bei Wildcards (z.B. data.feature.trustee.*).' + )}
@@ -272,7 +274,7 @@ const AddRuleForm: React.FC = ({ context, availableObjects, on onChange={(e) => setView(e.target.checked)} style={{ marginRight: '0.5rem' }} /> - Sichtbar (View) + {t('Sichtbar (Ansicht)')}
@@ -290,8 +292,13 @@ const AddRuleForm: React.FC = ({ context, availableObjects, on {(['create', 'read', 'update', 'delete'] as const).map(op => { const value = op === 'delete' ? del : op === 'create' ? create : op === 'update' ? update : read; const setValue = op === 'delete' ? setDel : op === 'create' ? setCreate : op === 'update' ? setUpdate : setRead; - const labels = { create: 'Create', read: 'Read', update: 'Update', delete: 'Delete' }; - + const labels = { + create: t('Erstellen'), + read: t('Lesen'), + update: t('Bearbeiten'), + delete: t('Löschen'), + }; + return (
{labels[op]}
@@ -310,7 +317,7 @@ const AddRuleForm: React.FC = ({ context, availableObjects, on setValue(hierarchy[idx - 1] || 'n'); } }} - title={`${labels[op]} - ${level === 'm' ? 'Eigene' : level === 'g' ? 'Gruppe' : 'Alle'}`} + title={`${labels[op]} - ${level === 'm' ? t('Eigene') : level === 'g' ? t('Gruppe') : t('Alle')}`} />
))} @@ -322,10 +329,10 @@ const AddRuleForm: React.FC = ({ context, availableObjects, on
@@ -355,6 +362,7 @@ const RulesSection: React.FC = ({ onDelete, onAdd, }) => { + const { t } = useLanguage(); const [showAddForm, setShowAddForm] = useState(false); const [useTableView, setUseTableView] = useState(context === 'DATA'); // Default to table for DATA @@ -373,9 +381,12 @@ const RulesSection: React.FC = ({ const getEmptyText = () => { switch (context) { - case 'DATA': return 'Keine Daten-Regeln definiert'; - case 'UI': return 'Keine UI-Regeln definiert'; - case 'RESOURCE': return 'Keine Ressourcen-Regeln definiert'; + case 'DATA': + return t('Keine Daten-Regeln definiert'); + case 'UI': + return t('Keine UI-Regeln definiert'); + case 'RESOURCE': + return t('Keine Ressourcen-Regeln definiert'); } }; @@ -384,7 +395,7 @@ const RulesSection: React.FC = ({ {!readOnly && !showAddForm && (
- {rules.length} {rules.length === 1 ? 'Regel' : 'Regeln'} + {rules.length} {rules.length === 1 ? t('Regel') : t('Regeln')}
{/* View Toggle */} @@ -393,14 +404,14 @@ const RulesSection: React.FC = ({ @@ -410,7 +421,7 @@ const RulesSection: React.FC = ({ className={styles.addButton} onClick={() => setShowAddForm(true)} > - Neue Regel + {t('Neue Regel')}
@@ -431,7 +442,7 @@ const RulesSection: React.FC = ({

{getEmptyText()}

{!readOnly && (

- Klicken Sie auf "Neue Regel" um eine Berechtigung hinzuzufügen. + {t('Klicken Sie auf „Neue Regel“, um eine Berechtigung hinzuzufügen.')}

)}
@@ -469,6 +480,7 @@ interface JsonEditorProps { } const JsonEditor: React.FC = ({ rules, readOnly, onApply }) => { + const { t } = useLanguage(); const [jsonText, setJsonText] = useState(''); const [error, setError] = useState(null); @@ -481,7 +493,7 @@ const JsonEditor: React.FC = ({ rules, readOnly, onApply }) => try { const parsed = JSON.parse(jsonText); if (!Array.isArray(parsed)) { - throw new Error('JSON muss ein Array sein'); + throw new Error(t('JSON muss ein Array sein')); } setError(null); onApply(parsed); @@ -501,8 +513,9 @@ const JsonEditor: React.FC = ({ rules, readOnly, onApply }) => /> {error &&
{error}
}

- Experten-Modus: Bearbeiten Sie die Regeln direkt als JSON. - Änderungen werden erst nach Klick auf "Anwenden" übernommen. + {t( + 'Experten-Modus: Bearbeiten Sie die Regeln direkt als JSON. Änderungen werden erst nach Klick auf „Anwenden“ übernommen.' + )}

{!readOnly && (
@@ -512,7 +525,7 @@ const JsonEditor: React.FC = ({ rules, readOnly, onApply }) => onClick={handleApply} disabled={!!error} > - JSON anwenden + {t('JSON anwenden')}
)} @@ -607,7 +620,7 @@ export const AccessRulesEditor: React.FC = ({ setHasChanges(false); onSave?.(); } else { - showError('Fehler', result.error || 'Fehler beim Speichern'); + showError(t('Fehler'), result.error || t('Fehler beim Speichern')); } }; @@ -655,7 +668,7 @@ export const AccessRulesEditor: React.FC = ({

Berechtigungen{roleName ? `: ${roleName}` : ''} - {isTemplate && Template} + {isTemplate && {t('Vorlage')}}

{!readOnly && hasChanges && (
diff --git a/src/components/AccessRules/AccessRulesTable.tsx b/src/components/AccessRules/AccessRulesTable.tsx index be88bc2..61c713e 100644 --- a/src/components/AccessRules/AccessRulesTable.tsx +++ b/src/components/AccessRules/AccessRulesTable.tsx @@ -77,6 +77,8 @@ const AccessRuleRow: React.FC = ({ onDelete, }) => { const { t } = useLanguage(); + const opTitle = (op: 'create' | 'read' | 'update' | 'delete') => + ({ create: t('Erstellen'), read: t('Lesen'), update: t('Bearbeiten'), delete: t('Löschen') })[op]; const handleLevelToggle = ( field: 'read' | 'create' | 'update' | 'delete', targetLevel: 'm' | 'g' | 'a', @@ -112,7 +114,7 @@ const AccessRuleRow: React.FC = ({ checked={rule.view} onChange={(e) => onUpdate(rule.id, { view: e.target.checked })} disabled={readOnly} - title="Sichtbar" + title={t('Sichtbar')} /> @@ -127,7 +129,7 @@ const AccessRuleRow: React.FC = ({ checked={hasLevel(rule[op] as AccessLevel, 'm')} onChange={(e) => handleLevelToggle(op, 'm', e.target.checked)} disabled={readOnly} - title={`${op.charAt(0).toUpperCase() + op.slice(1)} - Eigene`} + title={`${opTitle(op)} - ${t('Eigene')}`} /> ))} @@ -140,7 +142,7 @@ const AccessRuleRow: React.FC = ({ checked={hasLevel(rule[op] as AccessLevel, 'g')} onChange={(e) => handleLevelToggle(op, 'g', e.target.checked)} disabled={readOnly} - title={`${op.charAt(0).toUpperCase() + op.slice(1)} - Gruppe`} + title={`${opTitle(op)} - ${t('Gruppe')}`} /> ))} @@ -153,7 +155,7 @@ const AccessRuleRow: React.FC = ({ checked={hasLevel(rule[op] as AccessLevel, 'a')} onChange={(e) => handleLevelToggle(op, 'a', e.target.checked)} disabled={readOnly} - title={`${op.charAt(0).toUpperCase() + op.slice(1)} - Alle`} + title={`${opTitle(op)} - ${t('Alle')}`} /> ))} @@ -166,7 +168,7 @@ const AccessRuleRow: React.FC = ({ @@ -200,7 +202,7 @@ export const AccessRulesTable: React.FC = ({ {t('object dot notation')} - View + {t('Ansicht')} {isDataContext && ( <> {t('own')} @@ -214,18 +216,18 @@ export const AccessRulesTable: React.FC = ({ - C - R - U - D - C - R - U - D - C - R - U - D + C + R + U + D + C + R + U + D + C + R + U + D )} diff --git a/src/components/Chat/ChatInput.tsx b/src/components/Chat/ChatInput.tsx index 1ca44e6..c9efb60 100644 --- a/src/components/Chat/ChatInput.tsx +++ b/src/components/Chat/ChatInput.tsx @@ -4,6 +4,7 @@ * Simple text input with send button, usable by both Workspace and Editor. */ import React, { useState, useCallback, useRef, useEffect } from 'react'; +import { useLanguage } from '../../providers/language/LanguageContext'; interface ChatInputProps { onSend: (message: string) => void; @@ -17,11 +18,13 @@ interface ChatInputProps { export const ChatInput: React.FC = ({ onSend, isProcessing, - placeholder = 'Type a message...', + placeholder, disabled, autoFocus = true, style, }) => { + const { t } = useLanguage(); + const resolvedPlaceholder = placeholder ?? t('Nachricht eingeben…'); const [value, setValue] = useState(''); const inputRef = useRef(null); @@ -62,7 +65,7 @@ export const ChatInput: React.FC = ({ value={value} onChange={(e) => setValue(e.target.value)} onKeyDown={_handleKeyDown} - placeholder={placeholder} + placeholder={resolvedPlaceholder} disabled={isProcessing || disabled} rows={1} style={{ @@ -95,7 +98,7 @@ export const ChatInput: React.FC = ({ whiteSpace: 'nowrap', }} > - {isProcessing ? '...' : 'Send'} + {isProcessing ? '…' : t('Senden')}
); diff --git a/src/components/Chat/ChatMessageList.tsx b/src/components/Chat/ChatMessageList.tsx index 4dc15b9..2234934 100644 --- a/src/components/Chat/ChatMessageList.tsx +++ b/src/components/Chat/ChatMessageList.tsx @@ -7,6 +7,7 @@ import React, { useRef, useEffect } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; +import { useLanguage } from '../../providers/language/LanguageContext'; export interface ChatMessage { id: string; @@ -31,9 +32,11 @@ const _roleColors: Record = { export const ChatMessageList: React.FC = ({ messages, isProcessing, - emptyMessage = 'No messages yet.', + emptyMessage, style, }) => { + const { t } = useLanguage(); + const resolvedEmpty = emptyMessage ?? t('Noch keine Nachrichten.'); const bottomRef = useRef(null); useEffect(() => { @@ -55,7 +58,7 @@ export const ChatMessageList: React.FC = ({ > {messages.length === 0 && (
- {emptyMessage} + {resolvedEmpty}
)} {messages.map((msg) => ( @@ -80,7 +83,7 @@ export const ChatMessageList: React.FC = ({ ))} {isProcessing && (
- Processing... + {t('Wird verarbeitet…')}
)}
diff --git a/src/components/ContentPreview/ContentPreview.tsx b/src/components/ContentPreview/ContentPreview.tsx index 648ca75..2ee4d82 100644 --- a/src/components/ContentPreview/ContentPreview.tsx +++ b/src/components/ContentPreview/ContentPreview.tsx @@ -64,7 +64,7 @@ export function ContentPreview({ setError(t('Ungültige Datei-ID')); return; } - if (!fileName || fileName === 'Unknown Item') { + if (!fileName || fileName === 'Unknown Item' || fileName === 'Unbekanntes Element') { setError(t('Dateiname nicht verfügbar')); return; } @@ -77,7 +77,7 @@ export function ContentPreview({ } setError(null); } - }, [isOpen, fileId, fileName]); + }, [isOpen, fileId, fileName, t]); const loadPreview = async () => { @@ -95,7 +95,7 @@ export function ContentPreview({ } // If it's text content but MIME type says PDF, we'll handle it in renderPreview } else { - setError(result.error || 'Failed to load preview'); + setError(result.error || t('Vorschau konnte nicht geladen werden.')); } } catch (err) { setError(t('Ein unerwarteter Fehler ist aufgetreten, während')); @@ -201,7 +201,7 @@ export function ContentPreview({
             
-              {previewContent || 'No content available'}
+              {previewContent || t('Kein Inhalt verfügbar')}
             
           
diff --git a/src/components/ContentPreview/UrlContentPreview.tsx b/src/components/ContentPreview/UrlContentPreview.tsx index 45e1fe3..8648275 100644 --- a/src/components/ContentPreview/UrlContentPreview.tsx +++ b/src/components/ContentPreview/UrlContentPreview.tsx @@ -96,7 +96,9 @@ export function UrlContentPreview({ const warningTimeout = setTimeout(() => { if (isLoading && !hasLoaded) { - setWarning('PDF lädt langsam. Sie können es auch direkt herunterladen oder in einem neuen Tab öffnen.'); + setWarning( + t('PDF lädt langsam. Sie können es auch direkt herunterladen oder in einem neuen Tab öffnen.') + ); // Don't set isLoading to false - let it continue } }, WARNING_TIMEOUT); @@ -107,7 +109,7 @@ export function UrlContentPreview({ console.log('PDF loading timeout, switching to PDF.js fallback'); setUsePdfJs(true); setIsLoading(true); // Restart loading with PDF.js - setWarning('PDF lädt langsam. Versuche alternative Anzeigemethode...'); + setWarning(t('PDF lädt langsam. Alternative Anzeigemethode wird versucht…')); } else if (isLoading && !hasLoaded && usePdfJs) { // PDF.js also failed, show error setShowPdfAnyway(true); @@ -121,7 +123,7 @@ export function UrlContentPreview({ clearTimeout(errorTimeout); }; } - }, [isOpen, isLoading, hasLoaded, usePdfJs]); + }, [isOpen, isLoading, hasLoaded, usePdfJs, t]); // Validate URL useEffect(() => { @@ -184,7 +186,7 @@ export function UrlContentPreview({ padding: '0.5rem 1rem' }} > - In neuem Tab öffnen + {t('In neuem Tab öffnen')}
@@ -241,7 +243,7 @@ export function UrlContentPreview({ fontWeight: '500' }} > - In neuem Tab öffnen + {t('In neuem Tab öffnen')} @@ -284,7 +286,7 @@ export function UrlContentPreview({ fontWeight: '500' }} > - In neuem Tab öffnen + {t('In neuem Tab öffnen')} @@ -316,7 +318,7 @@ export function UrlContentPreview({
{fileName}

{t('Vorschau wird hierfür nicht unterstützt')}

); diff --git a/src/components/ContentPreview/renderers/JsonRenderer.tsx b/src/components/ContentPreview/renderers/JsonRenderer.tsx index e268756..783789a 100644 --- a/src/components/ContentPreview/renderers/JsonRenderer.tsx +++ b/src/components/ContentPreview/renderers/JsonRenderer.tsx @@ -303,7 +303,7 @@ export function JsonRenderer({ previewContent, fileName }: JsonRendererProps) { @@ -479,7 +479,7 @@ export function JsonRenderer({ previewContent, fileName }: JsonRendererProps) { ); } catch (parseError) { const rawData = { - keys: ['Raw Content'], + keys: [t('Rohinhalt')], values: [previewContent], types: ['string'], isNested: [false] diff --git a/src/components/ContentPreview/renderers/PdfJsRenderer.tsx b/src/components/ContentPreview/renderers/PdfJsRenderer.tsx index fea4d48..2042f3a 100644 --- a/src/components/ContentPreview/renderers/PdfJsRenderer.tsx +++ b/src/components/ContentPreview/renderers/PdfJsRenderer.tsx @@ -66,7 +66,7 @@ export function PdfJsRenderer({ } catch (err) { console.error('Error loading PDF with PDF.js:', err); if (isMounted) { - setError(err instanceof Error ? err.message : 'Failed to load PDF'); + setError(err instanceof Error ? err.message : t('PDF konnte nicht geladen werden.')); setIsLoading(false); onError(); } @@ -116,7 +116,7 @@ export function PdfJsRenderer({ } catch (err) { console.error('Error rendering PDF page:', err); if (isMounted) { - setError(err instanceof Error ? err.message : 'Failed to render PDF page'); + setError(err instanceof Error ? err.message : t('PDF-Seite konnte nicht gerendert werden.')); } } }; @@ -132,7 +132,9 @@ export function PdfJsRenderer({ return (
⚠️
-

Fehler beim Laden der PDF: {error}

+

+ {t('Fehler beim Laden der PDF:')} {error} +

); } diff --git a/src/components/FlowEditor/editor/Automation2FlowEditor.tsx b/src/components/FlowEditor/editor/Automation2FlowEditor.tsx index b9a6474..de4a245 100644 --- a/src/components/FlowEditor/editor/Automation2FlowEditor.tsx +++ b/src/components/FlowEditor/editor/Automation2FlowEditor.tsx @@ -57,8 +57,8 @@ import { useLanguage } from '../../../providers/language/LanguageContext'; const LOG = '[Automation2]'; -const DEFAULT_INVOCATIONS = (): WorkflowEntryPoint[] => - buildInvocationsForPrimaryKind('manual', [], 'Jetzt ausführen'); +const _buildDefaultInvocations = (runLabel: string): WorkflowEntryPoint[] => + buildInvocationsForPrimaryKind('manual', [], runLabel); interface Automation2FlowEditorProps { instanceId: string; @@ -106,7 +106,9 @@ export const Automation2FlowEditor: React.FC = ({ in const [currentWorkflowId, setCurrentWorkflowId] = useState(null); const [selectedNode, setSelectedNode] = useState(null); const [saving, setSaving] = useState(false); - const [invocations, setInvocations] = useState(DEFAULT_INVOCATIONS); + const [invocations, setInvocations] = useState(() => + _buildDefaultInvocations(t('Jetzt ausführen')) + ); const [workflowSettingsOpen, setWorkflowSettingsOpen] = useState(false); const [leftPanelOpen, setLeftPanelOpen] = useState(true); const [tracingRunId, setTracingRunId] = useState(null); @@ -176,7 +178,7 @@ export const Automation2FlowEditor: React.FC = ({ in const applyGraphWithSync = useCallback( (graph: Automation2Graph | null | undefined, wfInvocations: WorkflowEntryPoint[] | undefined) => { - const inv = wfInvocations?.length ? wfInvocations : DEFAULT_INVOCATIONS(); + const inv = wfInvocations?.length ? wfInvocations : _buildDefaultInvocations(t('Jetzt ausführen')); setInvocations(inv); if (!graph?.nodes?.length) { const synced = syncCanvasStartNode([], [], inv, nodeTypes, language); @@ -189,7 +191,7 @@ export const Automation2FlowEditor: React.FC = ({ in setCanvasNodes(synced.nodes); setCanvasConnections(synced.connections); }, - [nodeTypes, language] + [nodeTypes, language, t] ); const handleFromApiGraph = useCallback( @@ -202,7 +204,7 @@ export const Automation2FlowEditor: React.FC = ({ in const handleExecute = useCallback(async () => { const graph = toApiGraph(canvasNodes, canvasConnections); if (graph.nodes.length === 0) { - setExecuteResult({ success: false, error: 'Keine Nodes im Workflow.' }); + setExecuteResult({ success: false, error: t('Keine Nodes im Workflow.') }); return; } setExecuting(true); @@ -222,12 +224,12 @@ export const Automation2FlowEditor: React.FC = ({ in } finally { setExecuting(false); } - }, [request, instanceId, canvasNodes, canvasConnections, currentWorkflowId, invocations]); + }, [request, instanceId, canvasNodes, canvasConnections, currentWorkflowId, invocations, t]); const handleSave = useCallback(async () => { const graph = toApiGraph(canvasNodes, canvasConnections); if (graph.nodes.length === 0) { - setExecuteResult({ success: false, error: 'Keine Nodes zum Speichern.' }); + setExecuteResult({ success: false, error: t('Keine Nodes zum Speichern.') }); return; } setSaving(true); @@ -236,17 +238,17 @@ export const Automation2FlowEditor: React.FC = ({ in await updateWorkflow(request, instanceId, currentWorkflowId, { graph, invocations }); setExecuteResult({ success: true } as ExecuteGraphResponse); } else { - const label = await promptInput('Workflow-Name:', { + const label = await promptInput(t('Workflow-Name:'), { title: t('Workflow speichern'), - defaultValue: 'Neuer Workflow', - placeholder: 'Name des Workflows', + defaultValue: t('Neuer Workflow'), + placeholder: t('Name des Workflows'), }); if (!label) { setSaving(false); return; } const created = await createWorkflow(request, instanceId, { - label: label.trim() || 'Neuer Workflow', + label: label.trim() || t('Neuer Workflow'), graph, invocations, }); @@ -260,7 +262,7 @@ export const Automation2FlowEditor: React.FC = ({ in } finally { setSaving(false); } - }, [request, instanceId, canvasNodes, canvasConnections, currentWorkflowId, promptInput, invocations]); + }, [request, instanceId, canvasNodes, canvasConnections, currentWorkflowId, promptInput, invocations, t]); const handleLoad = useCallback( async (workflowId: string) => { @@ -287,17 +289,17 @@ export const Automation2FlowEditor: React.FC = ({ in if (workflowId) handleLoad(workflowId); else { setExecuteResult(null); - applyGraphWithSync({ nodes: [], connections: [] }, DEFAULT_INVOCATIONS()); + applyGraphWithSync({ nodes: [], connections: [] }, _buildDefaultInvocations(t('Jetzt ausführen'))); } }, - [handleLoad, applyGraphWithSync] + [handleLoad, applyGraphWithSync, t] ); const handleNew = useCallback(() => { setCurrentWorkflowId(null); setExecuteResult(null); - applyGraphWithSync({ nodes: [], connections: [] }, DEFAULT_INVOCATIONS()); - }, [applyGraphWithSync]); + applyGraphWithSync({ nodes: [], connections: [] }, _buildDefaultInvocations(t('Jetzt ausführen'))); + }, [applyGraphWithSync, t]); const handleNodeParametersChange = useCallback((nodeId: string, parameters: Record) => { setCanvasNodes((prev) => @@ -401,7 +403,7 @@ export const Automation2FlowEditor: React.FC = ({ in if (loading || nodeTypes.length === 0) return; if (currentWorkflowId || initialWorkflowId) return; if (canvasNodes.length > 0) return; - applyGraphWithSync({ nodes: [], connections: [] }, DEFAULT_INVOCATIONS()); + applyGraphWithSync({ nodes: [], connections: [] }, _buildDefaultInvocations(t('Jetzt ausführen'))); }, [ loading, nodeTypes.length, @@ -409,6 +411,7 @@ export const Automation2FlowEditor: React.FC = ({ in initialWorkflowId, canvasNodes.length, applyGraphWithSync, + t, ]); const toggleCategory = useCallback((id: string) => { @@ -591,7 +594,7 @@ export const Automation2FlowEditor: React.FC = ({ in return (
-

Nodes

+

{t('Knoten')}

@@ -604,12 +607,12 @@ export const Automation2FlowEditor: React.FC = ({ in return (
-

Nodes

+

{t('Knoten')}

{error}

@@ -648,7 +651,7 @@ export const Automation2FlowEditor: React.FC = ({ in className={`${styles.rightTab} ${udbTab === tab ? styles.rightTabActive : ''}`} onClick={() => setUdbTab(tab)} > - {{ chats: 'Chats', files: 'Dateien', sources: 'Quellen' }[tab]} + {{ chats: t('Chats'), files: t('Dateien'), sources: t('Quellen') }[tab]} ))}
@@ -755,13 +758,13 @@ export const Automation2FlowEditor: React.FC = ({ in className={`${styles.rightTab} ${rightTab === 'nodes' ? styles.rightTabActive : ''}`} onClick={() => setRightTab('nodes')} > - Nodes + {t('Knoten')}
diff --git a/src/components/FlowEditor/editor/CanvasHeader.tsx b/src/components/FlowEditor/editor/CanvasHeader.tsx index 9696106..a7ea722 100644 --- a/src/components/FlowEditor/editor/CanvasHeader.tsx +++ b/src/components/FlowEditor/editor/CanvasHeader.tsx @@ -2,7 +2,7 @@ * CanvasHeader - Workflow controls (Neu, Speichern, laden, Ausführen), version selector, and execute result. */ -import React, { useState, useRef, useEffect, useCallback } from 'react'; +import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react'; import { FaCog, FaPlay, FaSpinner, FaCloudUploadAlt, FaCloudDownloadAlt, FaArchive, FaDatabase, FaBookmark, FaCaretDown } from 'react-icons/fa'; import type { Automation2Workflow, ExecuteGraphResponse, AutoVersion, AutoTemplateScope } from '../../../api/workflowApi'; import styles from './Automation2FlowEditor.module.css'; @@ -118,7 +118,15 @@ export const CanvasHeader: React.FC = ({ workflows, return () => document.removeEventListener('mousedown', _handleClickOutside); }, []); - const SCOPE_LABELS: Record = { user: 'Meine Vorlagen', instance: 'Instanz', mandate: 'Mandant' }; + const scopeLabels = useMemo( + () => + ({ + user: t('Meine Vorlagen'), + instance: t('Instanz'), + mandate: t('Mandant'), + }) as Record, + [t] + ); return (
@@ -139,14 +147,14 @@ export const CanvasHeader: React.FC = ({ workflows, className={styles.canvasTitle} style={{ margin: 0, cursor: onWorkflowRename ? 'pointer' : 'default', fontSize: '0.95rem', fontWeight: 600 }} onClick={_startNameEdit} - title={onWorkflowRename ? 'Klicken zum Umbenennen' : undefined} + title={onWorkflowRename ? t('Klicken zum Umbenennen') : undefined} > {currentWorkflow.label} ) ) : (

- Neuer Workflow + {t('Neuer Workflow')}

)} {onWorkflowSettings && ( @@ -154,7 +162,7 @@ export const CanvasHeader: React.FC = ({ workflows, type="button" className={styles.canvasGearBtn} title={t('Workflowkonfiguration Einstieg/Starts')} - aria-label="Workflow-Konfiguration" + aria-label={t('Workflow-Konfiguration')} onClick={onWorkflowSettings} > @@ -165,7 +173,7 @@ export const CanvasHeader: React.FC = ({ workflows,
{onNewFromTemplate && ( )}
@@ -205,7 +213,7 @@ export const CanvasHeader: React.FC = ({ workflows, onClick={onSave} disabled={saving || !hasNodes} > - {saving ? : 'Speichern'} + {saving ? : t('Speichern')} {/* Save as template */} @@ -229,7 +237,7 @@ export const CanvasHeader: React.FC = ({ workflows, onClick={() => { onSaveAsTemplate(s); setTemplateMenuOpen(false); }} style={{ display: 'block', width: '100%', textAlign: 'left', padding: '8px 12px', border: 'none', background: 'transparent', cursor: 'pointer', fontSize: '0.85rem', borderTop: s !== 'user' ? '1px solid var(--border-color, #e0e0e0)' : undefined }} > - {SCOPE_LABELS[s]} + {scopeLabels[s]} ))}
@@ -260,19 +268,19 @@ export const CanvasHeader: React.FC = ({ workflows, {executing ? ( <> - Ausführen… + {t('Ausführen…')} ) : ( <> - Ausführen + {t('Ausführen')} )} {onToggleChat && ( )}
@@ -280,7 +288,7 @@ export const CanvasHeader: React.FC = ({ workflows, {/* Version Selector */} {currentWorkflowId && versions && versions.length > 0 && (
- Version: + {t('Version:')} = ({ if (!runId) { return (
- Select a run to see tracing details. + {t('Run auswählen, um Tracing-Details zu sehen.')}
); } @@ -180,7 +180,10 @@ export const RunTracingPanel: React.FC = ({ return (
- Run Steps {loading && (loading...)} + {t('Run-Schritte')}{' '} + {loading && ( + ({t('wird geladen…')}) + )}
{steps.length === 0 && !loading && (
{t('Noch keine Schritte aufgezeichnet')}
@@ -223,7 +226,7 @@ export const RunTracingPanel: React.FC = ({ {step.retryCount > 0 && ( - {step.retryCount}x retry + {step.retryCount}x {t('Wiederholung')} )} {step.durationMs != null && ( @@ -244,11 +247,13 @@ export const RunTracingPanel: React.FC = ({
{step.error}
)} {step.tokensUsed > 0 && ( -
{step.tokensUsed} tokens
+
+ {step.tokensUsed} {t('Tokens')} +
)} - - + +
); })} diff --git a/src/components/FlowEditor/editor/TemplatePicker.tsx b/src/components/FlowEditor/editor/TemplatePicker.tsx index 022031d..6cf16ef 100644 --- a/src/components/FlowEditor/editor/TemplatePicker.tsx +++ b/src/components/FlowEditor/editor/TemplatePicker.tsx @@ -2,7 +2,7 @@ * TemplatePicker - modal to browse and select a workflow template for creating a new workflow. */ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { FaSpinner } from 'react-icons/fa'; import { fetchTemplates, @@ -11,14 +11,7 @@ import { type ApiRequestFunction, } from '../../../api/workflowApi'; import styles from './Automation2FlowEditor.module.css'; - -const SCOPE_LABELS: Record = { - all: 'Alle', - user: 'Meine', - instance: 'Instanz', - mandate: 'Mandant', - system: 'System', -}; +import { useLanguage } from '../../../providers/language/LanguageContext'; interface TemplatePickerProps { open: boolean; @@ -35,6 +28,18 @@ export const TemplatePicker: React.FC = ({ instanceId, request, }) => { + const { t } = useLanguage(); + const scopeLabels = useMemo( + () => + ({ + all: t('Alle'), + user: t('Meine'), + instance: t('Instanz'), + mandate: t('Mandant'), + system: t('System'), + }) as Record, + [t] + ); const [templates, setTemplates] = useState([]); const [loading, setLoading] = useState(false); const [activeScope, setActiveScope] = useState('all'); @@ -76,10 +81,10 @@ export const TemplatePicker: React.FC = ({

- Neu aus Vorlage + {t('Neu aus Vorlage')}

- Wählen Sie eine Vorlage, um einen neuen Workflow zu erstellen. + {t('Wählen Sie eine Vorlage, um einen neuen Workflow zu erstellen.')}

@@ -91,7 +96,7 @@ export const TemplatePicker: React.FC = ({ onClick={() => setActiveScope(s)} style={{ fontSize: '0.8rem', padding: '4px 10px' }} > - {SCOPE_LABELS[s]} + {scopeLabels[s]} ))}
@@ -103,14 +108,14 @@ export const TemplatePicker: React.FC = ({
) : templates.length === 0 ? (
- Keine Vorlagen gefunden. + {t('Keine Vorlagen gefunden.')}
) : ( - - + + @@ -119,7 +124,7 @@ export const TemplatePicker: React.FC = ({ @@ -141,7 +146,7 @@ export const TemplatePicker: React.FC = ({
diff --git a/src/components/FlowEditor/editor/WorkflowConfigurationModal.tsx b/src/components/FlowEditor/editor/WorkflowConfigurationModal.tsx index d77ebb3..b615961 100644 --- a/src/components/FlowEditor/editor/WorkflowConfigurationModal.tsx +++ b/src/components/FlowEditor/editor/WorkflowConfigurationModal.tsx @@ -64,7 +64,7 @@ export const WorkflowConfigurationModal: React.FC { e.preventDefault(); const label = - titleDe.trim() || kindOptions.find((o) => o.value === kind)?.label || 'Start'; + titleDe.trim() || kindOptions.find((o) => o.value === kind)?.label || t('Start'); const next = buildInvocationsForPrimaryKind(kind, invocations, label); onApply(next); onClose(); @@ -74,15 +74,16 @@ export const WorkflowConfigurationModal: React.FC

- Workflow-Konfiguration + {t('Workflow-Konfiguration')}

- Legen Sie fest, wie dieser Workflow gestartet werden soll. Die Start-Node im Editor passt sich dem - gewählten Einstieg an (z. B. Formular-Felder auf der Start-Node bearbeiten). + {t( + 'Legen Sie fest, wie dieser Workflow gestartet werden soll. Die Start-Node im Editor passt sich dem gewählten Einstieg an (z. B. Formular-Felder auf der Start-Node bearbeiten).' + )}

-
+
{kindOptions.map((o) => (
diff --git a/src/components/FlowEditor/nodes/form/FormNodeConfig.tsx b/src/components/FlowEditor/nodes/form/FormNodeConfig.tsx index 8d8c3ae..3bb88f5 100644 --- a/src/components/FlowEditor/nodes/form/FormNodeConfig.tsx +++ b/src/components/FlowEditor/nodes/form/FormNodeConfig.tsx @@ -57,7 +57,7 @@ export const FormNodeConfig: React.FC = ({ params, return (
- +
{fields.map((f, i) => (
= ({ params,
{ const next = [...fields]; @@ -96,7 +96,7 @@ export const FormNodeConfig: React.FC = ({ params, }} /> { const next = [...fields]; @@ -111,13 +111,13 @@ export const FormNodeConfig: React.FC = ({ params, value={f.type ?? 'string'} onChange={(e) => { const next = [...fields]; - const t = e.target.value; + const fieldType = e.target.value; next[i] = { ...next[i], - type: t, - ...(t === 'clickup_tasks' + type: fieldType, + ...(fieldType === 'clickup_tasks' ? { clickupStatusOptions: undefined } - : t === 'clickup_status' + : fieldType === 'clickup_status' ? { clickupConnectionId: undefined, clickupListId: undefined } : { clickupConnectionId: undefined, @@ -129,10 +129,10 @@ export const FormNodeConfig: React.FC = ({ params, }} style={{ width: 'auto', minWidth: 90 }} > - - - - + + + + @@ -146,7 +146,7 @@ export const FormNodeConfig: React.FC = ({ params, updateParam('fields', next); }} /> - Pflichtfeld + {t('Pflichtfeld')}
diff --git a/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx b/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx index d54173e..f966f32 100644 --- a/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx +++ b/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx @@ -180,6 +180,7 @@ const ConnectionPicker: React.FC = ({ param, value, onChange }; const FolderPicker: React.FC = ({ param, value, onChange, allParams }) => { + const { t } = useLanguage(); const dependsOn = param.frontendOptions?.dependsOn as string | undefined; const depValue = dependsOn ? allParams?.[dependsOn] : undefined; const disabled = dependsOn && !depValue; @@ -191,7 +192,7 @@ const FolderPicker: React.FC = ({ param, value, onChange, al value={typeof value === 'string' ? value : ''} onChange={(e) => onChange(e.target.value)} disabled={!!disabled} - placeholder={disabled ? `Select ${dependsOn} first` : param.name} + placeholder={disabled ? t('Zuerst {field} wählen', { field: dependsOn ?? '' }) : param.name} style={{ width: '100%', padding: '4px 8px', borderRadius: 4, border: '1px solid #ccc', opacity: disabled ? 0.5 : 1 }} />
@@ -214,9 +215,9 @@ const CaseListEditor: React.FC = ({ param, value, onChange } {cases.map((c: Record, i: number) => (
@@ -244,18 +245,18 @@ const FieldBuilderEditor: React.FC = ({ param, value, onChan {fields.map((f: Record, i: number) => (
- updateField(i, 'name', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} /> + updateField(i, 'name', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} /> - updateField(i, 'label', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} /> + updateField(i, 'label', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
@@ -280,8 +281,8 @@ const KeyValueRowsEditor: React.FC = ({ param, value, onChan {rows.map((r: Record, i: number) => (
- updateRow(i, 'key', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} /> - updateRow(i, 'value', e.target.value)} style={{ flex: 2, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} /> + updateRow(i, 'key', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} /> + updateRow(i, 'value', e.target.value)} style={{ flex: 2, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
))} @@ -299,7 +300,7 @@ const CronBuilder: React.FC = ({ param, value, onChange }) = type="text" value={typeof value === 'string' ? value : ''} onChange={(e) => onChange(e.target.value)} - placeholder={t('index.5')} + placeholder={t('0 9 * * *')} style={{ width: '100%', padding: '4px 8px', borderRadius: 4, border: '1px solid #ccc', fontFamily: 'monospace' }} />

{t('Cron: Min Stunde Tag Monat')}

@@ -316,17 +317,17 @@ const ConditionBuilder: React.FC = ({ param, value, onChange
- update('value', e.target.value)} style={{ flex: 2, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} /> + update('value', e.target.value)} style={{ flex: 2, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
); @@ -366,18 +367,18 @@ const FilterExpressionEditor: React.FC = ({ param, value, on
- update('field', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} /> + update('field', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} /> - update('value', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} /> + update('value', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
); diff --git a/src/components/FlowEditor/nodes/ifElse/IfElseNodeConfig.tsx b/src/components/FlowEditor/nodes/ifElse/IfElseNodeConfig.tsx index a19d1fd..0cb2e52 100644 --- a/src/components/FlowEditor/nodes/ifElse/IfElseNodeConfig.tsx +++ b/src/components/FlowEditor/nodes/ifElse/IfElseNodeConfig.tsx @@ -99,7 +99,7 @@ export const IfElseNodeConfig: React.FC = ({ params, up return (
- +
@@ -114,7 +114,7 @@ export const IfElseNodeConfig: React.FC = ({ params, up
{needsValue && (
- + {mimeTypeOptions.length > 0 ? (

- Z.B. für jedes Formularfeld, jede Datei aus Upload, jede E-Mail aus Suche. + {t('Z.B. für jedes Formularfeld, jede Datei aus Upload, jede E-Mail aus Suche.')}

); diff --git a/src/components/FlowEditor/nodes/shared/RefSourceSelect.tsx b/src/components/FlowEditor/nodes/shared/RefSourceSelect.tsx index 3301b8d..62e8dc1 100644 --- a/src/components/FlowEditor/nodes/shared/RefSourceSelect.tsx +++ b/src/components/FlowEditor/nodes/shared/RefSourceSelect.tsx @@ -6,6 +6,7 @@ import React from 'react'; import { createRef, isRef, isValue, createValue, type DataRef } from './dataRef'; import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext'; +import { useLanguage } from '../../../../providers/language/LanguageContext'; /** How to build path options for StatischKontextSelect / RefSourceSelect. */ export type PathPickMode = 'default' | 'clickup_task_id' | 'exclude_forms'; @@ -131,6 +132,11 @@ export function refToOptionValue(ref: DataRef): string { return JSON.stringify(ref); } +function _pathLabelForDisplay(pathLabel: string, translate: (key: string) => string): string { + if (pathLabel === 'Aufgaben-ID') return translate('Aufgaben-ID'); + return pathLabel; +} + export function optionValueToRef(s: string): DataRef | null { try { const o = JSON.parse(s) as unknown; @@ -190,10 +196,11 @@ interface StatischKontextSelectProps { export const StatischKontextSelect: React.FC = ({ value, onChange, - placeholder = '— Quelle wählen —', - staticLabel = 'Statisch', + placeholder, + staticLabel, pathPickMode = 'default', }) => { + const { t } = useLanguage(); const dataFlow = useAutomation2DataFlow(); if (!dataFlow) return null; @@ -213,7 +220,8 @@ export const StatischKontextSelect: React.FC = ({ const nodeLabel = node ? dataFlow.getNodeLabel(node) : nodeId; const paths = pickPathsForNode(node, preview, pathPickMode); for (const p of paths) { - const displayLabel = p.pathLabel ? `${nodeLabel} → ${p.pathLabel}` : nodeLabel; + const pathLabelUi = _pathLabelForDisplay(p.pathLabel, t); + const displayLabel = pathLabelUi ? `${nodeLabel} → ${pathLabelUi}` : nodeLabel; options.push({ ref: createRef(nodeId, p.path), label: displayLabel, @@ -245,8 +253,8 @@ export const StatischKontextSelect: React.FC = ({ if (ref) onChange(ref); }} > - - + + {options.map((o) => (
diff --git a/src/components/NotificationBell/NotificationBell.tsx b/src/components/NotificationBell/NotificationBell.tsx index fe61219..571f4a9 100644 --- a/src/components/NotificationBell/NotificationBell.tsx +++ b/src/components/NotificationBell/NotificationBell.tsx @@ -20,32 +20,32 @@ const typeIcons: Record = { mention: }; -// Format timestamp to relative time (Unix seconds) -function formatRelativeTime(timestamp: number): string { - if (!Number.isFinite(timestamp) || timestamp <= 0) { - return ''; - } - const now = Date.now() / 1000; - const diff = now - timestamp; - - if (diff < 60) return 'Gerade eben'; - if (diff < 3600) return `vor ${Math.floor(diff / 60)} Min.`; - if (diff < 86400) return `vor ${Math.floor(diff / 3600)} Std.`; - if (diff < 604800) return `vor ${Math.floor(diff / 86400)} Tagen`; - - const date = new Date(timestamp * 1000); - if (Number.isNaN(date.getTime())) { - return ''; - } - return date.toLocaleDateString('de-DE'); -} - interface NotificationBellProps { className?: string; } export const NotificationBell: React.FC = ({ className }) => { const { t } = useLanguage(); + + const formatRelativeTime = useCallback((timestamp: number): string => { + if (!Number.isFinite(timestamp) || timestamp <= 0) { + return ''; + } + const now = Date.now() / 1000; + const diff = now - timestamp; + + if (diff < 60) return t('Gerade eben'); + if (diff < 3600) return t('vor {minutes} Min.', { minutes: String(Math.floor(diff / 60)) }); + if (diff < 86400) return t('vor {hours} Std.', { hours: String(Math.floor(diff / 3600)) }); + if (diff < 604800) return t('vor {days} Tagen', { days: String(Math.floor(diff / 86400)) }); + + const date = new Date(timestamp * 1000); + if (Number.isNaN(date.getTime())) { + return ''; + } + return date.toLocaleDateString('de-DE'); + }, [t]); + const { notifications, unreadCount, @@ -144,7 +144,7 @@ export const NotificationBell: React.FC = ({ className }) )}
@@ -201,7 +201,7 @@ export const NotificationBell: React.FC = ({ className }) {actionSuccess === notification.id && (
- {notification.actionResult || 'Erfolgreich'} + {notification.actionResult || t('Erfolgreich')}
)} diff --git a/src/components/OnboardingAssistant.tsx b/src/components/OnboardingAssistant.tsx index 489d45a..717cc79 100644 --- a/src/components/OnboardingAssistant.tsx +++ b/src/components/OnboardingAssistant.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import api from '../api'; import OnboardingWizard from './OnboardingWizard'; @@ -19,13 +19,6 @@ interface OnboardingAssistantProps { const _STORAGE_KEY = 'onboarding_hidden'; -const _CALLOUTS: Record = { - mandate: 'Tipp: Ein Mandant ist Ihr Arbeitsbereich. Sie koennen spaeter weitere Mandanten fuer Teams oder Projekte erstellen.', - feature: 'Tipp: Im Store finden Sie AI-Workspace, CommCoach und weitere Features. Aktivieren Sie mindestens eines, um loszulegen.', - connection: 'Tipp: Verbinden Sie Ihre Datenquellen (z.B. SharePoint, Google Drive), damit der AI-Assistent auf Ihre Dokumente zugreifen kann.', - chat: 'Tipp: Starten Sie einen Chat mit dem AI-Assistenten. Er kann Ihre verbundenen Daten analysieren und Fragen beantworten.', -}; - export function _isOnboardingHidden(): boolean { try { return localStorage.getItem(_STORAGE_KEY) === 'true'; @@ -48,6 +41,12 @@ function _hideOnboarding(): void { const OnboardingAssistant: React.FC = ({ onDismiss }) => { const { t } = useLanguage(); + const callouts = useMemo(() => ({ + mandate: t('Tipp: Ein Mandant ist Ihr Arbeitsbereich. Sie koennen spaeter weitere Mandanten fuer Teams oder Projekte erstellen.'), + feature: t('Tipp: Im Store finden Sie AI-Workspace, CommCoach und weitere Features. Aktivieren Sie mindestens eines, um loszulegen.'), + connection: t('Tipp: Verbinden Sie Ihre Datenquellen (z.B. SharePoint, Google Drive), damit der AI-Assistent auf Ihre Dokumente zugreifen kann.'), + chat: t('Tipp: Starten Sie einen Chat mit dem AI-Assistenten. Er kann Ihre verbundenen Daten analysieren und Fragen beantworten.'), + }), [t]); const navigate = useNavigate(); const location = useLocation(); const [hidden, setHidden] = useState(() => _isOnboardingHidden()); @@ -99,7 +98,7 @@ const OnboardingAssistant: React.FC = ({ onDismiss }) id: 'mandate', label: t('Mandat einrichten'), description: hasAdminMandate - ? 'Dein Mandant ist eingerichtet.' + ? t('Dein Mandant ist eingerichtet.') : hasFeature ? t('Du bist Mitglied eines Mandanten') : t('Erstelle deinen Arbeitsbereich'), @@ -166,7 +165,7 @@ const OnboardingAssistant: React.FC = ({ onDismiss }) } finally { setLoading(false); } - }, [navigate]); + }, [navigate, t]); useEffect(() => { const state = location.state as { showOnboarding?: number } | null; @@ -215,7 +214,7 @@ const OnboardingAssistant: React.FC = ({ onDismiss })

{t('Willkommen bei Poweron')}

- {completedCount} von {steps.length} Schritten abgeschlossen + {t('{completed} von {total} Schritten abgeschlossen', { completed: String(completedCount), total: String(steps.length) })}

@@ -263,14 +262,14 @@ const OnboardingAssistant: React.FC = ({ onDismiss }) {'\u2192'} )}
- {isNextStep && _CALLOUTS[step.id] && ( + {isNextStep && callouts[step.id as keyof typeof callouts] && (
- {_CALLOUTS[step.id]} + {callouts[step.id as keyof typeof callouts]}
)} @@ -290,7 +289,7 @@ const OnboardingAssistant: React.FC = ({ onDismiss }) onChange={(e) => setDontShowAgain(e.target.checked)} style={{ margin: 0 }} /> - Nicht wieder anzeigen + {t('Nicht wieder anzeigen')} diff --git a/src/components/OnboardingWizard.tsx b/src/components/OnboardingWizard.tsx index 5a5b947..deb9963 100644 --- a/src/components/OnboardingWizard.tsx +++ b/src/components/OnboardingWizard.tsx @@ -30,7 +30,7 @@ const OnboardingWizard: React.FC = ({ onComplete, onDismi window.dispatchEvent(new CustomEvent('features-changed')); onComplete(); } catch (err: any) { - setError(err?.response?.data?.detail || 'Fehler bei der Einrichtung'); + setError(err?.response?.data?.detail || t('Fehler bei der Einrichtung')); } finally { setLoading(false); } @@ -48,7 +48,7 @@ const OnboardingWizard: React.FC = ({ onComplete, onDismi }}>

{t('Mandant erstellen')}

- Erstelle deinen eigenen Arbeitsbereich mit Abo-Auswahl. + {t('Erstelle deinen eigenen Arbeitsbereich mit Abo-Auswahl.')}

@@ -62,7 +62,7 @@ const OnboardingWizard: React.FC = ({ onComplete, onDismi
{t('Kostenlos testen')}
- 7 Tage gratis, danach flexibel upgraden + {t('7 Tage gratis, danach flexibel upgraden')}
@@ -77,7 +77,7 @@ const OnboardingWizard: React.FC = ({ onComplete, onDismi
{t('Standard monatlich')}
- Team-Workspace mit vollem Funktionsumfang + {t('Team-Workspace mit vollem Funktionsumfang')}
@@ -85,7 +85,7 @@ const OnboardingWizard: React.FC = ({ onComplete, onDismi
= ({ onComplete, onDismi padding: '10px 20px', borderRadius: '6px', border: '1px solid var(--border, #d1d5db)', background: 'transparent', cursor: 'pointer', }}> - Abbrechen + {t('Abbrechen')}
diff --git a/src/components/UiComponents/Log/Log.tsx b/src/components/UiComponents/Log/Log.tsx index a946c40..c3d15db 100644 --- a/src/components/UiComponents/Log/Log.tsx +++ b/src/components/UiComponents/Log/Log.tsx @@ -3,6 +3,7 @@ import { LogProps } from './LogTypes'; import { AutoScroll } from '../AutoScroll'; import { formatUnixTimestamp } from '../../../utils/time'; import styles from './Log.module.css'; +import { useLanguage } from '../../../providers/language/LanguageContext'; // Helper to get status badge class const getStatusBadgeClass = (status?: string | null): string => { @@ -22,11 +23,13 @@ const getStatusBadgeClass = (status?: string | null): string => { const Log: React.FC = ({ className = '', - emptyMessage = 'No log information available', + emptyMessage = 'Keine Log-Informationen verfügbar', dashboardTree, onToggleOperationExpanded, getChildOperations }) => { + const { t } = useLanguage(); + const resolvedEmptyMessage = typeof emptyMessage === 'string' ? t(emptyMessage, emptyMessage) : emptyMessage; const formatLogTimestamp = (timestamp: number): string => { try { const formatted = formatUnixTimestamp(timestamp, undefined, { @@ -87,7 +90,7 @@ const Log: React.FC = ({ } // Use stable operation name (from first log) or fallback to operationId - const operationName = operation.operationName || `Operation ${operationId}`; + const operationName = operation.operationName || `${t('Operation')} ${operationId}`; // Use latest message as status tag (updates with each poll) const latestMessage = operation.latestMessage || ''; const operationStatus = operation.latestStatus || 'running'; @@ -137,7 +140,7 @@ const Log: React.FC = ({ )} @@ -68,14 +70,14 @@ export const OerebSection: React.FC = ({ oereb }) => { {restriction.law_status && ( {restriction.law_status === 'inKraft' || restriction.law_status === 'inForce' - ? 'In Kraft' + ? t('In Kraft') : restriction.law_status} )} {restriction.type && (
- Typ: + {t('Typ:')} {restriction.type}
)} @@ -86,7 +88,7 @@ export const OerebSection: React.FC = ({ oereb }) => { )} {restriction.documents && restriction.documents.length > 0 && ( @@ -105,14 +107,14 @@ export const OerebSection: React.FC = ({ oereb }) => { ) : (
- Keine öffentlich-rechtlichen Beschränkungen gefunden. + {t('Keine öffentlich-rechtlichen Beschränkungen gefunden.')}
)} {oereb.last_updated && (
- Aktualisiert: {new Date(oereb.last_updated).toLocaleString('de-CH')} + {t('Aktualisiert')}: {new Date(oereb.last_updated).toLocaleString('de-CH')}
)} @@ -124,7 +126,7 @@ export const OerebSection: React.FC = ({ oereb }) => { isOpen={isPreviewOpen} onClose={() => setIsPreviewOpen(false)} url={oereb.extract_url} - fileName="ÖREB-Auszug.pdf" + fileName={t('ÖREB-Auszug.pdf')} mimeType="application/pdf" /> )} diff --git a/src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx b/src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx index 226dc98..b055f23 100644 --- a/src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx +++ b/src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx @@ -147,12 +147,12 @@ const ParcelInfoPanel: React.FC = ({ isOpen, } catch (e: any) { setDocsError(prev => ({ ...prev, - [parcelId]: e?.response?.data?.detail || e?.message || 'Fehler beim Laden' + [parcelId]: e?.response?.data?.detail || e?.message || t('Fehler beim Laden') })); } finally { setDocsLoading(prev => ({ ...prev, [parcelId]: false })); } - }, [instanceId]); + }, [instanceId, t]); const runExtraction = useCallback(async ( parcelId: string, @@ -174,12 +174,12 @@ const ParcelInfoPanel: React.FC = ({ isOpen, } catch (e: any) { setExtractError(prev => ({ ...prev, - [parcelId]: e?.response?.data?.detail || e?.message || 'Fehler bei der Extraktion' + [parcelId]: e?.response?.data?.detail || e?.message || t('Fehler bei der Extraktion') })); } finally { setExtractLoading(prev => ({ ...prev, [parcelId]: false })); } - }, [instanceId]); + }, [instanceId, t]); useEffect(() => { if (!isOpen || !instanceId) return; @@ -218,7 +218,7 @@ const ParcelInfoPanel: React.FC = ({ isOpen, className={styles.panel} >
-

Parzellen-Informationen ({parcels.length})

+

{t('Parzellen-Informationen')} ({parcels.length})

@@ -248,7 +248,7 @@ const ParcelInfoPanel: React.FC = ({ isOpen, return [

- Bauzone {bz.bauzone} + {t('Bauzone')} {bz.bauzone} {bz.area_m2 != null && ( — {bz.area_m2.toFixed(2)} m² )} @@ -260,7 +260,7 @@ const ParcelInfoPanel: React.FC = ({ isOpen,

- Parzelle {idx + 1}: {parcelData.parcel.number || parcelData.parcel.id || 'Unbekannt'} + {t('Parzelle')} {idx + 1}: {parcelData.parcel.number || parcelData.parcel.id || t('Unbekannt')}

{onRemoveParcel && (
))} @@ -387,7 +387,7 @@ const ParcelInfoPanel: React.FC = ({ isOpen, ) : ( )} - Inhalt extrahieren (LangGraph) + {t('Inhalt extrahieren (LangGraph)')} )} {extractError[parcelData.parcel.id] && ( @@ -470,7 +470,9 @@ const ParcelInfoPanel: React.FC = ({ isOpen,
Zone: - {parcelData.parcel.zone.length} Zone{parcelData.parcel.zone.length !== 1 ? 'n' : ''} gefunden + {parcelData.parcel.zone.length !== 1 + ? t('{n} Zonen gefunden', { n: String(parcelData.parcel.zone.length) }) + : t('1 Zone gefunden')} {(() => { // Extract zone types from zone array const zoneTypes = parcelData.parcel.zone @@ -509,7 +511,7 @@ const ParcelInfoPanel: React.FC = ({ isOpen, rel="noopener noreferrer" className={styles.link} > - Link öffnen + {t('Link öffnen')}
)} @@ -526,7 +528,7 @@ const ParcelInfoPanel: React.FC = ({ isOpen,

- Parzelle {index + 1}: {parcelData.parcel.number || parcelData.parcel.id || 'Unbekannt'} + {t('Parzelle')} {index + 1}: {parcelData.parcel.number || parcelData.parcel.id || t('Unbekannt')}

{onRemoveParcel && (
))} @@ -653,7 +655,7 @@ const ParcelInfoPanel: React.FC = ({ isOpen, ) : ( )} - Inhalt extrahieren (LangGraph) + {t('Inhalt extrahieren (LangGraph)')} )} {extractError[parcelData.parcel.id] && ( @@ -736,7 +738,9 @@ const ParcelInfoPanel: React.FC = ({ isOpen,
Zone: - {parcelData.parcel.zone.length} Zone{parcelData.parcel.zone.length !== 1 ? 'n' : ''} gefunden + {parcelData.parcel.zone.length !== 1 + ? t('{n} Zonen gefunden', { n: String(parcelData.parcel.zone.length) }) + : t('1 Zone gefunden')} {(() => { const zoneTypes = parcelData.parcel.zone .map((z: any) => { @@ -773,7 +777,7 @@ const ParcelInfoPanel: React.FC = ({ isOpen, rel="noopener noreferrer" className={styles.link} > - Link öffnen + {t('Link öffnen')}
)} diff --git a/src/components/UiComponents/WorkflowStatus/WorkflowStatus.tsx b/src/components/UiComponents/WorkflowStatus/WorkflowStatus.tsx index b400967..e3cd4a0 100644 --- a/src/components/UiComponents/WorkflowStatus/WorkflowStatus.tsx +++ b/src/components/UiComponents/WorkflowStatus/WorkflowStatus.tsx @@ -46,6 +46,17 @@ const WorkflowStatus: React.FC = ({ latestStats }) => { const { t } = useLanguage(); + const statusLabel = (status: WorkflowStatusType | null): string => { + if (!status) return ''; + const map: Record = { + completed: t('Abgeschlossen'), + failed: t('Fehlgeschlagen'), + started: t('Gestartet'), + stopped: t('Gestoppt'), + resumed: t('Fortgesetzt'), + }; + return map[status] ?? status; + }; // Use workflow status and round from API response, fallback to extracting from logs const workflowStatus = useMemo(() => { if (workflowStatusFromApi) { @@ -73,11 +84,11 @@ const WorkflowStatus: React.FC = ({ )} {workflowStatus.status && ( - {workflowStatus.status.charAt(0).toUpperCase() + workflowStatus.status.slice(1)} + {statusLabel(workflowStatus.status)} )} {workflowStatus.round !== null && ( - Round {workflowStatus.round} + {t('Runde {nr}', { nr: String(workflowStatus.round) })} )}

@@ -85,7 +96,7 @@ const WorkflowStatus: React.FC = ({ {latestStats && latestStats.priceCHF !== undefined && (
- Cost: + {t('Kosten')} {_formatCurrency(latestStats.priceCHF)}
diff --git a/src/components/UnifiedDataBar/ChatsTab.tsx b/src/components/UnifiedDataBar/ChatsTab.tsx index 225a5ff..a9a1b23 100644 --- a/src/components/UnifiedDataBar/ChatsTab.tsx +++ b/src/components/UnifiedDataBar/ChatsTab.tsx @@ -103,7 +103,7 @@ const ChatsTab: React.FC = ({ context, } groupMap.get(fiId)!.chats.push({ id: wf.id, - label: wf.label || wf.name || `Chat ${wf.id.slice(0, 8)}`, + label: wf.label || wf.name || `${t('Chat')} ${wf.id.slice(0, 8)}`, updatedAt: wf.updatedAt || wf.lastActivity || wf.startedAt, lastMessageAt: wf.lastMessageAt, featureInstanceId: fiId, @@ -132,7 +132,7 @@ const ChatsTab: React.FC = ({ context, } finally { setLoading(false); } - }, [context.instanceId]); + }, [context.instanceId, t]); useEffect(() => { _loadChats(); }, [_loadChats]); @@ -283,7 +283,7 @@ const ChatsTab: React.FC = ({ context, @@ -292,7 +292,7 @@ const ChatsTab: React.FC = ({ context, @@ -300,7 +300,7 @@ const ChatsTab: React.FC = ({ context, @@ -323,10 +323,10 @@ const ChatsTab: React.FC = ({ context, const _featureCodeLabel = (code: string): string => { const labels: Record = { - workspace: 'AI Workspace', - commcoach: 'CommCoach', - trustee: 'Trustee', - automation: 'Automation', + workspace: t('KI-Arbeitsbereich'), + commcoach: t('CommCoach'), + trustee: t('Trustee'), + automation: t('Automation'), }; return labels[code] || code; }; @@ -351,7 +351,7 @@ const ChatsTab: React.FC = ({ context, @@ -362,13 +362,13 @@ const ChatsTab: React.FC = ({ context, className={`${styles.filterTab} ${filter === 'active' ? styles.filterTabActive : ''}`} onClick={() => setFilter('active')} > - Aktiv ({_activeCount}) + {t('Aktiv')} ({_activeCount}) diff --git a/src/components/UnifiedDataBar/FilesTab.tsx b/src/components/UnifiedDataBar/FilesTab.tsx index df87e91..2c8da10 100644 --- a/src/components/UnifiedDataBar/FilesTab.tsx +++ b/src/components/UnifiedDataBar/FilesTab.tsx @@ -250,12 +250,12 @@ const FilesTab: React.FC = ({ context, onFileSelect }) => { zIndex: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 13, fontWeight: 600, color: '#F25843', }}> - Dateien hier ablegen + {t('Dateien hier ablegen')} )}
- Files + {t('Dateien')}
- {'\uD83D\uDC64'} Persönlich - {'\uD83D\uDC65'} Instanz - {'\uD83C\uDFE2'} Mandant - {'\uD83D\uDD12'} Neutralisiert + {'\uD83D\uDC64'} {t('Persönlich')} + {'\uD83D\uDC65'} {t('Instanz')} + {'\uD83C\uDFE2'} {t('Mandant')} + {'\uD83D\uDD12'} {t('Neutralisiert')}
); diff --git a/src/components/UnifiedDataBar/SourcesTab.tsx b/src/components/UnifiedDataBar/SourcesTab.tsx index b050d89..8fbb91b 100644 --- a/src/components/UnifiedDataBar/SourcesTab.tsx +++ b/src/components/UnifiedDataBar/SourcesTab.tsx @@ -85,7 +85,7 @@ interface MandateGroupNode { interface FeatureTableNode { objectKey: string; tableName: string; - label: Record; + label: string; fields: string[]; isParent?: boolean; parentTable?: string; @@ -185,13 +185,6 @@ const _SCOPE_ICONS: Record = { global: '\uD83C\uDF10', }; -const _SCOPE_LABELS: Record = { - personal: 'Personal', - featureInstance: 'Feature Instance', - mandate: 'Mandate', - global: 'Global', -}; - function _nextScope(current: string): string { const idx = _SCOPE_ORDER.indexOf(current); if (idx === -1) return _SCOPE_ORDER[0]; @@ -348,6 +341,14 @@ function _Spinner(): React.ReactElement { const SourcesTab: React.FC = ({ context, onSourcesChanged }) => { const { t } = useLanguage(); + const _scopeLabel = (scope: string) => ({ + personal: t('Persönlich'), + featureInstance: t('Feature-Instanz'), + mandate: t('Mandant'), + global: t('Global'), + } as Record)[scope] || scope; + const _scopeCycleTitle = (scope: string) => + `${t('Bereich')}: ${_scopeLabel(scope)} → ${_scopeLabel(_nextScope(scope))}`; const instanceId = context.instanceId; /* ── Active sources (fetched internally) ── */ @@ -663,7 +664,7 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged }) => featureCode: node.featureCode, tableName: table.tableName, objectKey: table.objectKey, - label: table.label?.en || table.label?.de || table.tableName, + label: table.label || table.tableName, }); _fetchFeatureDataSources(); onSourcesChanged?.(); @@ -764,7 +765,7 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged }) => const childTables = allTables.filter(t => t.parentTable === parentRecord.tableName); if (parentTable) { - const parentLabel = `${parentTable.label?.en || parentTable.label?.de || parentTable.tableName}: ${parentRecord.displayLabel}`; + const parentLabel = `${parentTable.label || parentTable.tableName}: ${parentRecord.displayLabel}`; await api.post(`/api/workspace/${instanceId}/feature-datasources`, { featureInstanceId: node.featureInstanceId, featureCode: node.featureCode, @@ -776,7 +777,7 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged }) => } for (const child of childTables) { - const childLabel = `${child.label?.en || child.label?.de || child.tableName}: ${parentRecord.displayLabel}`; + const childLabel = `${child.label || child.tableName}: ${parentRecord.displayLabel}`; await api.post(`/api/workspace/${instanceId}/feature-datasources`, { featureInstanceId: node.featureInstanceId, featureCode: node.featureCode, @@ -813,7 +814,7 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged }) => {dataSources.length > 0 && (
- Active Personal Sources + {t('Aktive persönliche Quellen')}
{[...dataSources].sort((a, b) => { const aKey = `${a.sourceType}|${a.label || a.path || ''}`; @@ -842,7 +843,7 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged }) => background: 'none', border: 'none', cursor: 'pointer', fontSize: 13, padding: '0 2px', lineHeight: 1, }} - title={`Scope: ${_SCOPE_LABELS[ds.scope] || ds.scope} → ${_SCOPE_LABELS[_nextScope(ds.scope)]}`} + title={_scopeCycleTitle(ds.scope)} > {_SCOPE_ICONS[ds.scope] || _SCOPE_ICONS.personal} @@ -874,7 +875,7 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged }) => {/* ── Browse Sources header ── */}
- Browse Sources + {t('Quellen durchsuchen')} @@ -1022,7 +1023,7 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged }) => @@ -1053,7 +1054,7 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged }) => {/* ── Feature Data header ── */}
- Feature Data + {t('Feature-Daten')} )} {canAdd && alreadyAdded && ( @@ -1197,7 +1198,7 @@ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({ {node.expanded && node.children && node.children.length === 0 && !node.loading && (
- (empty) + {t('(leer)')}
)}
@@ -1299,6 +1300,7 @@ const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({ onToggleParentGroup, onToggleParentRecord, onAddParentRecord, isParentRecordAdded, expandedParentGroups, loadingParentGroup, addingParentKey, }) => { + const { t } = useLanguage(); const [hovered, setHovered] = useState(false); const chevron = node.expanded ? '\u25BE' : '\u25B8'; @@ -1329,7 +1331,7 @@ const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({ {node.label} - {node.tableCount} tables + {node.tableCount} {t('Tabellen')}
@@ -1342,7 +1344,7 @@ const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({ const isGroupLoading = loadingParentGroup === groupKey; const records = node.parentRecords[pt.tableName]; const childTables = (node.tables || []).filter(t => t.parentTable === pt.tableName); - const ptLabel = pt.label?.en || pt.label?.de || pt.tableName; + const ptLabel = pt.label || pt.tableName; return ( <_ParentGroupView @@ -1380,7 +1382,7 @@ const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({ {node.expanded && node.tables && node.tables.length === 0 && !node.loading && (
- (no tables) + {t('(keine Tabellen)')}
)}
@@ -1402,7 +1404,7 @@ const _FeatureTableRow: React.FC<_FeatureTableRowProps> = ({ }) => { const { t } = useLanguage(); const [hovered, setHovered] = useState(false); - const tableLabel = table.label?.en || table.label?.de || table.tableName; + const tableLabel = table.label || table.tableName; return (
= ({ }} title={t('Als Feature-Datenquelle hinzufügen')} > - {isAdding ? '...' : '+ Add'} + {isAdding ? '...' : `+ ${t('Hinzufügen')}`} )} {isAdded && ( @@ -1467,6 +1469,7 @@ const _ParentGroupView: React.FC<_ParentGroupViewProps> = ({ featureNode, parentTable: _parentTable, label, expanded, loading, records, childTables, allTables, onToggleGroup, onToggleRecord, onAddRecord, isRecordAdded, addingParentKey, }) => { + const { t } = useLanguage(); const [hovered, setHovered] = useState(false); const chevron = expanded ? '\u25BE' : '\u25B8'; @@ -1493,7 +1496,7 @@ const _ParentGroupView: React.FC<_ParentGroupViewProps> = ({ {childTables.length > 0 && ( - +{childTables.length} tables + +{childTables.length} {t('Tabellen')} )}
@@ -1518,7 +1521,7 @@ const _ParentGroupView: React.FC<_ParentGroupViewProps> = ({ {expanded && records && records.length === 0 && !loading && (
- (no records) + {t('(keine Einträge)')}
)} @@ -1580,7 +1583,7 @@ const _ParentRecordRow: React.FC<_ParentRecordRowProps> = ({ }} title={t('Alle Tabellen für diese Quelle hinzufügen')} > - {isAdding ? '...' : '+ Add'} + {isAdding ? '...' : `+ ${t('Hinzufügen')}`} )} {isAdded && ( @@ -1593,7 +1596,7 @@ const _ParentRecordRow: React.FC<_ParentRecordRowProps> = ({ {record.expanded && (
{childTables.map(ct => { - const ctLabel = ct.label?.en || ct.label?.de || ct.tableName; + const ctLabel = ct.label || ct.tableName; return (
= { - chats: 'Chats', + chats: 'Chatverläufe', files: 'Dateien', sources: 'Quellen', }; diff --git a/src/core/PageManager/SidebarProvider.tsx b/src/core/PageManager/SidebarProvider.tsx index 0d0256f..294628f 100644 --- a/src/core/PageManager/SidebarProvider.tsx +++ b/src/core/PageManager/SidebarProvider.tsx @@ -83,8 +83,6 @@ export const SidebarProvider: React.FC = ({ children }) => // Helper function to resolve node name const resolveNodeName = (pathSegment: string, fullPath: string, page?: GenericPageData): string => { - const { t } = useLanguage(); - if (page) { return resolveLanguageText(page.name, t); } @@ -460,7 +458,7 @@ export const SidebarProvider: React.FC = ({ children }) => setSidebarItems(items); } catch (err) { console.error('❌ SidebarProvider: Error refreshing sidebar:', err); - setError(err instanceof Error ? err.message : 'Failed to load sidebar items'); + setError(err instanceof Error ? err.message : t('Seitenleiste konnte nicht geladen werden')); } finally { setLoading(false); } diff --git a/src/core/PageManager/pageInterface.ts b/src/core/PageManager/pageInterface.ts index a53ba2a..f0a1133 100644 --- a/src/core/PageManager/pageInterface.ts +++ b/src/core/PageManager/pageInterface.ts @@ -34,16 +34,11 @@ export interface GenericPageData { type TranslationFunction = (key: string, fallback?: string) => string; /** - * Resolve display text from a page name that may be a i18n key or { de?, en? }. + * Resolve display text from a page name (i18n key) via the translation function. */ export function resolveLanguageText( - name: string | { de?: string; en?: string }, + name: string, t: TranslationFunction ): string { - if (typeof name === 'string') { - const resolved = t(name); - return resolved !== name ? resolved : name; - } - const lang = (typeof navigator !== 'undefined' && navigator.language?.startsWith('en')) ? 'en' : 'de'; - return name[lang] ?? name.de ?? name.en ?? ''; + return t(name); } diff --git a/src/hooks/useAccessRules.tsx b/src/hooks/useAccessRules.tsx index 7b19dec..bd6bd0a 100644 --- a/src/hooks/useAccessRules.tsx +++ b/src/hooks/useAccessRules.tsx @@ -7,6 +7,7 @@ import { useState, useCallback } from 'react'; import api from '../api'; +import { useLanguage } from '../providers/language/LanguageContext'; // ============================================================================= // TYPES @@ -78,6 +79,7 @@ interface SaveResult { // ============================================================================= export function useAccessRules(roleId: string, apiBasePath: string = '/api/rbac', mandateId?: string) { + const { t } = useLanguage(); const [rules, setRules] = useState([]); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); @@ -113,14 +115,14 @@ export function useAccessRules(roleId: string, apiBasePath: string = '/api/rbac' setRules(fetchedRules); return fetchedRules; } catch (err: any) { - const errorMsg = err.response?.data?.detail || err.message || 'Fehler beim Laden der Regeln'; + const errorMsg = err.response?.data?.detail || err.message || t('Fehler beim Laden der Regeln'); setError(errorMsg); console.error('Error fetching rules:', err); return []; } finally { setLoading(false); } - }, [roleId, apiBasePath, isInstanceApi, getHeaders]); + }, [roleId, apiBasePath, isInstanceApi, getHeaders, t]); /** * Save all rules for the role @@ -196,14 +198,14 @@ export function useAccessRules(roleId: string, apiBasePath: string = '/api/rbac' return { success: true }; } catch (err: any) { - const errorMsg = err.response?.data?.detail || err.message || 'Fehler beim Speichern'; + const errorMsg = err.response?.data?.detail || err.message || t('Fehler beim Speichern'); setError(errorMsg); console.error('Error saving rules:', err); return { success: false, error: errorMsg }; } finally { setSaving(false); } - }, [roleId, apiBasePath, isInstanceApi, fetchRules, getHeaders]); + }, [roleId, apiBasePath, isInstanceApi, fetchRules, getHeaders, t]); /** * Get rules grouped by context diff --git a/src/hooks/useAdminMandates.ts b/src/hooks/useAdminMandates.ts index dcf66a4..030e06a 100644 --- a/src/hooks/useAdminMandates.ts +++ b/src/hooks/useAdminMandates.ts @@ -188,15 +188,10 @@ export function useMandates() { // Handle options - can be array or string reference const attrOptions = (attr as any).options; if (Array.isArray(attrOptions)) { - options = attrOptions.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attrOptions.map((opt: any) => ({ + value: opt.value, + label: opt.label || String(opt.value) + })); } else if (typeof attrOptions === 'string') { // Options reference (e.g., "user.role", "auth.authority") optionsReference = attrOptions; @@ -206,15 +201,10 @@ export function useMandates() { // Handle options - can be array or string reference const attrOptions = (attr as any).options; if (Array.isArray(attrOptions)) { - options = attrOptions.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attrOptions.map((opt: any) => ({ + value: opt.value, + label: opt.label || String(opt.value) + })); } else if (typeof attrOptions === 'string') { // Options reference (e.g., "user.role", "auth.authority") optionsReference = attrOptions; @@ -327,15 +317,10 @@ export function useMandates() { fieldType = 'enum'; const attrOptions = (attr as any).options; if (Array.isArray(attrOptions)) { - options = attrOptions.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attrOptions.map((opt: any) => ({ + value: opt.value, + label: opt.label || String(opt.value) + })); } else if (typeof attrOptions === 'string') { optionsReference = attrOptions; } @@ -343,15 +328,10 @@ export function useMandates() { fieldType = 'multiselect'; const attrOptions = (attr as any).options; if (Array.isArray(attrOptions)) { - options = attrOptions.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attrOptions.map((opt: any) => ({ + value: opt.value, + label: opt.label || String(opt.value) + })); } else if (typeof attrOptions === 'string') { optionsReference = attrOptions; } diff --git a/src/hooks/useAdminRbacRoles.ts b/src/hooks/useAdminRbacRoles.ts index 9134eb7..b140135 100644 --- a/src/hooks/useAdminRbacRoles.ts +++ b/src/hooks/useAdminRbacRoles.ts @@ -229,15 +229,10 @@ export function useRbacRoles() { // Handle options - can be array or string reference const attrOptions = (attr as any).options; if (Array.isArray(attrOptions)) { - options = attrOptions.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attrOptions.map((opt: any) => ({ + value: opt.value, + label: opt.label || String(opt.value) + })); } else if (typeof attrOptions === 'string') { // Options reference (e.g., "user.role", "auth.authority") optionsReference = attrOptions; @@ -247,15 +242,10 @@ export function useRbacRoles() { // Handle options - can be array or string reference const attrOptions = (attr as any).options; if (Array.isArray(attrOptions)) { - options = attrOptions.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attrOptions.map((opt: any) => ({ + value: opt.value, + label: opt.label || String(opt.value) + })); } else if (typeof attrOptions === 'string') { // Options reference (e.g., "user.role", "auth.authority") optionsReference = attrOptions; @@ -368,15 +358,10 @@ export function useRbacRoles() { fieldType = 'enum'; const attrOptions = (attr as any).options; if (Array.isArray(attrOptions)) { - options = attrOptions.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attrOptions.map((opt: any) => ({ + value: opt.value, + label: opt.label || String(opt.value) + })); } else if (typeof attrOptions === 'string') { optionsReference = attrOptions; } @@ -384,15 +369,10 @@ export function useRbacRoles() { fieldType = 'multiselect'; const attrOptions = (attr as any).options; if (Array.isArray(attrOptions)) { - options = attrOptions.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attrOptions.map((opt: any) => ({ + value: opt.value, + label: opt.label || String(opt.value) + })); } else if (typeof attrOptions === 'string') { optionsReference = attrOptions; } diff --git a/src/hooks/useAdminRbacRules.ts b/src/hooks/useAdminRbacRules.ts index 4daab54..5894293 100644 --- a/src/hooks/useAdminRbacRules.ts +++ b/src/hooks/useAdminRbacRules.ts @@ -205,15 +205,10 @@ export function useRbacRules() { // Handle options - can be array or string reference const attrOptions = (attr as any).options; if (Array.isArray(attrOptions)) { - options = attrOptions.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attrOptions.map((opt: any) => ({ + value: opt.value, + label: opt.label || String(opt.value) + })); } else if (typeof attrOptions === 'string') { // Options reference (e.g., "user.role", "auth.authority") optionsReference = attrOptions; @@ -223,15 +218,10 @@ export function useRbacRules() { // Handle options - can be array or string reference const attrOptions = (attr as any).options; if (Array.isArray(attrOptions)) { - options = attrOptions.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attrOptions.map((opt: any) => ({ + value: opt.value, + label: opt.label || String(opt.value) + })); } else if (typeof attrOptions === 'string') { // Options reference (e.g., "user.role", "auth.authority") optionsReference = attrOptions; @@ -344,15 +334,10 @@ export function useRbacRules() { fieldType = 'enum'; const attrOptions = (attr as any).options; if (Array.isArray(attrOptions)) { - options = attrOptions.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attrOptions.map((opt: any) => ({ + value: opt.value, + label: opt.label || String(opt.value) + })); } else if (typeof attrOptions === 'string') { optionsReference = attrOptions; } @@ -360,15 +345,10 @@ export function useRbacRules() { fieldType = 'multiselect'; const attrOptions = (attr as any).options; if (Array.isArray(attrOptions)) { - options = attrOptions.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attrOptions.map((opt: any) => ({ + value: opt.value, + label: opt.label || String(opt.value) + })); } else if (typeof attrOptions === 'string') { optionsReference = attrOptions; } diff --git a/src/hooks/useFeatureAccess.ts b/src/hooks/useFeatureAccess.ts index 739c022..e67658e 100644 --- a/src/hooks/useFeatureAccess.ts +++ b/src/hooks/useFeatureAccess.ts @@ -26,7 +26,7 @@ export interface PaginationMetadata { export interface Feature { code: string; - label: string | { [key: string]: string }; + label: string; icon?: string; enabled?: boolean; } @@ -62,7 +62,7 @@ export interface FeatureAccessUser { export interface FeatureInstanceRole { id: string; roleLabel: string; - description?: { [key: string]: string }; + description?: string; featureCode?: string; isSystemRole?: boolean; } @@ -312,7 +312,7 @@ export function useFeatureAccess() { name: string; features: Array<{ code: string; - label: string | { [key: string]: string }; + label: string; instances: Array<{ id: string; featureCode: string; diff --git a/src/hooks/useMandateRoles.ts b/src/hooks/useMandateRoles.ts index 4feb2a7..b042344 100644 --- a/src/hooks/useMandateRoles.ts +++ b/src/hooks/useMandateRoles.ts @@ -12,7 +12,7 @@ import api from '../api'; export interface Role { id: string; roleLabel: string; - description?: string | { [key: string]: string }; + description?: string; mandateId?: string; featureInstanceId?: string; featureCode?: string; @@ -24,7 +24,7 @@ export interface Role { export interface RoleCreate { roleLabel: string; - description?: string | { [key: string]: string }; + description?: string; mandateId?: string; featureInstanceId?: string; featureCode?: string; @@ -32,7 +32,7 @@ export interface RoleCreate { export interface RoleUpdate { roleLabel?: string; - description?: string | { [key: string]: string }; + description?: string; mandateId?: string | null; } diff --git a/src/hooks/useMandates.ts b/src/hooks/useMandates.ts index f57c299..31d9621 100644 --- a/src/hooks/useMandates.ts +++ b/src/hooks/useMandates.ts @@ -33,7 +33,7 @@ export interface AttributeDefinition { description?: string; required?: boolean; default?: any; - options?: Array<{ value: string | number; label: string | { [key: string]: string } }> | string; + options?: Array<{ value: string | number; label: string }> | string; sortable?: boolean; filterable?: boolean; searchable?: boolean; diff --git a/src/hooks/usePrompts.ts b/src/hooks/usePrompts.ts index 0454e37..25c2d55 100644 --- a/src/hooks/usePrompts.ts +++ b/src/hooks/usePrompts.ts @@ -20,7 +20,7 @@ export type { Prompt, AttributeDefinition, PaginationParams }; // Re-export AttributeOption for backward compatibility export interface AttributeOption { value: string | number; - label: string | { [key: string]: string }; + label: string; } // Prompts list hook @@ -185,15 +185,10 @@ export function usePrompts() { fieldType = 'enum'; // Handle options - can be array or string reference if (Array.isArray(attr.options)) { - options = attr.options.map(opt => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attr.options.map(opt => ({ + value: opt.value, + label: String(opt.label ?? opt.value) + })); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } @@ -201,15 +196,10 @@ export function usePrompts() { fieldType = 'multiselect'; // Handle options - can be array or string reference if (Array.isArray(attr.options)) { - options = attr.options.map(opt => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attr.options.map(opt => ({ + value: opt.value, + label: String(opt.label ?? opt.value) + })); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } diff --git a/src/hooks/useRbacExportImport.ts b/src/hooks/useRbacExportImport.ts index d4a959a..857916e 100644 --- a/src/hooks/useRbacExportImport.ts +++ b/src/hooks/useRbacExportImport.ts @@ -25,7 +25,7 @@ export interface RbacExportScope { export interface RbacExportRole { roleLabel: string; - description?: { [key: string]: string }; + description?: string; featureCode?: string; } diff --git a/src/hooks/useRealEstate.ts b/src/hooks/useRealEstate.ts index 35e320c..7e07f17 100644 --- a/src/hooks/useRealEstate.ts +++ b/src/hooks/useRealEstate.ts @@ -181,7 +181,7 @@ function _createRealEstateEntityHook(config: RealEstat if (Array.isArray(attr.options)) { options = (attr.options as any[]).map((opt: any) => ({ value: opt.value, - label: typeof opt.label === 'string' ? opt.label : opt.label?.en || String(opt.value), + label: opt.label || String(opt.value), })); } else if (typeof attr.options === 'string') optionsReference = attr.options; } else if (attr.type === 'multiselect') { @@ -189,7 +189,7 @@ function _createRealEstateEntityHook(config: RealEstat if (Array.isArray(attr.options)) { options = (attr.options as any[]).map((opt: any) => ({ value: opt.value, - label: typeof opt.label === 'string' ? opt.label : opt.label?.en || String(opt.value), + label: opt.label || String(opt.value), })); } else if (typeof attr.options === 'string') optionsReference = attr.options; } else if (attr.type === 'textarea') fieldType = 'textarea'; @@ -224,7 +224,7 @@ function _createRealEstateEntityHook(config: RealEstat if (Array.isArray(attr.options)) { options = (attr.options as any[]).map((opt: any) => ({ value: opt.value, - label: typeof opt.label === 'string' ? opt.label : opt.label?.en || String(opt.value), + label: opt.label || String(opt.value), })); } else if (typeof attr.options === 'string') optionsReference = attr.options; } else if (attr.type === 'multiselect') { @@ -232,7 +232,7 @@ function _createRealEstateEntityHook(config: RealEstat if (Array.isArray(attr.options)) { options = (attr.options as any[]).map((opt: any) => ({ value: opt.value, - label: typeof opt.label === 'string' ? opt.label : opt.label?.en || String(opt.value), + label: opt.label || String(opt.value), })); } else if (typeof attr.options === 'string') optionsReference = attr.options; } else if (attr.type === 'textarea') fieldType = 'textarea'; diff --git a/src/hooks/useTrustee.ts b/src/hooks/useTrustee.ts index 08ba6d6..de367fd 100644 --- a/src/hooks/useTrustee.ts +++ b/src/hooks/useTrustee.ts @@ -237,24 +237,18 @@ function _createTrusteeEntityHook(config: TrusteeEntit } else if (attr.type === 'select') { fieldType = 'enum'; if (Array.isArray(attr.options)) { - options = attr.options.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); - return { value: opt.value, label: labelValue }; - }); + options = attr.options.map((opt: any) => ({ + value: opt.value, label: opt.label || String(opt.value) + })); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } } else if (attr.type === 'multiselect') { fieldType = 'multiselect'; if (Array.isArray(attr.options)) { - options = attr.options.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); - return { value: opt.value, label: labelValue }; - }); + options = attr.options.map((opt: any) => ({ + value: opt.value, label: opt.label || String(opt.value) + })); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } @@ -303,24 +297,18 @@ function _createTrusteeEntityHook(config: TrusteeEntit } else if (attr.type === 'select') { fieldType = 'enum'; if (Array.isArray(attr.options)) { - options = attr.options.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); - return { value: opt.value, label: labelValue }; - }); + options = attr.options.map((opt: any) => ({ + value: opt.value, label: opt.label || String(opt.value) + })); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } } else if (attr.type === 'multiselect') { fieldType = 'multiselect'; if (Array.isArray(attr.options)) { - options = attr.options.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); - return { value: opt.value, label: labelValue }; - }); + options = attr.options.map((opt: any) => ({ + value: opt.value, label: opt.label || String(opt.value) + })); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } diff --git a/src/hooks/useTrusteeAccess.ts b/src/hooks/useTrusteeAccess.ts index 6e1db73..b0f147c 100644 --- a/src/hooks/useTrusteeAccess.ts +++ b/src/hooks/useTrusteeAccess.ts @@ -193,15 +193,10 @@ export function useTrusteeAccess() { } else if (attr.type === 'select') { fieldType = 'enum'; if (Array.isArray(attr.options)) { - options = attr.options.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attr.options.map((opt: any) => ({ + value: opt.value, + label: opt.label || String(opt.value) + })); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } diff --git a/src/hooks/useTrusteeContracts.ts b/src/hooks/useTrusteeContracts.ts index 0b628c1..0012caf 100644 --- a/src/hooks/useTrusteeContracts.ts +++ b/src/hooks/useTrusteeContracts.ts @@ -193,15 +193,10 @@ export function useTrusteeContracts() { } else if (attr.type === 'select') { fieldType = 'enum'; if (Array.isArray(attr.options)) { - options = attr.options.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attr.options.map((opt: any) => ({ + value: opt.value, + label: opt.label || String(opt.value) + })); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } diff --git a/src/hooks/useTrusteeDocuments.ts b/src/hooks/useTrusteeDocuments.ts index 2453b0d..d14aa2c 100644 --- a/src/hooks/useTrusteeDocuments.ts +++ b/src/hooks/useTrusteeDocuments.ts @@ -194,15 +194,10 @@ export function useTrusteeDocuments() { } else if (attr.type === 'select') { fieldType = 'enum'; if (Array.isArray(attr.options)) { - options = attr.options.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attr.options.map((opt: any) => ({ + value: opt.value, + label: opt.label || String(opt.value) + })); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } diff --git a/src/hooks/useTrusteeOrganisations.ts b/src/hooks/useTrusteeOrganisations.ts index 11eb9d1..c0f3c89 100644 --- a/src/hooks/useTrusteeOrganisations.ts +++ b/src/hooks/useTrusteeOrganisations.ts @@ -191,30 +191,20 @@ export function useTrusteeOrganisations() { } else if (attr.type === 'select') { fieldType = 'enum'; if (Array.isArray(attr.options)) { - options = attr.options.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attr.options.map((opt: any) => ({ + value: opt.value, + label: opt.label || String(opt.value) + })); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } } else if (attr.type === 'multiselect') { fieldType = 'multiselect'; if (Array.isArray(attr.options)) { - options = attr.options.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attr.options.map((opt: any) => ({ + value: opt.value, + label: opt.label || String(opt.value) + })); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } diff --git a/src/hooks/useTrusteePositionDocuments.ts b/src/hooks/useTrusteePositionDocuments.ts index e55d053..ba2c9a4 100644 --- a/src/hooks/useTrusteePositionDocuments.ts +++ b/src/hooks/useTrusteePositionDocuments.ts @@ -181,15 +181,10 @@ export function useTrusteePositionDocuments() { } else if (attr.type === 'select') { fieldType = 'enum'; if (Array.isArray(attr.options)) { - options = attr.options.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attr.options.map((opt: any) => ({ + value: opt.value, + label: opt.label || String(opt.value) + })); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } diff --git a/src/hooks/useTrusteePositions.ts b/src/hooks/useTrusteePositions.ts index c2a51c2..59a8764 100644 --- a/src/hooks/useTrusteePositions.ts +++ b/src/hooks/useTrusteePositions.ts @@ -207,15 +207,10 @@ export function useTrusteePositions() { } else if (attr.type === 'select') { fieldType = 'enum'; if (Array.isArray(attr.options)) { - options = attr.options.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attr.options.map((opt: any) => ({ + value: opt.value, + label: opt.label || String(opt.value) + })); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } diff --git a/src/hooks/useTrusteeRoles.ts b/src/hooks/useTrusteeRoles.ts index 5be6605..c42a674 100644 --- a/src/hooks/useTrusteeRoles.ts +++ b/src/hooks/useTrusteeRoles.ts @@ -201,15 +201,10 @@ export function useTrusteeRoles() { } else if (attr.type === 'select') { fieldType = 'enum'; if (Array.isArray(attr.options)) { - options = attr.options.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attr.options.map((opt: any) => ({ + value: opt.value, + label: opt.label || String(opt.value) + })); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } diff --git a/src/hooks/useUserMandates.ts b/src/hooks/useUserMandates.ts index b73838d..bc1dc43 100644 --- a/src/hooks/useUserMandates.ts +++ b/src/hooks/useUserMandates.ts @@ -51,7 +51,7 @@ export interface UserMandateResponse { export interface Role { id: string; roleLabel: string; - description?: string | { [key: string]: string }; + description?: string; mandateId?: string; featureInstanceId?: string; featureCode?: string; @@ -60,7 +60,7 @@ export interface Role { export interface Mandate { id: string; - name: string | { [key: string]: string }; + name: string; label?: string; code?: string; language?: string; diff --git a/src/hooks/useUsers.ts b/src/hooks/useUsers.ts index 130c285..b9797d7 100644 --- a/src/hooks/useUsers.ts +++ b/src/hooks/useUsers.ts @@ -253,7 +253,7 @@ export function useCurrentUser() { // Re-export AttributeOption for backward compatibility export interface AttributeOption { value: string | number; - label: string | { [key: string]: string }; + label: string; } // Organization users hook (list, update, delete) - following prompts/workflows pattern @@ -436,15 +436,10 @@ export function useOrgUsers() { fieldType = 'enum'; // Handle options - can be array or string reference if (Array.isArray(attr.options)) { - options = attr.options.map(opt => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attr.options.map(opt => ({ + value: opt.value, + label: opt.label || String(opt.value) + })); } else if (typeof attr.options === 'string') { // Options reference (e.g., "user.role", "auth.authority") optionsReference = attr.options; @@ -453,15 +448,10 @@ export function useOrgUsers() { fieldType = 'multiselect'; // Handle options - can be array or string reference if (Array.isArray(attr.options)) { - options = attr.options.map(opt => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); + options = attr.options.map(opt => ({ + value: opt.value, + label: opt.label || String(opt.value) + })); } else if (typeof attr.options === 'string') { // Options reference (e.g., "user.role", "auth.authority") optionsReference = attr.options; @@ -582,12 +572,9 @@ export function useOrgUsers() { fieldType = 'enum'; if (Array.isArray(attr.options)) { options = attr.options.map(opt => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); return { value: opt.value, - label: labelValue + label: opt.label || String(opt.value) }; }); } else if (typeof attr.options === 'string') { @@ -596,15 +583,10 @@ export function useOrgUsers() { } else if (attrType === 'multiselect') { fieldType = 'multiselect'; if (Array.isArray(attr.options)) { - options = attr.options.map(opt => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); - return { + options = attr.options.map(opt => ({ value: opt.value, - label: labelValue - }; - }); + label: opt.label || String(opt.value) + })); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } diff --git a/src/hooks/useWorkflows.ts b/src/hooks/useWorkflows.ts index f8c8738..4723e70 100644 --- a/src/hooks/useWorkflows.ts +++ b/src/hooks/useWorkflows.ts @@ -93,7 +93,7 @@ export type { AttributeDefinition } from '../api/attributesApi'; // Attribute option interface (from backend) export interface AttributeOption { value: string | number; - label: string | { [key: string]: string }; // Can be string or object with language keys + label: string; } // Pagination parameters @@ -300,15 +300,10 @@ export function useUserWorkflows(options?: { instanceId?: string; featureCode?: fieldType = 'enum'; // Handle options - can be array or string reference if (Array.isArray(attr.options)) { - options = attr.options.map(opt => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); - return { + options = attr.options.map(opt => ({ value: opt.value, - label: labelValue - }; - }); + label: opt.label || String(opt.value) + })); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } @@ -316,15 +311,10 @@ export function useUserWorkflows(options?: { instanceId?: string; featureCode?: fieldType = 'multiselect'; // Handle options - can be array or string reference if (Array.isArray(attr.options)) { - options = attr.options.map(opt => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); - return { + options = attr.options.map(opt => ({ value: opt.value, - label: labelValue - }; - }); + label: opt.label || String(opt.value) + })); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } diff --git a/src/layouts/FeatureLayout.tsx b/src/layouts/FeatureLayout.tsx index ef442b5..5771983 100644 --- a/src/layouts/FeatureLayout.tsx +++ b/src/layouts/FeatureLayout.tsx @@ -5,10 +5,11 @@ * Stellt den Instanz-Kontext bereit und rendert Sidebar + Content. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { Outlet, Navigate, useLocation } from 'react-router-dom'; import { useCurrentInstance } from '../hooks/useCurrentInstance'; import { useFeaturesInitialized, useFeaturesLoading } from '../stores/featureStore'; +import useNavigation from '../hooks/useNavigation'; import styles from './FeatureLayout.module.css'; import { useLanguage } from '../providers/language/LanguageContext'; @@ -44,7 +45,7 @@ const ErrorScreen: React.FC = ({ message, returnPath = '/' })

{t('Zugriff nicht möglich')}

{message}

- Zurück zur Übersicht + {t('Zurück zur Übersicht')}
); @@ -65,10 +66,27 @@ const ErrorScreen: React.FC = ({ message, returnPath = '/' }) * Bei Erfolg: Rendert für die verschachtelten Routes */ export const FeatureLayout: React.FC = () => { + const { t } = useLanguage(); const location = useLocation(); const initialized = useFeaturesInitialized(); const loading = useFeaturesLoading(); - const { instance, mandate, feature, isValid, isLoading } = useCurrentInstance(); + const { instance, mandate, feature, isValid, isLoading, mandateId, featureCode, instanceId } = useCurrentInstance(); + const { dynamicBlock } = useNavigation(); + + const navLabels = useMemo(() => { + if (!dynamicBlock || !mandateId) return null; + const navMandate = dynamicBlock.mandates.find(m => m.id === mandateId); + if (!navMandate) return null; + const navFeature = featureCode ? navMandate.features.find(f => f.uiComponent === featureCode) : undefined; + const navInstance = navFeature && instanceId + ? navFeature.instances.find(i => i.id === instanceId) + : undefined; + return { + mandate: t(navMandate.uiLabel), + feature: navFeature ? t(navFeature.uiLabel) : undefined, + instance: navInstance ? t(navInstance.uiLabel) : undefined, + }; + }, [dynamicBlock, mandateId, featureCode, instanceId, t]); // Warten bis Features geladen sind if (!initialized || loading || isLoading) { @@ -86,7 +104,7 @@ export const FeatureLayout: React.FC = () => { return ( ); } @@ -97,11 +115,11 @@ export const FeatureLayout: React.FC = () => { {/* Header mit Instanz-Info */}
- {mandate?.name} + {navLabels?.mandate || mandate?.label || mandate?.name} / - {feature?.label?.de || feature?.code} + {navLabels?.feature || feature?.code} / - {instance?.instanceLabel} + {navLabels?.instance || instance?.instanceLabel}
{instance?.userRoles?.join(', ') || '-'} @@ -133,6 +151,7 @@ export const ProtectedFeatureRoute: React.FC = ({ requiredView, children, }) => { + const { t } = useLanguage(); const { instance, isValid } = useCurrentInstance(); if (!isValid) { @@ -146,7 +165,7 @@ export const ProtectedFeatureRoute: React.FC = ({ if (!hasViewAccess) { return ( ); diff --git a/src/layouts/MainLayout.tsx b/src/layouts/MainLayout.tsx index 9b6cc2c..2f96ac0 100644 --- a/src/layouts/MainLayout.tsx +++ b/src/layouts/MainLayout.tsx @@ -84,13 +84,13 @@ const MainLayoutInner: React.FC = () => {
diff --git a/src/pages/FeatureView.tsx b/src/pages/FeatureView.tsx index 4eb71dd..aa1baac 100644 --- a/src/pages/FeatureView.tsx +++ b/src/pages/FeatureView.tsx @@ -73,12 +73,12 @@ const ChatworkflowDashboard: React.FC = () => { const ChatworkflowRuns: React.FC = () => { const { t } = useLanguage(); - return ; + return ; }; const ChatworkflowFiles: React.FC = () => { const { t } = useLanguage(); - return ; + return ; }; // Chatbot Views diff --git a/src/pages/GDPR.tsx b/src/pages/GDPR.tsx index c02a0e1..b409e8d 100644 --- a/src/pages/GDPR.tsx +++ b/src/pages/GDPR.tsx @@ -68,7 +68,7 @@ export const GDPRPage: React.FC = () => { } catch (error: any) { console.error('Failed to load GDPR consent info:', error); if (isActive) { - setConsentError('Consent information could not be loaded.'); + setConsentError(t('Einwilligungsinformationen konnten nicht geladen werden.')); } } finally { if (isActive) { @@ -82,7 +82,7 @@ export const GDPRPage: React.FC = () => { return () => { isActive = false; }; - }, []); + }, [t]); const handleDataExport = async () => { if (isActionLocked) return; @@ -91,10 +91,10 @@ export const GDPRPage: React.FC = () => { try { const response = await api.get('/api/user/me/data-export'); downloadJson(response.data, 'gdpr-data-export.json'); - setActionMessage({ type: 'success', text: 'Data export downloaded.' }); + setActionMessage({ type: 'success', text: t('Datenexport heruntergeladen.') }); } catch (error: any) { console.error('GDPR export failed:', error); - setActionMessage({ type: 'error', text: 'Data export failed. Please try again.' }); + setActionMessage({ type: 'error', text: t('Datenexport fehlgeschlagen. Bitte erneut versuchen.') }); } finally { setIsExporting(false); } @@ -109,10 +109,10 @@ export const GDPRPage: React.FC = () => { headers: { Accept: 'application/ld+json' } }); downloadJson(response.data, 'gdpr-data-portability.json', 'application/ld+json'); - setActionMessage({ type: 'success', text: 'Portable export downloaded.' }); + setActionMessage({ type: 'success', text: t('Portabler Export heruntergeladen.') }); } catch (error: any) { console.error('GDPR portability export failed:', error); - setActionMessage({ type: 'error', text: 'Portable export failed. Please try again.' }); + setActionMessage({ type: 'error', text: t('Portabler Export fehlgeschlagen. Bitte erneut versuchen.') }); } finally { setIsPortabilityExporting(false); } @@ -121,7 +121,7 @@ export const GDPRPage: React.FC = () => { const handleDeleteAccount = async () => { setActionMessage(null); if (deleteConfirmText !== 'LOESCHEN') { - setActionMessage({ type: 'error', text: 'Please type LOESCHEN to confirm deletion.' }); + setActionMessage({ type: 'error', text: t('Bitte geben Sie LOESCHEN ein, um die Löschung zu bestätigen.') }); return; } @@ -132,11 +132,11 @@ export const GDPRPage: React.FC = () => { sessionStorage.removeItem('auth_authority'); clearUserDataCache(); setIsDeleted(true); - setActionMessage({ type: 'success', text: 'Account deleted. Redirecting to login...' }); + setActionMessage({ type: 'success', text: t('Konto gelöscht. Weiterleitung zur Anmeldung…') }); window.location.replace('/login'); } catch (error: any) { console.error('GDPR deletion failed:', error); - setActionMessage({ type: 'error', text: 'Account deletion failed. Please try again.' }); + setActionMessage({ type: 'error', text: t('Kontolöschung fehlgeschlagen. Bitte erneut versuchen.') }); } finally { setIsDeleting(false); } @@ -148,14 +148,14 @@ export const GDPRPage: React.FC = () => {

- GDPR / Privacy + {t('DSGVO / Datenschutz')}

- Manage your personal data exports and account deletion. + {t('Verwalten Sie Ihre personenbezogenen Datenexporte und Kontolöschung.')}

- Back to Settings + {t('Zurück zu Einstellungen')} @@ -174,12 +174,12 @@ export const GDPRPage: React.FC = () => { {isExporting ? ( - Exporting... + {t('Export wird erstellt…')} ) : ( <> - Export data + {t('Daten exportieren')} )} @@ -196,12 +196,12 @@ export const GDPRPage: React.FC = () => { {isPortabilityExporting ? ( - Exporting... + {t('Export wird erstellt…')} ) : ( <> - Export portable data + {t('Portabler Datenexport')} )} @@ -217,13 +217,15 @@ export const GDPRPage: React.FC = () => { disabled={isActionLocked} > - Start deletion + {t('Löschung starten')} )} {showDeleteConfirm && (

- This action is irreversible. Type LOESCHEN to confirm. + {t('Diese Aktion ist unwiderruflich. Geben Sie {word} ein, um zu bestätigen.', { + word: 'LOESCHEN', + })}

{ }} disabled={isDeleting} > - Cancel + {t('Abbrechen')} @@ -314,7 +316,7 @@ export const GDPRPage: React.FC = () => {
-

Contact

+

{t('Kontakt')}

    {Object.entries({ ...(consentInfo.contact || {}), diff --git a/src/pages/InvitePage.tsx b/src/pages/InvitePage.tsx index b6a9339..ca5ae7d 100644 --- a/src/pages/InvitePage.tsx +++ b/src/pages/InvitePage.tsx @@ -126,7 +126,7 @@ export const InvitePage: React.FC = () => { sessionStorage.removeItem('auth_authority'); handleLoginRedirect(); } else { - setError(result.error || 'Fehler beim Annehmen der Einladung'); + setError(result.error || t('Fehler beim Annehmen der Einladung')); } setAccepting(false); diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 53b1da6..c4feb60 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -168,7 +168,7 @@ function Login() { }} className={`${styles.input} ${usernameFocused || username ? styles.focused : ''}`} /> - +

- Mit der Anmeldung stimmen Sie unseren Datenschutzbestimmungen zur KI-Nutzung zu. + {t('Mit der Anmeldung stimmen Sie unseren Datenschutzbestimmungen zur KI-Nutzung zu.')}

@@ -206,12 +206,12 @@ function Login() { className={styles.textButton} onClick={() => navigate("/password-reset-request")} > - Passwort vergessen? + {t('Passwort vergessen?')}
- oder + {t('oder')}
@@ -232,7 +232,7 @@ function Login() { >
- {isGoogleLoading ? "Signing in..." : "Mit Google anmelden"} + {isGoogleLoading ? t('Anmeldung läuft…') : t('Mit Google anmelden')}
@@ -245,7 +245,7 @@ function Login() { className={styles.ctaPrimary} onClick={() => navigate('/register', { state: location.state })} > - Kostenlos registrieren + {t('Kostenlos registrieren')}
diff --git a/src/pages/PasswordResetRequest.tsx b/src/pages/PasswordResetRequest.tsx index 204f172..2b3964f 100644 --- a/src/pages/PasswordResetRequest.tsx +++ b/src/pages/PasswordResetRequest.tsx @@ -28,26 +28,26 @@ function PasswordResetRequest() { setValidationError(null); if (!username.trim()) { - setValidationError('Bitte geben Sie Ihren Benutzernamen ein.'); + setValidationError(t('Bitte geben Sie Ihren Benutzernamen ein.')); return; } try { await requestReset(username.trim()); - setSuccessMessage('Falls ein Konto mit diesem Benutzernamen existiert, wurde ein Reset-Link an die hinterlegte E-Mail-Adresse gesendet. Bitte prüfen Sie auch Ihren Spam-Ordner.'); + setSuccessMessage(t('Falls ein Konto mit diesem Benutzernamen existiert, wurde ein Reset-Link an die hinterlegte E-Mail-Adresse gesendet. Bitte prüfen Sie auch Ihren Spam-Ordner.')); // Redirect to login after delay setTimeout(() => { navigate('/login', { state: { passwordResetRequested: true, - message: 'Bitte prüfen Sie Ihre E-Mail für den Passwort-Reset-Link.' + message: t('Bitte prüfen Sie Ihre E-Mail für den Passwort-Reset-Link.') } }); }, 5000); } catch (err) { // For security, still show success message even on error - setSuccessMessage('Falls ein Konto mit diesem Benutzernamen existiert, wurde ein Reset-Link an die hinterlegte E-Mail-Adresse gesendet. Bitte prüfen Sie auch Ihren Spam-Ordner.'); + setSuccessMessage(t('Falls ein Konto mit diesem Benutzernamen existiert, wurde ein Reset-Link an die hinterlegte E-Mail-Adresse gesendet. Bitte prüfen Sie auch Ihren Spam-Ordner.')); setTimeout(() => { navigate('/login'); }, 5000); @@ -97,7 +97,7 @@ function PasswordResetRequest() { }} className={`${styles.input} ${usernameFocused || username ? styles.focused : ''}`} /> - +
@@ -109,7 +109,7 @@ function PasswordResetRequest() { onClick={handleSubmit} disabled={isLoading} > - {isLoading ? "Wird gesendet..." : "Reset-Link anfordern"} + {isLoading ? t('Wird gesendet…') : t('Reset-Link anfordern')} )} @@ -120,7 +120,7 @@ function PasswordResetRequest() { className={styles.textButton} onClick={() => navigate("/login")} > - Login + {t('Login')}
diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx index 801205a..ca4cf56 100644 --- a/src/pages/Register.tsx +++ b/src/pages/Register.tsx @@ -58,12 +58,12 @@ function Register() { const _validateForm = (): boolean => { if (!formData.username || !formData.email || !formData.fullName) { - setValidationError('Bitte füllen Sie alle Pflichtfelder aus.'); + setValidationError(t('Bitte füllen Sie alle Pflichtfelder aus.')); return false; } if (!formData.email.includes('@')) { - setValidationError('Bitte geben Sie eine gültige E-Mail-Adresse ein.'); + setValidationError(t('Bitte geben Sie eine gültige E-Mail-Adresse ein.')); return false; } @@ -83,19 +83,19 @@ function Register() { if (!availabilityResult.available) { const errorMessage = availabilityResult.message || 'Username is not available'; if (errorMessage === 'Username is already taken') { - setValidationError('Benutzername ist bereits vergeben'); + setValidationError(t('Benutzername ist bereits vergeben')); setUsernameHighlight(true); } else { - setValidationError('Benutzername ist nicht verfügbar'); + setValidationError(t('Benutzername ist nicht verfügbar')); } return; } await register({ ...formData, registrationType: 'personal' }); - let message = 'Registrierung erfolgreich! Bitte prüfen Sie Ihre E-Mail (auch den Spam-Ordner) für den Link zum Setzen Ihres Passworts.'; + let message = t('Registrierung erfolgreich! Bitte prüfen Sie Ihre E-Mail (auch den Spam-Ordner) für den Link zum Setzen Ihres Passworts.'); if (hasPendingInvitation) { - message += ' Nach dem Setzen Ihres Passworts können Sie sich anmelden und Ihre Einladung annehmen.'; + message += t(' Nach dem Setzen Ihres Passworts können Sie sich anmelden und Ihre Einladung annehmen.'); } setSuccessMessage(message); @@ -104,7 +104,7 @@ function Register() { navigate('/login', { state: { registered: true, - message: 'Registrierung erfolgreich. Bitte prüfen Sie Ihre E-Mail für den Passwort-Link.', + message: t('Registrierung erfolgreich. Bitte prüfen Sie Ihre E-Mail für den Passwort-Link.'), ...(location.state || {}) } }); @@ -116,9 +116,9 @@ function Register() { const _getErrorMessage = () => { if (validationError) return validationError; - if (registerError) return typeof registerError === 'string' ? registerError : 'Registration failed'; - if (msalError) return typeof msalError === 'string' ? msalError : 'Microsoft registration failed'; - if (availabilityError) return typeof availabilityError === 'string' ? availabilityError : 'Username availability check failed'; + if (registerError) return typeof registerError === 'string' ? registerError : t('Registrierung fehlgeschlagen'); + if (msalError) return typeof msalError === 'string' ? msalError : t('Microsoft-Registrierung fehlgeschlagen'); + if (availabilityError) return typeof availabilityError === 'string' ? availabilityError : t('Benutzernamen-Prüfung fehlgeschlagen'); return null; }; @@ -163,7 +163,7 @@ function Register() { onBlur={() => setUsernameFocused(false)} className={`${styles.input} ${usernameFocused || formData.username ? styles.focused : ''} ${usernameHighlight ? styles.usernameError : ''}`} /> - +
@@ -177,7 +177,7 @@ function Register() { onBlur={() => setEmailFocused(false)} className={`${styles.input} ${emailFocused || formData.email ? styles.focused : ''}`} /> - +
@@ -200,7 +200,7 @@ function Register() {

- Mit der Registrierung stimmen Sie unseren Datenschutzbestimmungen zur KI-Nutzung zu. + {t('Mit der Registrierung stimmen Sie unseren Datenschutzbestimmungen zur KI-Nutzung zu.')}

@@ -209,7 +209,7 @@ function Register() { onClick={handleSubmit} disabled={isLoading || isChecking} > - {isLoading ? "Registrierung läuft..." : isChecking ? "Benutzername wird geprüft..." : 'Kostenlos registrieren'} + {isLoading ? t('Registrierung läuft…') : isChecking ? t('Benutzername wird geprüft…') : t('Kostenlos registrieren')} )} @@ -220,7 +220,7 @@ function Register() { className={styles.textButton} onClick={() => navigate("/login", { state: location.state })} > - Jetzt anmelden + {t('Jetzt anmelden')}
diff --git a/src/pages/Reset.tsx b/src/pages/Reset.tsx index d93e9f3..319d808 100644 --- a/src/pages/Reset.tsx +++ b/src/pages/Reset.tsx @@ -31,11 +31,11 @@ function Reset() { // Validate token exists and format if (!token) { - setTokenError('Ungültiger Reset-Link. Bitte fordern Sie einen neuen Link an.'); + setTokenError(t('Ungültiger Reset-Link. Bitte fordern Sie einen neuen Link an.')); } else if (!_isValidUUID(token)) { - setTokenError('Ungültiger Reset-Link. Bitte fordern Sie einen neuen Link an.'); + setTokenError(t('Ungültiger Reset-Link. Bitte fordern Sie einen neuen Link an.')); } - }, [token]); + }, [token, t]); const _isValidUUID = (str: string): boolean => { const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; @@ -44,12 +44,12 @@ function Reset() { const validateForm = (): boolean => { if (!password || password.length < 8) { - setValidationError('Passwort muss mindestens 8 Zeichen lang sein.'); + setValidationError(t('Passwort muss mindestens 8 Zeichen lang sein.')); return false; } if (password !== confirmPassword) { - setValidationError('Die Passwörter stimmen nicht überein.'); + setValidationError(t('Die Passwörter stimmen nicht überein.')); return false; } @@ -65,28 +65,28 @@ function Reset() { } if (!token) { - setValidationError('Token fehlt. Bitte fordern Sie einen neuen Reset-Link an.'); + setValidationError(t('Token fehlt. Bitte fordern Sie einen neuen Reset-Link an.')); return; } try { await resetPassword(token, password); - setSuccessMessage('Passwort erfolgreich gesetzt! Sie werden zum Login weitergeleitet...'); + setSuccessMessage(t('Passwort erfolgreich gesetzt! Sie werden zum Login weitergeleitet…')); // Redirect to login after delay setTimeout(() => { navigate('/login', { state: { passwordReset: true, - message: 'Passwort erfolgreich geändert. Bitte melden Sie sich an.' + message: t('Passwort erfolgreich geändert. Bitte melden Sie sich an.') } }); }, 3000); } catch (err: any) { // Error is already set by the hook - const errorMessage = err?.response?.data?.detail || err?.message || 'Passwort-Zurücksetzung fehlgeschlagen.'; + const errorMessage = err?.response?.data?.detail || err?.message || t('Passwort-Zurücksetzung fehlgeschlagen.'); if (errorMessage.includes('abgelaufen') || errorMessage.includes('expired') || errorMessage.includes('Ungültig') || errorMessage.includes('invalid')) { - setValidationError('Der Reset-Link ist ungültig oder abgelaufen. Bitte fordern Sie einen neuen Link an.'); + setValidationError(t('Der Reset-Link ist ungültig oder abgelaufen. Bitte fordern Sie einen neuen Link an.')); } else { setValidationError(errorMessage); } @@ -115,7 +115,7 @@ function Reset() { className={styles.textButton} onClick={() => navigate("/password-reset-request")} > - Neuen Reset-Link anfordern + {t('Neuen Reset-Link anfordern')}
@@ -124,7 +124,7 @@ function Reset() { className={styles.textButton} onClick={() => navigate("/login")} > - Login + {t('Login')}
@@ -200,7 +200,7 @@ function Reset() { className={`${styles.button} ${styles.loginButton}`} disabled={isLoading} > - {isLoading ? "Wird gespeichert..." : "Passwort setzen"} + {isLoading ? t('Wird gespeichert…') : t('Passwort setzen')} )} @@ -211,7 +211,7 @@ function Reset() { className={styles.textButton} onClick={() => navigate("/login")} > - Login + {t('Login')} diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index af96433..d0e8734 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -60,7 +60,7 @@ const ProfileEditModal: React.FC = ({ isOpen, onClose, us await onSave(formData); onClose(); } catch (err: any) { - setError(err.message || 'Fehler beim Speichern des Profils'); + setError(err.message || t('Fehler beim Speichern des Profils')); } finally { setIsSaving(false); } @@ -520,17 +520,17 @@ export const SettingsPage: React.FC = () => { {currentUser && (
-
Benutzername{currentUser.username}
-
Name{currentUser.fullName || '-'}
-
E-Mail{currentUser.email || '-'}
+
{t('Benutzername')}{currentUser.username}
+
{t('Name')}{currentUser.fullName || '-'}
+
{t('E-Mail')}{currentUser.email || '-'}
)}
-

Ueber

+

{t('Applikation')}

-
Version2.0.0
-
Build2026.03.23
+
{t('Version')}2.0.0
+
{t('Build')}2026.03.23
diff --git a/src/pages/admin/AccessManagementHub.tsx b/src/pages/admin/AccessManagementHub.tsx index df428f5..232471c 100644 --- a/src/pages/admin/AccessManagementHub.tsx +++ b/src/pages/admin/AccessManagementHub.tsx @@ -24,18 +24,13 @@ import { FeatureInstanceWizard } from './wizards/FeatureInstanceWizard'; import { InstanceHierarchyView } from './InstanceHierarchyView'; import { useLanguage } from '../../providers/language/LanguageContext'; -import { labelAsI18nKey } from '../../types/mandate'; function getMandateName(mandate: Mandate): string { - if (mandate.label) return mandate.label; - if (typeof mandate.name === 'object') { - return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id; - } - return mandate.name || mandate.id; + return mandate.label || mandate.name || mandate.id; } function getFeatureLabel(feature: Feature, t: (k: string) => string): string { - return t(labelAsI18nKey(feature.label, feature.code)); + return t(feature.label || feature.code); } export interface InstanceWithStats extends FeatureInstance { @@ -146,15 +141,19 @@ export const AccessManagementHub: React.FC = () => { const result = await syncInstanceRoles(selectedMandateId, instance.id, true); if (result.success && result.data) { showSuccess( - 'Rollen synchronisiert', - `Hinzugefügt: ${result.data.added}, Entfernt: ${result.data.removed}, Unverändert: ${result.data.unchanged}` + t('Rollen synchronisiert'), + t('Hinzugefügt: {added}, Entfernt: {removed}, Unverändert: {unchanged}', { + added: result.data.added, + removed: result.data.removed, + unchanged: result.data.unchanged, + }) ); fetchInstances(selectedMandateId, selectedFeatureCode || undefined); } else { - showError('Synchronisierung fehlgeschlagen', result.error || 'Fehler beim Synchronisieren'); + showError(t('Synchronisierung fehlgeschlagen'), result.error || t('Fehler beim Synchronisieren')); } } catch { - showError('Fehler', 'Rollen konnten nicht synchronisiert werden'); + showError(t('Fehler'), t('Rollen konnten nicht synchronisiert werden')); } }; @@ -295,7 +294,7 @@ export const AccessManagementHub: React.FC = () => { return { id: inst.id, label: inst.label, - featureLabel: feature ? getFeatureLabel(feature) : inst.featureCode, + featureLabel: feature ? getFeatureLabel(feature, t) : inst.featureCode, userCount: inst.userCount ?? 0, }; }), @@ -307,9 +306,11 @@ export const AccessManagementHub: React.FC = () => {
⚠️ -

Fehler: {error}

+

+ {t('Fehler')}: {error} +

@@ -320,9 +321,9 @@ export const AccessManagementHub: React.FC = () => {
-

Zugriffsverwaltung

+

{t('Zugriffsverwaltung')}

- Feature-Instanzen, Benutzer und Rollen an einem Ort verwalten + {t('Feature-Instanzen, Benutzer und Rollen an einem Ort verwalten')}

@@ -334,7 +335,7 @@ export const AccessManagementHub: React.FC = () => {
{ } disabled={loading} > - Aktualisieren + {t('Aktualisieren')}
)} @@ -398,21 +399,21 @@ export const AccessManagementHub: React.FC = () => { className={viewMode === 'list' ? hubStyles.viewModeActive : hubStyles.viewModeButton} onClick={() => setViewMode('list')} > - Listenansicht + {t('Listenansicht')}
- Mandanten verwalten + {t('Mandanten verwalten')} - Mandant-Benutzer + {t('Mandant-Benutzer')} @@ -423,7 +424,7 @@ export const AccessManagementHub: React.FC = () => { instancesByMandate={instancesByMandate} instanceUsersMap={instanceUsersMap} features={features} - getFeatureLabel={getFeatureLabel} + getFeatureLabel={(f) => getFeatureLabel(f, t)} loading={hierarchyUsersLoading} onOpenDetail={handleOpenDetail} /> @@ -432,7 +433,7 @@ export const AccessManagementHub: React.FC = () => {

{t('Kein Mandant ausgewählt')}

- Wählen Sie einen Mandanten, um dessen Feature-Instanzen und Zugriffe zu verwalten. + {t('Wählen Sie einen Mandanten, um dessen Feature-Instanzen und Zugriffe zu verwalten.')}

) : ( @@ -444,7 +445,7 @@ export const AccessManagementHub: React.FC = () => { {loading || statsLoading ? '…' : overviewStats.instances} - Instanzen + {t('Instanzen')}
@@ -469,7 +470,7 @@ export const AccessManagementHub: React.FC = () => {
- Beziehungen + {t('Beziehungen')}
{relationshipData.mandateName}
@@ -480,7 +481,7 @@ export const AccessManagementHub: React.FC = () => { ))} {relationshipData.instances.length > 5 && (
- +{relationshipData.instances.length - 5} weitere + +{relationshipData.instances.length - 5} {t('weitere')}
)}
@@ -491,7 +492,7 @@ export const AccessManagementHub: React.FC = () => {
-

Feature-Instanzen

+

{t('Feature-Instanzen')}

{loading && filteredInstances.length === 0 ? (
@@ -502,14 +503,14 @@ export const AccessManagementHub: React.FC = () => {

{t('Keine Feature-Instanzen')}

- Erstellen Sie eine neue Instanz oder wählen Sie ein anderes Feature. + {t('Erstellen Sie eine neue Instanz oder wählen Sie ein anderes Feature.')}

) : ( @@ -526,8 +527,12 @@ export const AccessManagementHub: React.FC = () => {
{getFeatureLabel(features.find((f) => f.code === inst.featureCode) || { code: inst.featureCode, label: inst.featureCode }, t)} - {inst.userCount ?? '—'} Benutzer - {inst.roleCount ?? '—'} Rollen + + {inst.userCount ?? '—'} {t('Benutzer')} + + + {inst.roleCount ?? '—'} {t('Rollen')} +
diff --git a/src/pages/admin/AdminFeatureAccessPage.tsx b/src/pages/admin/AdminFeatureAccessPage.tsx index 26c59ee..7a0707a 100644 --- a/src/pages/admin/AdminFeatureAccessPage.tsx +++ b/src/pages/admin/AdminFeatureAccessPage.tsx @@ -20,7 +20,6 @@ import { TextField } from '../../components/UiComponents/TextField'; import styles from './Admin.module.css'; import { useLanguage } from '../../providers/language/LanguageContext'; -import { labelAsI18nKey } from '../../types/mandate'; export const AdminFeatureAccessPage: React.FC = () => { const { t } = useLanguage(); @@ -94,7 +93,7 @@ export const AdminFeatureAccessPage: React.FC = () => { render: (value: string) => { const feature = features.find(f => f.code === value); if (feature) { - return t(labelAsI18nKey(feature.label, value)); + return t(feature.label || value); } return value; } @@ -122,7 +121,7 @@ export const AdminFeatureAccessPage: React.FC = () => { try { // Validate label if (!createLabel || createLabel.trim() === '') { - showError('Fehler', 'Label ist erforderlich.'); + showError(t('Fehler'), t('Label ist erforderlich.')); setIsSubmitting(false); return; } @@ -132,7 +131,7 @@ export const AdminFeatureAccessPage: React.FC = () => { if (createFeatureCode === 'chatbot') { // Validate required fields if (!chatbotSystemPrompt || chatbotSystemPrompt.trim() === '') { - showError('Fehler', 'System Prompt ist erforderlich für Chatbot-Instanzen.'); + showError(t('Fehler'), t('System Prompt ist erforderlich für Chatbot-Instanzen.')); setIsSubmitting(false); return; } @@ -176,9 +175,9 @@ export const AdminFeatureAccessPage: React.FC = () => { setChatbotAllowedProviders([]); fetchInstances(selectedMandateId); loadFeatures(); // Refresh global navigation cache - showSuccess('Feature-Instanz erstellt', `Die Instanz "${createLabel}" wurde erfolgreich erstellt.`); + showSuccess(t('Feature-Instanz erstellt'), t('Die Instanz "{name}" wurde erfolgreich erstellt.', { name: createLabel })); } else { - showError('Fehler', result.error || 'Fehler beim Erstellen der Feature-Instanz'); + showError(t('Fehler'), result.error || t('Fehler beim Erstellen der Feature-Instanz')); } } finally { setIsSubmitting(false); @@ -228,7 +227,7 @@ export const AdminFeatureAccessPage: React.FC = () => { if (editingInstance.featureCode === 'chatbot') { // Validate required fields if (!chatbotSystemPrompt || chatbotSystemPrompt.trim() === '') { - showError('Fehler', 'System Prompt ist erforderlich für Chatbot-Instanzen.'); + showError(t('Fehler'), t('System Prompt ist erforderlich für Chatbot-Instanzen.')); setIsSubmitting(false); return; } @@ -270,9 +269,9 @@ export const AdminFeatureAccessPage: React.FC = () => { setChatbotAllowedProviders([]); fetchInstances(selectedMandateId); loadFeatures(); // Refresh global navigation cache - showSuccess('Feature-Instanz aktualisiert', `Die Instanz "${data.label}" wurde erfolgreich aktualisiert.`); + showSuccess(t('Feature-Instanz aktualisiert'), t('Die Instanz "{name}" wurde erfolgreich aktualisiert.', { name: data.label })); } else { - showError('Fehler', result.error || 'Fehler beim Aktualisieren der Feature-Instanz'); + showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren der Feature-Instanz')); } } finally { setIsSubmitting(false); @@ -285,10 +284,10 @@ export const AdminFeatureAccessPage: React.FC = () => { const result = await deleteInstance(selectedMandateId, instanceId); if (result.success) { loadFeatures(); // Refresh global navigation cache - showSuccess('Instanz gelöscht', 'Die Feature-Instanz wurde gelöscht.'); + showSuccess(t('Instanz gelöscht'), t('Die Feature-Instanz wurde gelöscht.')); return true; } else { - showError('Fehler', result.error || 'Fehler beim Löschen der Feature-Instanz'); + showError(t('Fehler'), result.error || t('Fehler beim Löschen der Feature-Instanz')); return false; } }; @@ -301,11 +300,15 @@ export const AdminFeatureAccessPage: React.FC = () => { const result = await syncInstanceRoles(selectedMandateId, instance.id, true); if (result.success && result.data) { showSuccess( - 'Rollen synchronisiert', - `Hinzugefügt: ${result.data.added}\nEntfernt: ${result.data.removed}\nUnverändert: ${result.data.unchanged}` + t('Rollen synchronisiert'), + t('Hinzugefügt: {added}\nEntfernt: {removed}\nUnverändert: {unchanged}', { + added: result.data.added, + removed: result.data.removed, + unchanged: result.data.unchanged, + }) ); } else { - showError('Synchronisierung fehlgeschlagen', result.error || 'Fehler beim Synchronisieren der Rollen'); + showError(t('Synchronisierung fehlgeschlagen'), result.error || t('Fehler beim Synchronisieren der Rollen')); } } finally { setSyncingInstance(null); @@ -314,18 +317,14 @@ export const AdminFeatureAccessPage: React.FC = () => { // Get mandate name const getMandateName = (mandate: Mandate) => { - if (mandate.label) return mandate.label; - if (typeof mandate.name === 'object') { - return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id; - } - return mandate.name || mandate.id; + return mandate.label || mandate.name || mandate.id; }; // Get feature label const getFeatureLabel = (code: string) => { const feature = features.find(f => f.code === code); if (feature) { - return t(labelAsI18nKey(feature.label, code)); + return t(feature.label || code); } return code; }; @@ -337,7 +336,7 @@ export const AdminFeatureAccessPage: React.FC = () => { ⚠️

Fehler: {error}

@@ -348,7 +347,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
-

Feature-Instanzen

+

{t('Feature-Instanzen')}

{t('Verwalten Sie Feature-Instanzen für jeden')}

@@ -358,7 +357,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
{ onClick={() => refreshUsers()} disabled={usersLoading} > - Aktualisieren + {t('Aktualisieren')}
)} @@ -475,10 +476,14 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => { {selectedOption && (
- Mandant: {selectedOption.mandateName} + + {t('Mandant')}: {selectedOption.mandateName} + | - Instanz: {selectedOption.instanceLabel} ({selectedOption.featureCode}) + + {t('Instanz')}: {selectedOption.instanceLabel} ({selectedOption.featureCode}) +
)} @@ -497,7 +502,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => { {/* Warning if no roles available */} {selectedInstance && instanceRoles.length === 0 && !usersLoading && ( -
+
⚠️ {t('Diese Instanz hat noch keine')}
@@ -595,7 +600,9 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
setEditingUser(null)}>
e.stopPropagation()}>
-

Rollen bearbeiten: {editingUser.username}

+

+ {t('Rollen bearbeiten')}: {editingUser.username} +

@@ -297,7 +294,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
{ checked={showExpired} onChange={(e) => setShowExpired(e.target.checked)} /> - Abgelaufene anzeigen + {t('Abgelaufene anzeigen')}
@@ -314,13 +312,13 @@ export const AdminInvitationsPage: React.FC = () => { onClick={() => fetchInvitations(selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed })} disabled={loading} > - Aktualisieren + {t('Aktualisieren')}
)} @@ -332,7 +330,7 @@ export const AdminInvitationsPage: React.FC = () => {

{t('Kein Mandant ausgewählt')}

- Wählen Sie einen Mandanten aus, um dessen Einladungen zu verwalten. + {t('Wählen Sie einen Mandanten aus, um dessen Einladungen zu verwalten.')}

) : ( @@ -416,7 +414,7 @@ export const AdminInvitationsPage: React.FC = () => {
setShowUrlModal(null)}>
e.stopPropagation()}>
-

Einladungs-Link

+

{t('Einladungs-Link')}

- Einladung für Benutzer {showUrlModal.targetUsername}: + {t('Einladung für Benutzer')} {showUrlModal.targetUsername}:

{ title={t('In Zwischenablage kopieren')} > - {copySuccess ? ' Kopiert!' : ' Kopieren'} + {copySuccess ? ` ${t('Kopiert!')}` : ` ${t('Kopieren')}`}

- Dieser Link kann nur von Benutzer {showUrlModal.targetUsername} verwendet werden. + {t('Dieser Link kann nur von Benutzer')} {showUrlModal.targetUsername}{' '} + {t('verwendet werden.')}

{showUrlModal.email && (

- {showUrlModal.emailSent - ? `✓ Email wurde an ${showUrlModal.email} gesendet` - : `Email-Adresse: ${showUrlModal.email} (nicht gesendet)`} + {showUrlModal.emailSent + ? `✓ ${t('E-Mail wurde an {email} gesendet', { email: showUrlModal.email })}` + : `${t('E-Mail-Adresse')}: ${showUrlModal.email} (${t('nicht gesendet')})`}

)}

- Gültig bis: {formatDate(showUrlModal.expiresAt)} + {t('Gültig bis')}: {formatDate(showUrlModal.expiresAt)}

@@ -464,7 +463,7 @@ export const AdminInvitationsPage: React.FC = () => { className={styles.primaryButton} onClick={() => setShowUrlModal(null)} > - Schliessen + {t('Schliessen')}
diff --git a/src/pages/admin/AdminLogsPage.tsx b/src/pages/admin/AdminLogsPage.tsx index dce190f..1d3270f 100644 --- a/src/pages/admin/AdminLogsPage.tsx +++ b/src/pages/admin/AdminLogsPage.tsx @@ -50,11 +50,11 @@ export const AdminLogsPage: React.FC = () => { setLines(response.data.lines || []); setLogDir(response.data.logDir || ''); } catch (err: any) { - setError(err.response?.data?.detail || 'Fehler beim Laden der Logs'); + setError(err.response?.data?.detail || t('Fehler beim Laden der Logs')); } finally { setLoading(false); } - }, [count]); + }, [count, t]); const _handleLoad = () => { const parsed = parseInt(countInput, 10); @@ -82,7 +82,7 @@ export const AdminLogsPage: React.FC = () => { document.body.removeChild(a); URL.revokeObjectURL(url); } catch (err: any) { - setError(err.response?.data?.detail || 'Fehler beim Download'); + setError(err.response?.data?.detail || t('Fehler beim Download')); } }; @@ -110,9 +110,7 @@ export const AdminLogsPage: React.FC = () => {

{t('Gateway-Logs')}

- {lines.length > 0 - ? `${lines.length} Einträge` - : 'Keine Logs geladen'} + {lines.length > 0 ? t('{count} Einträge', { count: lines.length }) : t('Keine Logs geladen')} {logDir && — {logDir}}

@@ -123,14 +121,14 @@ export const AdminLogsPage: React.FC = () => { disabled={lines.length === 0} title={t('Log herunterladen')} > - Download + {t('Download')}
- + { onClick={_handleLoad} disabled={loading} > - Laden + {t('Laden')}
@@ -157,7 +155,7 @@ export const AdminLogsPage: React.FC = () => { checked={autoRefresh} onChange={(e) => setAutoRefresh(e.target.checked)} /> - Auto-Refresh (5s) + {t('Auto-Refresh (5s)')}
@@ -184,9 +182,7 @@ export const AdminLogsPage: React.FC = () => {
📋

{t('Keine Logs geladen')}

-

- Gib die gewünschte Anzahl Einträge ein und klicke auf "Laden". -

+

{t('Gib die gewünschte Anzahl Einträge ein und klicke auf „Laden“.')}

)} {loading && lines.length === 0 && ( diff --git a/src/pages/admin/AdminMandateRolePermissionsPage.tsx b/src/pages/admin/AdminMandateRolePermissionsPage.tsx index e619011..5117382 100644 --- a/src/pages/admin/AdminMandateRolePermissionsPage.tsx +++ b/src/pages/admin/AdminMandateRolePermissionsPage.tsx @@ -126,11 +126,8 @@ export const AdminMandateRolePermissionsPage: React.FC = () => { } }, [selectedMandateId, scopeFilter, fetchRoles]); - // Get description text from multilingual object - const getTextValue = (value: string | { [key: string]: string } | undefined): string => { - if (!value) return ''; - if (typeof value === 'string') return value; - return value.de || value.en || Object.values(value)[0] || ''; + const getTextValue = (value: string | undefined): string => { + return value || ''; }; // Toggle role expansion @@ -148,20 +145,20 @@ export const AdminMandateRolePermissionsPage: React.FC = () => { if (role.isSystemRole) { return ( - System-Template + {t('System-Template')} ); } if (!role.mandateId) { return ( - Template + {t('Template')} ); } return ( - Mandant + {t('Mandant')} ); }; @@ -181,11 +178,11 @@ export const AdminMandateRolePermissionsPage: React.FC = () => { setTemplateFixResult(data.templateRoleAssignments || null); setCleanupPhase('preview'); } catch (err: any) { - setCleanupError(err?.response?.data?.detail || err?.message || 'Fehler beim Laden der Duplikate'); + setCleanupError(err?.response?.data?.detail || err?.message || t('Fehler beim Laden der Duplikate')); } finally { setCleanupLoading(false); } - }, []); + }, [t]); const _executeCleanup = useCallback(async () => { setCleanupLoading(true); @@ -201,11 +198,11 @@ export const AdminMandateRolePermissionsPage: React.FC = () => { fetchRoles(selectedMandateId, { scopeFilter }); } } catch (err: any) { - setCleanupError(err?.response?.data?.detail || err?.message || 'Fehler beim Bereinigen'); + setCleanupError(err?.response?.data?.detail || err?.message || t('Fehler beim Bereinigen')); } finally { setCleanupLoading(false); } - }, [selectedMandateId, scopeFilter, fetchRoles]); + }, [selectedMandateId, scopeFilter, fetchRoles, t]); const _closeCleanupModal = useCallback(() => { setShowCleanupModal(false); @@ -226,9 +223,11 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
⚠️ -

Fehler beim Laden: {error}

+

+ {t('Fehler beim Laden')}: {error} +

@@ -242,10 +241,10 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {

- Rollen-Berechtigungen + {t('Rollen-Berechtigungen')}

- Verwalten Sie die Zugriffsrechte für Mandanten- und globale Rollen + {t('Verwalten Sie die Zugriffsrechte für Mandanten- und globale Rollen')}

@@ -255,14 +254,14 @@ export const AdminMandateRolePermissionsPage: React.FC = () => { disabled={loading} title={t('Doppelte Regeln finden und bereinigen')} > - Duplikate bereinigen + {t('Duplikate bereinigen')}
@@ -286,7 +285,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
NameScope{t('Name')}{t('Scope')}
{tpl.label} - {SCOPE_LABELS[(tpl.templateScope as AutoTemplateScope) || 'user']} + {scopeLabels[(tpl.templateScope as AutoTemplateScope) || 'user']}
- - - - + + + + @@ -500,7 +502,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => { {templateFixResult && templateFixResult.invalidAssignments > 0 && (

- Template-Rollen-Zuweisungen + {t('Template-Rollen-Zuweisungen')}

@@ -516,7 +518,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => { {cleanupPhase === 'done' ? templateFixResult.fixedCount : templateFixResult.invalidAssignments}
- {cleanupPhase === 'done' ? 'Repariert' : 'Zu reparieren'} + {cleanupPhase === 'done' ? t('Repariert') : t('Zu reparieren')}
@@ -528,7 +530,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => { - + @@ -584,7 +586,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => { onClick={_executeCleanup} disabled={cleanupLoading} > - Bereinigung ausfuehren + {t('Bereinigung ausführen')} )} diff --git a/src/pages/admin/AdminMandateRolesPage.tsx b/src/pages/admin/AdminMandateRolesPage.tsx index a3bad48..7357bde 100644 --- a/src/pages/admin/AdminMandateRolesPage.tsx +++ b/src/pages/admin/AdminMandateRolesPage.tsx @@ -92,11 +92,8 @@ export const AdminMandateRolesPage: React.FC = () => { }); }, [selectedMandateId, fetchRoles]); - // Get description text - const getDescriptionText = (desc: string | { [key: string]: string } | undefined) => { - if (!desc) return '-'; - if (typeof desc === 'string') return desc; - return desc.de || desc.en || Object.values(desc)[0] || '-'; + const getDescriptionText = (desc: string | undefined) => { + return desc || '-'; }; // Table columns - scopeType is now a backend-computed field @@ -117,7 +114,7 @@ export const AdminMandateRolesPage: React.FC = () => { sortable: false, filterable: false, width: 250, - formatter: (value: string | { [key: string]: string }) => getDescriptionText(value) + formatter: (value: string) => getDescriptionText(value) }, { key: 'scopeType', @@ -130,20 +127,20 @@ export const AdminMandateRolesPage: React.FC = () => { if (value === 'system') { return ( - System-Template + {t('System-Template')} ); } if (value === 'global') { return ( - Template + {t('Template')} ); } return ( - Mandant + {t('Mandant')} ); } @@ -173,7 +170,7 @@ export const AdminMandateRolesPage: React.FC = () => { }); } return fields; - }, [backendAttributes]); + }, [backendAttributes, t]); // Form attributes from backend - for edit form // NOTE: mandateId/featureInstanceId/featureCode are IMMUTABLE - only description can be edited @@ -193,21 +190,13 @@ export const AdminMandateRolesPage: React.FC = () => { }, [backendAttributes]); // Handle create role - const handleCreateRole = async (data: { roleLabel: string; description?: string | { [key: string]: string }; scope: 'mandate' | 'global' }) => { + const handleCreateRole = async (data: { roleLabel: string; description?: string; scope: 'mandate' | 'global' }) => { if (!selectedMandateId) return; setIsSubmitting(true); try { - // Ensure description is always a multilingual object - let description: { [key: string]: string } = {}; - if (typeof data.description === 'string') { - description = { de: data.description, en: data.description }; - } else if (data.description && typeof data.description === 'object') { - description = data.description; - } - const roleData: RoleCreate = { roleLabel: data.roleLabel.toLowerCase().replace(/\s+/g, '_'), - description: description, + description: data.description, mandateId: data.scope === 'mandate' ? selectedMandateId : undefined }; @@ -217,11 +206,11 @@ export const AdminMandateRolesPage: React.FC = () => { setShowCreateModal(false); await fetchRoles(selectedMandateId, { scopeFilter }); } else { - showError('Fehler', result.error || 'Fehler beim Erstellen der Rolle'); + showError(t('Fehler'), result.error || t('Fehler beim Erstellen der Rolle')); } } catch (err: any) { console.error('Create role error:', err); - showError('Fehler', err.message || 'Fehler beim Erstellen der Rolle'); + showError(t('Fehler'), err.message || t('Fehler beim Erstellen der Rolle')); } finally { setIsSubmitting(false); } @@ -232,19 +221,11 @@ export const AdminMandateRolesPage: React.FC = () => { if (!editingRole) return; setIsSubmitting(true); try { - // Ensure description is always a multilingual object - let description: { [key: string]: string } | undefined; - if (typeof data.description === 'string') { - description = { de: data.description, en: data.description }; - } else if (data.description && typeof data.description === 'object') { - description = data.description as { [key: string]: string }; - } - // Convert scope to mandateId - NOTE: Context fields are IMMUTABLE per concept! // We should not be changing mandateId after creation const updateData: RoleUpdate = { roleLabel: data.roleLabel, - description: description, + description: data.description, // mandateId is immutable - don't include in update }; @@ -256,11 +237,11 @@ export const AdminMandateRolesPage: React.FC = () => { await fetchRoles(selectedMandateId, { scopeFilter }); } } else { - showError('Fehler', result.error || 'Fehler beim Aktualisieren der Rolle'); + showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren der Rolle')); } } catch (err: any) { console.error('Update role error:', err); - showError('Fehler', err.message || 'Fehler beim Aktualisieren der Rolle'); + showError(t('Fehler'), err.message || t('Fehler beim Aktualisieren der Rolle')); } finally { setIsSubmitting(false); } @@ -269,7 +250,7 @@ export const AdminMandateRolesPage: React.FC = () => { // Handle delete role (confirmation handled by DeleteActionButton) const handleDeleteRole = async (role: Role) => { if (role.isSystemRole) { - showWarning('Nicht erlaubt', 'System-Rollen können nicht gelöscht werden.'); + showWarning(t('Nicht erlaubt'), t('System-Rollen können nicht gelöscht werden.')); return; } @@ -278,7 +259,7 @@ export const AdminMandateRolesPage: React.FC = () => { // Refetch to update the list await fetchRoles(selectedMandateId, { scopeFilter }); } else { - showError('Fehler', result.error || 'Fehler beim Löschen der Rolle'); + showError(t('Fehler'), result.error || t('Fehler beim Löschen der Rolle')); } }; @@ -289,11 +270,7 @@ export const AdminMandateRolesPage: React.FC = () => { // Get mandate name const getMandateName = (mandate: Mandate) => { - if (mandate.label) return mandate.label; - if (typeof mandate.name === 'object') { - return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id; - } - return mandate.name || mandate.id; + return mandate.label || mandate.name || mandate.id; }; if (error && !selectedMandateId) { @@ -301,9 +278,11 @@ export const AdminMandateRolesPage: React.FC = () => {
⚠️ -

Fehler: {error}

+

+ {t('Fehler')}: {error} +

@@ -323,14 +302,14 @@ export const AdminMandateRolesPage: React.FC = () => { className={styles.secondaryButton} onClick={() => navigate('/admin/mandate-role-permissions')} > - Rollen-Berechtigungen + {t('Rollen-Berechtigungen')} @@ -340,7 +319,7 @@ export const AdminMandateRolesPage: React.FC = () => {
@@ -377,13 +356,13 @@ export const AdminMandateRolesPage: React.FC = () => { onClick={() => fetchRoles(selectedMandateId, { scopeFilter })} disabled={loading} > - Aktualisieren + {t('Aktualisieren')}
)} @@ -394,9 +373,10 @@ export const AdminMandateRolesPage: React.FC = () => {
- System-Templates (admin, user, viewer) werden bei der Mandant-Erstellung automatisch als Mandanten-Instanz-Rollen kopiert. - Templates selbst können nicht gelöscht werden. - Mandanten-Rollen gelten nur für den ausgewählten Mandanten und sind den Benutzern zuweisbar. + {t('System-Templates')}{' '} + {t('(admin, user, viewer) werden bei der Mandant-Erstellung automatisch als Mandanten-Instanz-Rollen kopiert. Templates selbst können nicht gelöscht werden.')}{' '} + {t('Mandanten-Rollen')}{' '} + {t('gelten nur für den ausgewählten Mandanten und sind den Benutzern zuweisbar.')}
)} @@ -407,7 +387,7 @@ export const AdminMandateRolesPage: React.FC = () => {

{t('Kein Mandant ausgewählt')}

- Wählen Sie einen Mandanten aus, um dessen Rollen zu verwalten. + {t('Wählen Sie einen Mandanten aus, um dessen Rollen zu verwalten.')}

) : ( @@ -428,12 +408,12 @@ export const AdminMandateRolesPage: React.FC = () => { type: 'edit' as const, onAction: handleEditClick, title: t('Rolle bearbeiten'), - disabled: (row: Role) => row.isSystemRole ? { disabled: true, message: 'System-Rollen können nicht bearbeitet werden' } : false + disabled: (row: Role) => row.isSystemRole ? { disabled: true, message: t('System-Rollen können nicht bearbeitet werden') } : false }, { type: 'delete' as const, title: t('Rolle löschen'), - disabled: (row: Role) => row.isSystemRole ? { disabled: true, message: 'System-Rollen können nicht gelöscht werden' } : false + disabled: (row: Role) => row.isSystemRole ? { disabled: true, message: t('System-Rollen können nicht gelöscht werden') } : false } ]} onDelete={handleDeleteRole} @@ -472,7 +452,7 @@ export const AdminMandateRolesPage: React.FC = () => { mode="create" onSubmit={handleCreateRole} onCancel={() => setShowCreateModal(false)} - submitButtonText={isSubmitting ? 'Erstelle...' : 'Rolle erstellen'} + submitButtonText={isSubmitting ? t('Erstelle…') : t('Rolle erstellen')} cancelButtonText={t('Abbrechen')} /> )} @@ -486,7 +466,9 @@ export const AdminMandateRolesPage: React.FC = () => {
setEditingRole(null)}>
e.stopPropagation()}>
-

Rolle bearbeiten: {editingRole.roleLabel}

+

+ {t('Rolle bearbeiten')}: {editingRole.roleLabel} +

@@ -172,7 +174,7 @@ export const AdminMandatesPage: React.FC = () => {
-

Mandanten

+

{t('Mandanten')}

{t('Verwalten Sie alle Mandanten im')}

@@ -181,21 +183,21 @@ export const AdminMandatesPage: React.FC = () => { className={styles.secondaryButton} onClick={() => navigate('/admin/user-mandates')} > - Benutzer-Zuweisungen + {t('Benutzer-Zuweisungen')} {canCreate && ( )}
@@ -223,7 +225,7 @@ export const AdminMandatesPage: React.FC = () => { type: 'delete' as const, title: t('Soft-Löschen deaktivieren'), disabled: (row: Mandate) => row.isSystem - ? { disabled: true, message: 'System-Mandanten können nicht gelöscht werden' } + ? { disabled: true, message: t('System-Mandanten können nicht gelöscht werden') } : false }] : []), ]} @@ -233,7 +235,7 @@ export const AdminMandatesPage: React.FC = () => { onClick: handleHardDeleteMandate, title: t('Unwiderrufliches Löschen'), disabled: (row: Mandate) => row.isSystem - ? { disabled: true, message: 'System-Mandanten können nicht gelöscht werden' } + ? { disabled: true, message: t('System-Mandanten können nicht gelöscht werden') } : false, }] : []} onDelete={handleDeleteMandate} @@ -264,8 +266,8 @@ export const AdminMandatesPage: React.FC = () => {

- Stammdaten kommen aus dem Modell Mandate (API). Abrechnung wird in{' '} - BillingSettings pro Mandant gespeichert. + {t('Stammdaten kommen aus dem Modell')} Mandate {t('(API). Abrechnung wird in')}{' '} + BillingSettings {t('pro Mandant gespeichert.')}

{mandateAttrsLoading || createFormAttributes.length === 0 ? (
@@ -316,7 +318,8 @@ export const AdminMandatesPage: React.FC = () => {
- Dies ist ein System-Mandant. Er kann nicht gelöscht werden und der Name sollte nicht geändert werden. + {t('Dies ist ein')} {t('System-Mandant')}.{' '} + {t('Er kann nicht gelöscht werden und der Name sollte nicht geändert werden.')}
)} diff --git a/src/pages/admin/AdminUserAccessOverviewPage.tsx b/src/pages/admin/AdminUserAccessOverviewPage.tsx index f95fd50..65c70d0 100644 --- a/src/pages/admin/AdminUserAccessOverviewPage.tsx +++ b/src/pages/admin/AdminUserAccessOverviewPage.tsx @@ -24,7 +24,7 @@ interface UserOption { interface RoleInfo { id: string; roleLabel: string; - description: { [key: string]: string }; + description: string; scope: 'global' | 'mandate' | 'instance'; mandateId?: string; featureInstanceId?: string; @@ -55,7 +55,7 @@ interface MandateInfo { id: string; label: string; featureCode: string; - featureLabel: { [key: string]: string }; + featureLabel: string; roleIds: string[]; }[]; } @@ -69,7 +69,7 @@ function _mandateNameLine(mandate: MandateInfo): string { } function _roleDescriptionLine(role: RoleInfo): string { - return role.description?.de || role.description?.en || ''; + return role.description?.trim() || ''; } interface UserAccessOverview { @@ -106,14 +106,14 @@ export const AdminUserAccessOverviewPage: React.FC = () => { const response = await api.get('/api/admin/user-access-overview/users'); setUsers(response.data); } catch (err: any) { - setError(err?.response?.data?.detail || err?.message || 'Failed to fetch users'); + setError(err?.response?.data?.detail || err?.message || t('Benutzer konnten nicht geladen werden')); } finally { setLoadingUsers(false); } }; fetchUsers(); - }, []); + }, [t]); // Fetch access overview when user is selected useEffect(() => { @@ -134,7 +134,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => { // Auto-expand all mandates setExpandedMandates(new Set(data.mandates?.map((m: MandateInfo) => m.id) || [])); } catch (err: any) { - setError(err?.response?.data?.detail || err?.message || 'Failed to fetch overview'); + setError(err?.response?.data?.detail || err?.message || t('Zugriffsübersicht konnte nicht geladen werden')); setOverview(null); } finally { setLoading(false); @@ -142,7 +142,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => { }; fetchOverview(); - }, [selectedUserId]); + }, [selectedUserId, t]); const toggleRole = (roleId: string) => { setExpandedRoles(prev => { @@ -201,7 +201,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => { {overview.isSysAdmin && (
- {overview.sysAdminNote || 'Dieser Benutzer ist SysAdmin und hat vollen Systemzugriff.'} + {overview.sysAdminNote || t('Dieser Benutzer ist SysAdmin und hat vollen Systemzugriff.')}
)} @@ -225,7 +225,10 @@ export const AdminUserAccessOverviewPage: React.FC = () => { )} {_mandateNameLine(mandate)} - {mandateRoles.length} Mandantenrolle(n) · {mandate.featureInstances.length} Feature-Instanz(en) + {t('{r} Mandantenrolle(n) · {i} Feature-Instanz(en)', { + r: mandateRoles.length, + i: mandate.featureInstances.length, + })}
@@ -255,14 +258,14 @@ export const AdminUserAccessOverviewPage: React.FC = () => { )} -
Feature-Instanzen
+
{t('Feature-Instanzen')}
{mandate.featureInstances.length === 0 ? (

{t('Keine Feature-Instanzen zugewiesen')}

) : (
{mandate.featureInstances.map((instance) => { const instanceRoles = _resolveRoles(instance.roleIds); - const featureTitle = instance.featureLabel?.de || instance.featureCode; + const featureTitle = instance.featureLabel || instance.featureCode; return (
@@ -271,7 +274,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
{instanceRoles.length === 0 ? (

- Keine Rollen. + {t('Keine Rollen.')}

) : (
    @@ -310,10 +313,10 @@ export const AdminUserAccessOverviewPage: React.FC = () => { {globalRoles.length > 0 && ( <>

    - Globale Rollen + {t('Globale Rollen')}

    - Nicht an einen Mandanten gebunden. + {t('Nicht an einen Mandanten gebunden.')}

    {globalRoles.map((role) => ( @@ -367,15 +370,15 @@ export const AdminUserAccessOverviewPage: React.FC = () => {

    {t('Keine UI-Berechtigungen')}

    - Diesem Benutzer wurden keine expliziten UI-Berechtigungen zugewiesen. + {t('Diesem Benutzer wurden keine expliziten UI-Berechtigungen zugewiesen.')}

    ) : (
KontextItemTotalDuplikate{t('Kontext')}{t('Item')}{t('Total')}{t('Duplikate')}
{t('Rolle')} {t('Mandant')}Aktion{t('Aktion')}
- - + + @@ -418,8 +421,8 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
- Daten-Zugriffsrechte: ALL = Alle Datensätze, GROUP = Gruppen-Datensätze, - MY = Eigene Datensätze, NONE = Kein Zugriff + {t('Daten-Zugriffsrechte:')} ALL {t('= Alle Datensätze,')} GROUP{' '} + {t('= Gruppen-Datensätze,')} MY {t('= Eigene Datensätze,')} NONE {t('= Kein Zugriff')}
@@ -428,17 +431,17 @@ export const AdminUserAccessOverviewPage: React.FC = () => {

{t('Keine Datenberechtigungen')}

- Diesem Benutzer wurden keine expliziten Daten-Berechtigungen zugewiesen. + {t('Diesem Benutzer wurden keine expliziten Daten-Berechtigungen zugewiesen.')}

) : (
UI-ElementSichtbar{t('UI-Element')}{t('Sichtbar')} {t('Gewährt durch')}
- - + + - + @@ -530,15 +533,15 @@ export const AdminUserAccessOverviewPage: React.FC = () => {

{t('Keine Ressourcenberechtigungen')}

- Diesem Benutzer wurden keine expliziten Ressourcen-Berechtigungen zugewiesen. + {t('Diesem Benutzer wurden keine expliziten Ressourcen-Berechtigungen zugewiesen.')}

) : (
Tabelle/FeldLesen{t('Tabelle/Feld')}{t('Lesen')} {t('Erstellen')}Update{t('Update')} {t('Löschen')} {t('Gewährt durch')}
- - + + @@ -578,12 +581,14 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
⚠️ -

Fehler: {error}

+

+ {t('Fehler')}: {error} +

@@ -604,7 +609,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
@@ -629,7 +634,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => { onClick={() => setSelectedUserId(selectedUserId)} disabled={loading} > - Aktualisieren + {t('Aktualisieren')} )}
@@ -640,7 +645,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {

{t('Benutzer auswählen')}

- Wählen Sie einen Benutzer aus, um dessen Zugriffsberechtigungen anzuzeigen. + {t('Wählen Sie einen Benutzer aus, um dessen Zugriffsberechtigungen anzuzeigen.')}

) : loading ? ( @@ -659,7 +664,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => { <> | - SysAdmin + {t('SysAdmin')} )} @@ -679,28 +684,28 @@ export const AdminUserAccessOverviewPage: React.FC = () => { onClick={() => setActiveTab('overview')} style={{ padding: '0.5rem 1rem' }} > - Übersicht + {t('Übersicht')} diff --git a/src/pages/admin/AdminUserMandatesPage.tsx b/src/pages/admin/AdminUserMandatesPage.tsx index f80d1ad..0be162c 100644 --- a/src/pages/admin/AdminUserMandatesPage.tsx +++ b/src/pages/admin/AdminUserMandatesPage.tsx @@ -174,20 +174,20 @@ export const AdminUserMandatesPage: React.FC = () => { return [ { name: 'targetUserId', - label: userIdAttr?.label || 'Benutzer', + label: userIdAttr?.label || t('Benutzer'), type: 'enum' as any, required: true, options: userOptions, }, { name: 'roleIds', - label: roleIdsAttr?.label || 'Rollen', + label: roleIdsAttr?.label || t('Rollen'), type: 'multiselect' as any, required: true, options: roleOptions, } ]; - }, [userOptions, roleOptions, backendAttributes]); + }, [userOptions, roleOptions, backendAttributes, t]); // Form attributes for editing user roles const editRolesFields: AttributeDefinition[] = useMemo(() => { @@ -195,12 +195,12 @@ export const AdminUserMandatesPage: React.FC = () => { return [{ name: 'roleIds', - label: roleIdsAttr?.label || 'Rollen', + label: roleIdsAttr?.label || t('Rollen'), type: 'multiselect' as any, required: true, options: roleOptions, }]; - }, [roleOptions, backendAttributes]); + }, [roleOptions, backendAttributes, t]); // Handle add user submit const handleAddUser = async (data: { targetUserId: string; roleIds: string[] }) => { @@ -212,7 +212,7 @@ export const AdminUserMandatesPage: React.FC = () => { setShowAddModal(false); fetchMandateUsers(selectedMandateId); } else { - showError('Fehler', result.error || 'Fehler beim Hinzufügen des Benutzers'); + showError(t('Fehler'), result.error || t('Fehler beim Hinzufügen des Benutzers')); } } finally { setIsSubmitting(false); @@ -229,7 +229,7 @@ export const AdminUserMandatesPage: React.FC = () => { setEditingUser(null); fetchMandateUsers(selectedMandateId); } else { - showError('Fehler', result.error || 'Fehler beim Aktualisieren der Rollen'); + showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren der Rollen')); } } finally { setIsSubmitting(false); @@ -241,7 +241,7 @@ export const AdminUserMandatesPage: React.FC = () => { if (!selectedMandateId) return; const result = await removeUserFromMandate(selectedMandateId, user.userId); if (!result.success) { - showError('Fehler', result.error || 'Fehler beim Entfernen des Benutzers'); + showError(t('Fehler'), result.error || t('Fehler beim Entfernen des Benutzers')); } }; @@ -252,11 +252,7 @@ export const AdminUserMandatesPage: React.FC = () => { // Get mandate name const getMandateName = (mandate: Mandate) => { - if (mandate.label) return mandate.label; - if (typeof mandate.name === 'object') { - return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id; - } - return mandate.name || mandate.id; + return mandate.label || mandate.name || mandate.id; }; if (error && !selectedMandateId) { @@ -264,9 +260,11 @@ export const AdminUserMandatesPage: React.FC = () => {
⚠️ -

Fehler: {error}

+

+ {t('Fehler')}: {error} +

@@ -277,7 +275,7 @@ export const AdminUserMandatesPage: React.FC = () => {
-

Mandanten-Mitglieder

+

{t('Mandanten-Mitglieder')}

{t('Verwalten Sie, welche Benutzer Zugriff')}

@@ -287,7 +285,7 @@ export const AdminUserMandatesPage: React.FC = () => {
- + {users.length === 0 ? ( ) : ( @@ -140,7 +140,7 @@ export const PermissionMatrix: React.FC = ({ users, onClick={onAddUser} disabled={disabled} > - + Benutzer hinzufügen + + {t('Benutzer hinzufügen')} diff --git a/src/pages/admin/wizards/AdminInvitationWizardPage.tsx b/src/pages/admin/wizards/AdminInvitationWizardPage.tsx index 8cc8248..483a801 100644 --- a/src/pages/admin/wizards/AdminInvitationWizardPage.tsx +++ b/src/pages/admin/wizards/AdminInvitationWizardPage.tsx @@ -8,7 +8,7 @@ * 4. Summary and send */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { useInvitations } from '../../../hooks/useInvitations'; import { useUserMandates, type Mandate, type Role } from '../../../hooks/useUserMandates'; import { useFeatureAccess, type FeatureInstance, type FeatureInstanceRole } from '../../../hooks/useFeatureAccess'; @@ -115,11 +115,7 @@ export const AdminInvitationWizardPage: React.FC = () => { // ========================================================================== const getMandateName = (m: Mandate): string => { - if (m.label) return m.label; - if (typeof m.name === 'object') { - return m.name.de || m.name.en || Object.values(m.name)[0] || m.id; - } - return m.name || m.id; + return m.label || m.name || m.id; }; // ========================================================================== @@ -173,17 +169,17 @@ export const AdminInvitationWizardPage: React.FC = () => { const email = inviteeForm.email.trim(); const username = inviteeForm.username.trim(); if (!email && !username) { - setError(t('Bitte mindestens eine E-Mail-Adresse oder')); + setError(t('Bitte mindestens eine E-Mail-Adresse oder einen Benutzernamen angeben.')); return; } const emailLower = email.toLowerCase(); const userLower = username.toLowerCase(); if (email && invitees.some(i => !i.isExisting && (i.email || '').toLowerCase() === emailLower)) { - setError(t('Diese E-Mail ist bereits in')); + setError(t('Diese E-Mail ist bereits in der Liste.')); return; } if (username && invitees.some(i => !i.isExisting && (i.username || '').toLowerCase() === userLower)) { - setError(t('Dieser Benutzername ist bereits in')); + setError(t('Dieser Benutzername ist bereits in der Liste.')); return; } setInvitees(prev => [...prev, { @@ -205,7 +201,7 @@ export const AdminInvitationWizardPage: React.FC = () => { if (!user) return; const email = (user.email || '').trim(); if (invitees.some(i => i.userId === user.id)) { - setError(t('Dieser Benutzer ist bereits in')); + setError(t('Dieser Benutzer ist bereits in der Liste.')); return; } setInvitees(prev => [...prev, { @@ -272,9 +268,9 @@ export const AdminInvitationWizardPage: React.FC = () => { } setDispatchResults(results); const ok = results.filter(r => r.success).length; - showSuccess('Versand', `${ok} von ${results.length} Einladungen erfolgreich versendet`); + showSuccess(t('Versand'), t('{ok} von {total} Einladungen erfolgreich versendet', { ok, total: results.length })); } catch (err: any) { - setError(err?.response?.data?.detail || err?.message || 'Fehler beim Versand'); + setError(err?.response?.data?.detail || err?.message || t('Fehler beim Versand der Einladungen')); } finally { setIsLoading(false); } @@ -295,9 +291,12 @@ export const AdminInvitationWizardPage: React.FC = () => { // STEP LABELS // ========================================================================== - const stepLabels = inviteType === 'featureInstance' - ? ['Art', 'Mandant & Instanz', 'Einladungen', 'Zusammenfassung'] - : ['Art', 'Mandant', 'Einladungen', 'Zusammenfassung']; + const stepLabels = useMemo( + () => (inviteType === 'featureInstance' + ? [t('Art'), t('Mandant & Instanz'), t('Einladungen'), t('Zusammenfassung')] + : [t('Art'), t('Mandant'), t('Einladungen'), t('Zusammenfassung')]), + [inviteType, t], + ); const canProceedStep3 = inviteType === 'mandate' ? selectedMandate !== null @@ -311,9 +310,9 @@ export const AdminInvitationWizardPage: React.FC = () => {
-

Einladungs-Wizard

+

{t('Einladungs-Wizard')}

- Benutzer zu Mandant oder Feature-Instanz einladen + {t('Benutzer zu Mandant oder Feature-Instanz einladen')}

@@ -369,7 +368,7 @@ export const AdminInvitationWizardPage: React.FC = () => { >
{t('Zum Mandanten')}
- Einladung zum Mandanten ohne spezifische Feature-Instanz + {t('Einladung zum Mandanten ohne spezifische Feature-Instanz')}
@@ -436,7 +435,7 @@ export const AdminInvitationWizardPage: React.FC = () => { {instances.map(inst => { const baseLabel = inst.label || inst.featureCode; - const suffix = inst.enabled === false ? ' (deaktiviert)' : ''; + const suffix = inst.enabled === false ? t(' (deaktiviert)') : ''; return ( ); @@ -452,7 +451,7 @@ export const AdminInvitationWizardPage: React.FC = () => { disabled={!canProceedStep3} onClick={() => setStep(3)} > - Weiter → + {t('Weiter →')} @@ -464,7 +463,7 @@ export const AdminInvitationWizardPage: React.FC = () => {

{t('Einladungen hinzufügen')}

- Für neue Benutzer: mindestens eine E-Mail oder ein Benutzername (vorgegeben). Ohne E-Mail wird kein Link per Mail versendet — der Einladungslink kann manuell geteilt werden. Bestehende Benutzer wählen Sie im zweiten Tab. + {t('Für neue Benutzer: mindestens eine E-Mail oder ein Benutzername (vorgegeben). Ohne E-Mail wird kein Link per Mail versendet — der Einladungslink kann manuell geteilt werden. Bestehende Benutzer wählen Sie im zweiten Tab.')}

{/* Add form: toggle email vs existing */} @@ -474,7 +473,7 @@ export const AdminInvitationWizardPage: React.FC = () => { style={{ fontSize: '12px', padding: '6px 12px' }} onClick={() => setAddMode('email')} > - Neue Benutzer (E-Mail und/oder Benutzername) + {t('Neue Benutzer (E-Mail und/oder Benutzername)')}
@@ -508,12 +507,14 @@ export const AdminInvitationWizardPage: React.FC = () => { placeholder={t('z.B. Vorname Nachname')} />

- Mindestens eines der beiden Felder ausfüllen. Mit Benutzername muss der Eingeladene genau diesen Namen beim Annehmen verwenden. + {t('Mindestens eines der beiden Felder ausfüllen. Mit Benutzername muss der Eingeladene genau diesen Namen beim Annehmen verwenden.')}

{roles.length > 0 && (
- +
{roles.map(r => (
) : ( @@ -602,7 +603,7 @@ export const AdminInvitationWizardPage: React.FC = () => { onClick={addInviteeExisting} disabled={!selectedExistingUserId || (roles.length > 0 && inviteeForm.roleIds.length === 0)} > - Hinzufügen + {t('Hinzufügen')}
)} @@ -613,10 +614,10 @@ export const AdminInvitationWizardPage: React.FC = () => {
- + - - + + @@ -631,13 +632,13 @@ export const AdminInvitationWizardPage: React.FC = () => { ? roles.filter(r => inv.roleIds.includes(r.id)).map(r => r.roleLabel).join(', ') : '-'} - + @@ -656,7 +657,7 @@ export const AdminInvitationWizardPage: React.FC = () => { disabled={invitees.length === 0} onClick={() => setStep(4)} > - Zur Zusammenfassung → + {t('Zur Zusammenfassung →')} @@ -667,18 +668,18 @@ export const AdminInvitationWizardPage: React.FC = () => {

{t('Zusammenfassung Versand')}

- Art: {inviteType === 'mandate' ? t('Einladung zum Mandanten') : t('Einladung zur Feature-Instanz')} + {t('Art:')} {inviteType === 'mandate' ? t('Einladung zum Mandanten') : t('Einladung zur Feature-Instanz')}
{t('Mandant')} {getMandateName(selectedMandate)}
{inviteType === 'featureInstance' && selectedInstance && (
- Feature-Instanz: {selectedInstance.label || selectedInstance.featureCode} + {t('Feature-Instanz:')} {selectedInstance.label || selectedInstance.featureCode}
)}
- Einladungen ({invitees.length}): + {t('Einladungen ({count}):', { count: invitees.length })}
    {invitees.map((inv, i) => (
  • @@ -696,7 +697,7 @@ export const AdminInvitationWizardPage: React.FC = () => { disabled={isLoading} onClick={handleSend} > - {isLoading ? 'Wird versendet...' : `${invitees.length} Einladungen versenden`} + {isLoading ? t('Wird versendet…') : t('{count} Einladungen versenden', { count: invitees.length })}
@@ -728,17 +729,17 @@ export const AdminInvitationWizardPage: React.FC = () => { background: r.success ? '#dcfce7' : '#fef2f2', color: r.success ? '#166534' : '#991b1b', }}> - {r.success ? 'Erfolgreich' : r.error || 'Fehler'} + {r.success ? t('Erfolgreich') : r.error || t('Fehler')} - + ))}
RessourceZugriff{t('Ressource')}{t('Zugriff')} {t('Gewährt durch')}
{t('Aktiv')}Aktionen{t('Aktionen')}
- Keine Benutzer zugewiesen. + {t('Keine Benutzer zugewiesen.')}
{t('E-Mail Benutzer')}Benutzername{t('Benutzername')} {t('Rollen')}TypAktion{t('Typ')}{t('Aktion')}
{inv.isExisting ? 'Bestehend' : 'Neu (Einladung)'}{inv.isExisting ? t('Bestehend') : t('Neu (Einladung)')}
{r.emailSent ? 'Ja' : '-'}{r.emailSent ? t('Ja') : t('—')}
diff --git a/src/pages/admin/wizards/AdminMandateWizardPage.tsx b/src/pages/admin/wizards/AdminMandateWizardPage.tsx index 82ebba9..6a20c51 100644 --- a/src/pages/admin/wizards/AdminMandateWizardPage.tsx +++ b/src/pages/admin/wizards/AdminMandateWizardPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { useUserMandates, type MandateUser, @@ -22,10 +22,8 @@ import { FormGeneratorForm } from '../../../components/FormGenerator/FormGenerat import styles from '../Admin.module.css'; import { useLanguage } from '../../../providers/language/LanguageContext'; -import { labelAsI18nKey } from '../../../types/mandate'; const TOTAL_STEPS = 4; -const STEP_LABELS = ['Mandant', 'Benutzer', 'Instances', 'Feature-Benutzer']; interface RoleOption { id: string; @@ -106,17 +104,13 @@ export const AdminMandateWizardPage: React.FC = () => { // ───────────────────────────────────────────────────────────────────────── const getMandateName = (m: Mandate | Record): string => { - if (m.label) return m.label; - if (typeof m.name === 'object') { - return m.name.de || m.name.en || Object.values(m.name)[0] || m.id; - } - return m.name || m.id; + return m.label || m.name || m.id; }; const getFeatureLabel = (code: string): string => { const f = features.find(feat => feat.code === code); if (f) { - return t(labelAsI18nKey(f.label, code)); + return t(f.label || code); } return code; }; @@ -138,10 +132,17 @@ export const AdminMandateWizardPage: React.FC = () => { } catch { setError(t('Fehler beim Laden der Mandanten')); } - }, [fetchMandatesList]); + }, [fetchMandatesList, t]); useEffect(() => { loadMandates(); }, [loadMandates]); + const stepLabels = useMemo(() => [ + t('Mandant'), + t('Benutzer'), + t('Feature-Instanzen'), + t('Feature-Benutzer'), + ], [t]); + useEffect(() => { fetchFeatures().then(setFeatures); }, [fetchFeatures]); @@ -230,20 +231,20 @@ export const AdminMandateWizardPage: React.FC = () => { } catch (billingErr: unknown) { console.error(billingErr); showWarning( - 'Mandant erstellt', - 'Abrechnungseinstellungen konnten nicht gespeichert werden. Bitte unter Administration → Abrechnung nachpflegen.', + t('Mandant erstellt'), + t('Abrechnungseinstellungen konnten nicht gespeichert werden. Bitte unter Administration → Abrechnung nachpflegen.'), ); } setSelectedMandate(created as Record); setIsCreatingMandate(false); if (billingSaved) { - showSuccess('Erstellt', 'Mandant inkl. Abrechnung gespeichert'); + showSuccess(t('Erstellt'), t('Mandant inkl. Abrechnung gespeichert')); } window.dispatchEvent(new CustomEvent('features-changed')); await loadMandates(); } catch (err: unknown) { const e = err as { response?: { data?: { detail?: string } }; message?: string }; - setError(e?.response?.data?.detail || e?.message || 'Fehler beim Erstellen'); + setError(e?.response?.data?.detail || e?.message || t('Fehler beim Erstellen des Mandanten')); } finally { setIsLoading(false); } @@ -262,17 +263,17 @@ export const AdminMandateWizardPage: React.FC = () => { roleIds: addMandateUserForm.roleIds, }); if (result.success) ok += 1; - else failures.push(`${uid}: ${result.error || 'Fehler'}`); + else failures.push(`${uid}: ${result.error || t('Fehler')}`); } if (ok > 0) { setIsAddingMandateUser(false); setAddMandateUserForm({ userIds: [], roleIds: [] }); - showSuccess('Hinzugefügt', `${ok} Benutzer zum Mandanten hinzugefügt`); + showSuccess(t('Hinzugefügt'), t('{count} Benutzer zum Mandanten hinzugefügt', { count: ok })); await loadMandateUsers(); } if (failures.length > 0) { showWarning( - 'Teilweise fehlgeschlagen', + t('Teilweise fehlgeschlagen'), failures.slice(0, 5).join('; ') + (failures.length > 5 ? '…' : ''), ); if (ok === 0) setError(failures.join('; ')); @@ -287,10 +288,10 @@ export const AdminMandateWizardPage: React.FC = () => { if (!selectedMandate) return; const result = await removeUserFromMandate(selectedMandate.id, userId); if (result.success) { - showSuccess('Entfernt', 'Benutzer aus Mandant entfernt'); + showSuccess(t('Entfernt'), t('Benutzer aus Mandant entfernt')); await loadMandateUsers(); } else { - setError(result.error || 'Fehler beim Entfernen'); + setError(result.error || t('Fehler beim Entfernen')); } }; @@ -307,10 +308,10 @@ export const AdminMandateWizardPage: React.FC = () => { if (result.success) { setIsCreatingInstance(false); setInstanceForm({ label: '', enabled: true }); - showSuccess('Erstellt', 'Instance erstellt'); + showSuccess(t('Erstellt'), t('Feature-Instanz erstellt')); await loadInstances(); } else { - setError(result.error || 'Fehler beim Erstellen (Limit erreicht?)'); + setError(result.error || t('Fehler beim Erstellen der Instanz (Limit erreicht?)')); } } finally { setIsLoading(false); @@ -321,10 +322,10 @@ export const AdminMandateWizardPage: React.FC = () => { if (!selectedMandate) return; const result = await deleteInstance(selectedMandate.id, instanceId); if (result.success) { - showSuccess('Gelöscht', 'Instance gelöscht'); + showSuccess(t('Gelöscht'), t('Feature-Instanz gelöscht')); await loadInstances(); } else { - setError(result.error || 'Fehler beim Löschen'); + setError(result.error || t('Fehler beim Löschen der Instanz')); } }; @@ -341,17 +342,17 @@ export const AdminMandateWizardPage: React.FC = () => { roleIds: addInstanceUserForm.roleIds, }); if (result.success) ok += 1; - else failures.push(`${uid}: ${result.error || 'Fehler'}`); + else failures.push(`${uid}: ${result.error || t('Fehler')}`); } if (ok > 0) { setIsAddingInstanceUser(false); setAddInstanceUserForm({ userIds: [], roleIds: [] }); - showSuccess('Hinzugefügt', `${ok} Benutzer zur Feature-Instanz hinzugefügt`); + showSuccess(t('Hinzugefügt'), t('{count} Benutzer zur Feature-Instanz hinzugefügt', { count: ok })); await loadInstanceUsers(); } if (failures.length > 0) { showWarning( - 'Teilweise fehlgeschlagen', + t('Teilweise fehlgeschlagen'), failures.slice(0, 5).join('; ') + (failures.length > 5 ? '…' : ''), ); if (ok === 0) setError(failures.join('; ')); @@ -368,12 +369,12 @@ export const AdminMandateWizardPage: React.FC = () => { setError(null); const r = await updateUserRoles(selectedMandate.id, roleEditContext.userId, roleEditDraft); if (r.success) { - showSuccess('Gespeichert', 'Rollen aktualisiert'); + showSuccess(t('Gespeichert'), t('Rollen aktualisiert')); setRoleEditContext(null); setRoleEditDraft([]); await loadMandateUsers(); } else { - showError('Fehler', r.error || 'Rollen konnten nicht gespeichert werden'); + showError(t('Fehler'), r.error || t('Rollen konnten nicht gespeichert werden')); } setIsLoading(false); }; @@ -389,12 +390,12 @@ export const AdminMandateWizardPage: React.FC = () => { { roleIds: roleEditDraft }, ); if (r.success) { - showSuccess('Gespeichert', 'Rollen aktualisiert'); + showSuccess(t('Gespeichert'), t('Rollen aktualisiert')); setRoleEditContext(null); setRoleEditDraft([]); await loadInstanceUsers(); } else { - showError('Fehler', r.error || 'Rollen konnten nicht gespeichert werden'); + showError(t('Fehler'), r.error || t('Rollen konnten nicht gespeichert werden')); } setIsLoading(false); }; @@ -403,10 +404,10 @@ export const AdminMandateWizardPage: React.FC = () => { if (!selectedInstance || !selectedMandate) return; const result = await removeUserFromInstance(selectedMandate.id, selectedInstance.id, userId); if (result.success) { - showSuccess('Entfernt', 'Benutzer aus Feature-Instanz entfernt'); + showSuccess(t('Entfernt'), t('Benutzer aus Feature-Instanz entfernt')); await loadInstanceUsers(); } else { - setError(result.error || 'Fehler beim Entfernen'); + setError(result.error || t('Fehler beim Entfernen')); } }; @@ -460,10 +461,10 @@ export const AdminMandateWizardPage: React.FC = () => { {t('Benutzer')} - E-Mail + {t('E-Mail')} {t('Rollen')} {t('Status')} - Aktion + {t('Aktion')} @@ -490,7 +491,7 @@ export const AdminMandateWizardPage: React.FC = () => { }} onClick={() => options.onEditRoles!(uid, ids)} > - Bearbeiten + {t('Bearbeiten')} )} @@ -510,7 +511,7 @@ export const AdminMandateWizardPage: React.FC = () => { }} onClick={() => onRemove(uid)} > - Entfernen + {t('Entfernen')} @@ -520,7 +521,7 @@ export const AdminMandateWizardPage: React.FC = () => { ) : (
- Noch keine Benutzer zugewiesen + {t('Noch keine Benutzer zugewiesen')}
) ); @@ -607,7 +608,7 @@ export const AdminMandateWizardPage: React.FC = () => { {isLoading ? t('Hinzufügen') : t('Hinzufügen')}
@@ -645,7 +646,7 @@ export const AdminMandateWizardPage: React.FC = () => { }}> {s < step ? '\u2713' : s} - {STEP_LABELS[s - 1]} + {stepLabels[s - 1]} ))} @@ -670,7 +671,7 @@ export const AdminMandateWizardPage: React.FC = () => {
-

Mandanten-Verwaltung

+

{t('Mandanten-Verwaltung')}

{t('Schritt-für-Schritt-Wizard zur Mandantenkonfiguration')}

@@ -718,7 +719,7 @@ export const AdminMandateWizardPage: React.FC = () => { ))}
) : ( @@ -743,7 +744,7 @@ export const AdminMandateWizardPage: React.FC = () => {
@@ -754,7 +755,7 @@ export const AdminMandateWizardPage: React.FC = () => {

- Benutzer von «{getMandateName(selectedMandate)}» + {t('Benutzer von «{name}»', { name: getMandateName(selectedMandate) })}

- Alle Systembenutzer können dem Mandanten zugewiesen werden. + {t('Alle Systembenutzer können dem Mandanten zugewiesen werden.')}

{isAddingMandateUser && renderAddUserForm( @@ -812,14 +813,14 @@ export const AdminMandateWizardPage: React.FC = () => { onClick={() => _saveMandateRoleEdit()} disabled={isLoading} > - Speichern + {t('Speichern')}
@@ -838,7 +839,7 @@ export const AdminMandateWizardPage: React.FC = () => {
@@ -849,10 +850,10 @@ export const AdminMandateWizardPage: React.FC = () => {

- Feature-Instances für «{getMandateName(selectedMandate)}» + {t('Feature-Instanzen für «{name}»', { name: getMandateName(selectedMandate) })}

- {instances.length} Instances + {t('{count} Instanzen', { count: instances.length })}
@@ -891,7 +892,7 @@ export const AdminMandateWizardPage: React.FC = () => { style={{ padding: '4px 8px', fontSize: '12px' }} onClick={() => { setSelectedInstance(inst); setStep(4); }} > - Benutzer + {t('Benutzer')}
@@ -909,7 +910,7 @@ export const AdminMandateWizardPage: React.FC = () => { {!isCreatingInstance ? ( ) : (
@@ -942,7 +943,7 @@ export const AdminMandateWizardPage: React.FC = () => { checked={instanceForm.enabled} onChange={e => setInstanceForm(p => ({ ...p, enabled: e.target.checked }))} /> - Instance aktiviert + {t('Instanz aktiviert')}
@@ -971,7 +972,7 @@ export const AdminMandateWizardPage: React.FC = () => {

- Feature-Benutzer für «{selectedInstance.label}» + {t('Feature-Benutzer für «{label}»', { label: selectedInstance.label })}

- Mandant: {getMandateName(selectedMandate)} | Mitglieder des Mandanten können der Feature-Instanz zugewiesen werden. + {t('Mandant: {mandate} | Mitglieder des Mandanten können der Feature-Instanz zugewiesen werden.', { mandate: getMandateName(selectedMandate) })}

{isAddingInstanceUser && renderAddUserForm( @@ -1029,14 +1030,14 @@ export const AdminMandateWizardPage: React.FC = () => { onClick={() => _saveInstanceRoleEdit()} disabled={isLoading} > - Speichern + {t('Speichern')}
@@ -1060,11 +1061,11 @@ export const AdminMandateWizardPage: React.FC = () => {
diff --git a/src/pages/admin/wizards/FeatureInstanceWizard.tsx b/src/pages/admin/wizards/FeatureInstanceWizard.tsx index 9420e07..9e14960 100644 --- a/src/pages/admin/wizards/FeatureInstanceWizard.tsx +++ b/src/pages/admin/wizards/FeatureInstanceWizard.tsx @@ -15,11 +15,9 @@ import styles from '../Admin.module.css'; import wizardStyles from './FeatureInstanceWizard.module.css'; import { useLanguage } from '../../../providers/language/LanguageContext'; -import { labelAsI18nKey } from '../../../types/mandate'; function getMandateName(m: Mandate): string { - if (typeof m.name === 'object') return m.name.de || m.name.en || Object.values(m.name)[0] || m.id; - return m.name || m.id; + return m.label || m.name || m.id; } export interface FeatureInstanceWizardProps { @@ -30,12 +28,6 @@ export interface FeatureInstanceWizardProps { onComplete: () => void; } -const STEPS = [ - { id: 'create', title: 'Instanz erstellen' }, - { id: 'roles', title: 'Rollen' }, - { id: 'users', title: 'Benutzer (optional)' }, -]; - export const FeatureInstanceWizard: React.FC = ({ mandateId: initialMandateId, mandates, features, @@ -46,6 +38,12 @@ export const FeatureInstanceWizard: React.FC = ({ ma const { showSuccess, showError } = useToast(); const { t } = useLanguage(); + const steps = useMemo(() => [ + { id: 'create' as const, title: t('Instanz erstellen') }, + { id: 'roles' as const, title: t('Rollen') }, + { id: 'users' as const, title: t('Benutzer (optional)') }, + ], [t]); + const [step, setStep] = useState(0); const [mandateId, setMandateId] = useState(initialMandateId || ''); const [featureCode, setFeatureCode] = useState(''); @@ -59,7 +57,7 @@ export const FeatureInstanceWizard: React.FC = ({ ma const [selectedUserRoles, setSelectedUserRoles] = useState>([]); const featureOptions = useMemo( - () => features.map((f) => ({ value: f.code, label: t(labelAsI18nKey(f.label, f.code)) })), + () => features.map((f) => ({ value: f.code, label: t(f.label || f.code) })), [features, t] ); const mandateOptions = useMemo( @@ -99,7 +97,7 @@ export const FeatureInstanceWizard: React.FC = ({ ma setCreatedInstanceId(result.data.id); setStep(1); } else { - showError('Fehler', result.error || 'Instanz konnte nicht erstellt werden'); + showError(t('Fehler'), result.error || t('Instanz konnte nicht erstellt werden')); } } finally { setSubmitting(false); @@ -147,10 +145,10 @@ export const FeatureInstanceWizard: React.FC = ({ ma await addUserToInstance(mandateId, createdInstanceId, { userId, roleIds }); } } - showSuccess('Fertig', 'Feature-Instanz wurde erstellt und Benutzer zugewiesen.'); + showSuccess(t('Fertig'), t('Feature-Instanz wurde erstellt und Benutzer zugewiesen.')); onComplete(); } catch { - showError('Fehler', 'Einige Benutzer konnten nicht zugewiesen werden.'); + showError(t('Fehler'), t('Einige Benutzer konnten nicht zugewiesen werden.')); } finally { setSubmitting(false); } @@ -164,7 +162,7 @@ export const FeatureInstanceWizard: React.FC = ({ ma }); }; - const currentStepId = STEPS[step]?.id; + const currentStepId = steps[step]?.id; return (
@@ -177,7 +175,7 @@ export const FeatureInstanceWizard: React.FC = ({ ma
- {STEPS.map((s, i) => ( + {steps.map((s, i) => (
= ({ ma checked={copyTemplateRoles} onChange={(e) => setCopyTemplateRoles(e.target.checked)} /> - Rollen von Feature-Vorlage übernehmen (empfohlen) + {t('Rollen von Feature-Vorlage übernehmen (empfohlen)')}
)} @@ -219,14 +217,14 @@ export const FeatureInstanceWizard: React.FC = ({ ma {currentStepId === 'roles' && (

- Die Rollen wurden beim Erstellen der Instanz übernommen. Sie können später unter „Benutzer verwalten“ weitere Rollen synchronisieren. + {t('Die Rollen wurden beim Erstellen der Instanz übernommen. Sie können später unter „Benutzer verwalten“ weitere Rollen synchronisieren.')}

@@ -235,7 +233,7 @@ export const FeatureInstanceWizard: React.FC = ({ ma {currentStepId === 'users' && (

- Optional: Weisen Sie Benutzern Rollen zu. Sie können dies auch später in der Zugriffsverwaltung tun. + {t('Optional: Weisen Sie Benutzern Rollen zu. Sie können dies auch später in der Zugriffsverwaltung tun.')}

{mandateUsers.length === 0 ? (

{t('Keine Mandantenbenutzer vorhanden')}

@@ -270,7 +268,7 @@ export const FeatureInstanceWizard: React.FC = ({ ma )}
diff --git a/src/pages/basedata/ConnectionsPage.tsx b/src/pages/basedata/ConnectionsPage.tsx index aca59a2..be8ff49 100644 --- a/src/pages/basedata/ConnectionsPage.tsx +++ b/src/pages/basedata/ConnectionsPage.tsx @@ -238,9 +238,9 @@ export const ConnectionsPage: React.FC = () => {
⚠️ -

Fehler beim Laden der Verbindungen: {error}

+

{t('Fehler beim Laden der Verbindungen: {detail}', { detail: String(error) })}

diff --git a/src/pages/basedata/FilesPage.tsx b/src/pages/basedata/FilesPage.tsx index 0c1e038..d6916d8 100644 --- a/src/pages/basedata/FilesPage.tsx +++ b/src/pages/basedata/FilesPage.tsx @@ -160,7 +160,7 @@ export const FilesPage: React.FC = () => { } as any); return cols; - }, [attributes]); + }, [attributes, t]); const canCreate = permissions?.create !== 'n'; const canUpdate = permissions?.update !== 'n'; @@ -219,21 +219,23 @@ export const FilesPage: React.FC = () => { await refetch(); if (successCount > 0) { showSuccess( - 'Upload erfolgreich', - `${successCount} Datei(en) hochgeladen${errorCount > 0 ? `, ${errorCount} fehlgeschlagen` : ''}` + t('Upload erfolgreich'), + errorCount > 0 + ? t('{successCount} Datei(en) hochgeladen, {errorCount} fehlgeschlagen', { successCount, errorCount }) + : t('{successCount} Datei(en) hochgeladen', { successCount }), ); } else if (errorCount > 0) { - showError('Upload fehlgeschlagen', `${errorCount} Datei(en) konnten nicht hochgeladen werden`); + showError(t('Upload fehlgeschlagen'), t('{errorCount} Datei(en) konnten nicht hochgeladen werden', { errorCount })); } } }; const _handleNewFolder = useCallback(async () => { - const name = await promptInput('Neuer Ordnername:', { title: t('Neuer Ordner'), placeholder: 'Ordnername' }); + const name = await promptInput(t('Neuer Ordnername:'), { title: t('Neuer Ordner'), placeholder: t('Ordnername') }); if (name?.trim()) { await handleCreateFolder(name.trim(), selectedFolderId); } - }, [handleCreateFolder, selectedFolderId, promptInput]); + }, [handleCreateFolder, selectedFolderId, promptInput, t]); const _onRowDragStart = useCallback((e: React.DragEvent, row: UserFile) => { const isInSelection = selectedFiles.some(f => f.id === row.id); @@ -304,9 +306,9 @@ export const FilesPage: React.FC = () => {
⚠️ -

Fehler beim Laden der Dateien: {error}

+

{t('Fehler beim Laden der Dateien: {detail}', { detail: String(error) })}

@@ -326,11 +328,11 @@ export const FilesPage: React.FC = () => {

{t('Dateien')}

-

Dateiverwaltung

+

{t('Dateiverwaltung')}

@@ -407,7 +409,7 @@ export const FilesPage: React.FC = () => { {canCreate && ( )}
diff --git a/src/pages/basedata/PromptsPage.tsx b/src/pages/basedata/PromptsPage.tsx index 07a64b1..23194e0 100644 --- a/src/pages/basedata/PromptsPage.tsx +++ b/src/pages/basedata/PromptsPage.tsx @@ -91,7 +91,7 @@ export const PromptsPage: React.FC = () => { }); return cols; - }, [attributes]); + }, [attributes, t]); // Check permissions const canCreate = permissions?.create !== 'n'; @@ -134,7 +134,7 @@ export const PromptsPage: React.FC = () => { // Handle duplicate prompt const handleDuplicate = async (prompt: Prompt) => { const result = await handlePromptCreate({ - name: `Kopie von ${prompt.name || 'Prompt'}`, + name: t('Kopie von {name}', { name: prompt.name || t('Prompt') }), content: prompt.content || '' }); if (result?.success) { @@ -162,9 +162,9 @@ export const PromptsPage: React.FC = () => {
⚠️ -

Fehler beim Laden der Prompts: {error}

+

{t('Fehler beim Laden der Prompts: {detail}', { detail: String(error) })}

@@ -184,14 +184,14 @@ export const PromptsPage: React.FC = () => { onClick={() => refetch()} disabled={loading} > - Aktualisieren + {t('Aktualisieren')} {canCreate && ( )} diff --git a/src/pages/billing/BillingAdmin.tsx b/src/pages/billing/BillingAdmin.tsx index 38a795f..24341b0 100644 --- a/src/pages/billing/BillingAdmin.tsx +++ b/src/pages/billing/BillingAdmin.tsx @@ -29,12 +29,7 @@ const _formatCurrency = (amount: number) => { }; const _mandateDisplayLabel = (m: UserMandateRow): string => { - if (m.label) return m.label; - if (typeof m.name === 'object' && m.name) { - const n = m.name as Record; - return n.de || n.en || Object.values(n)[0] || m.id; - } - return (m.name as string) || m.id; + return m.label || m.name || m.id; }; // ============================================================================ @@ -116,9 +111,9 @@ const SettingsEditor: React.FC = ({ settings, onSave, loadi try { await onSave(formData); - setMessage({ type: 'success', text: 'Einstellungen gespeichert!' }); + setMessage({ type: 'success', text: t('Einstellungen gespeichert!') }); } catch (err: any) { - setMessage({ type: 'error', text: err.message || 'Fehler beim Speichern' }); + setMessage({ type: 'error', text: err.message || t('Fehler beim Speichern') }); } finally { setSaving(false); } @@ -608,9 +603,9 @@ export const BillingAdmin: React.FC = () => {
-

Abrechnung

+

{t('Abrechnung')}

- Abonnement, Einstellungen und Guthaben pro Mandant + {t('Abonnement, Einstellungen und Guthaben pro Mandant')}

diff --git a/src/pages/billing/BillingDashboard.tsx b/src/pages/billing/BillingDashboard.tsx index 37671e0..c20a27d 100644 --- a/src/pages/billing/BillingDashboard.tsx +++ b/src/pages/billing/BillingDashboard.tsx @@ -21,6 +21,7 @@ interface BalanceCardProps { } const BalanceCard: React.FC = ({ balance, onClick }) => { + const { t } = useLanguage(); const formatCurrency = (amount: number) => { return new Intl.NumberFormat('de-CH', { style: 'currency', @@ -41,7 +42,7 @@ const BalanceCard: React.FC = ({ balance, onClick }) => { {balance.isWarning && (
- Niedriges Guthaben + {t('Niedriges Guthaben')}
)} @@ -80,7 +81,7 @@ const StatisticsChart: React.FC = ({ statistics, loading } return (
- Gesamtkosten + {t('Gesamtkosten')} {formatCurrency(statistics.totalCost)}
@@ -213,15 +214,15 @@ export const BillingDashboard: React.FC = () => { {/* Statistics */}
-

Nutzungsstatistik

+

{t('Nutzungsstatistik')}

= ({ balances, {t('Mandant')} {t('Benutzer')} {t('Guthaben')} - Warnschwelle + {t('Warnschwelle')} {t('Status')} @@ -139,12 +139,12 @@ const UserBalanceTable: React.FC = ({ balances, {balance.isWarning ? ( - Niedrig + {t('Niedrig')} ) : balance.enabled ? ( {t('Aktiv')} ) : ( - Deaktiviert + {t('Deaktiviert')} )} @@ -313,7 +313,7 @@ export const BillingUserView: React.FC = () => { return (
-

Benutzer-Billing

+

{t('Benutzer-Billing')}

{t('Guthaben und Transaktionen pro Benutzer')}

@@ -321,7 +321,7 @@ export const BillingUserView: React.FC = () => { {/* User Balances */}
-

Benutzer-Guthaben

+

{t('Benutzer-Guthaben')}

{loading && balances.length === 0 ? (
{t('Daten laden')}
) : balances.length === 0 ? ( @@ -341,10 +341,10 @@ export const BillingUserView: React.FC = () => {

- Transaktionen + {t('Transaktionen')} {(selectedMandateId || selectedUserId) && ( - ({filteredTransactionCount} gefiltert) + {t('({count} gefiltert)', { count: filteredTransactionCount })} )}

diff --git a/src/pages/billing/SubscriptionTab.tsx b/src/pages/billing/SubscriptionTab.tsx index e175eff..bf726a1 100644 --- a/src/pages/billing/SubscriptionTab.tsx +++ b/src/pages/billing/SubscriptionTab.tsx @@ -57,7 +57,7 @@ function _getPeriodLabel(t: (key: string) => string): Record { return { MONTHLY: t('Monatlich'), YEARLY: t('Jährlich'), - NONE: '—', + NONE: t('—'), }; } @@ -98,7 +98,7 @@ const PlanCard: React.FC = ({ plan, isCurrent, onActivate, activa Aktuell + }}>{t('Aktuell')} )}
@@ -108,28 +108,28 @@ const PlanCard: React.FC = ({ plan, isCurrent, onActivate, activa {!isFreePlan && (
-
User: {_formatCurrency(plan.pricePerUserCHF)} / {periodLabel[plan.billingPeriod] || plan.billingPeriod}
+
{t('User:')} {_formatCurrency(plan.pricePerUserCHF)} / {periodLabel[plan.billingPeriod] || plan.billingPeriod}
- Module inkl.: {plan.includedModules ?? 0} + {t('Module inkl.:')} {plan.includedModules ?? 0} {(plan.pricePerFeatureInstanceCHF ?? 0) > 0 && ( - <> · Zusatzmodul: {_formatCurrency(plan.pricePerFeatureInstanceCHF)} / Monat + <> · {t('Zusatzmodul:')} {_formatCurrency(plan.pricePerFeatureInstanceCHF)} {t('/ Monat')} )}
- AI-Budget: {_formatCurrency(plan.budgetAiPerUserCHF ?? 0)} / User / Monat + {t('AI-Budget:')} {_formatCurrency(plan.budgetAiPerUserCHF ?? 0)} {t('/ User / Monat')} {' · '} - Speicher:{' '} + {t('Speicher:')}{' '} {plan.maxDataVolumeMB == null - ? 'unbegrenzt' + ? t('unbegrenzt') : formatBinaryDataSizeFromMebibytes(plan.maxDataVolumeMB)}
{plan.maxUsers != null && (
- Max. User: {plan.maxUsers} + {t('Max. User:')} {plan.maxUsers} {' · '} - Speicher über Plan: {_formatCurrency(storageOverageChfPerGbMonth)} / GB / Monat + {t('Speicher über Plan:')} {_formatCurrency(storageOverageChfPerGbMonth)} {t('/ GB / Monat')}
)}
@@ -137,15 +137,15 @@ const PlanCard: React.FC = ({ plan, isCurrent, onActivate, activa {isFreePlan && plan.trialDays && (
- {plan.trialDays} Tage kostenlos - {plan.maxUsers && <> · max. {plan.maxUsers} User} - {(plan.includedModules ?? 0) > 0 && <> · {plan.includedModules} Module inkl.} + {t('{days} Tage kostenlos', { days: plan.trialDays })} + {plan.maxUsers && <> · {t('{count} Users maximal', { count: plan.maxUsers })}} + {(plan.includedModules ?? 0) > 0 && <> · {t('{count} Module inkl.', { count: plan.includedModules })}} {(plan.maxDataVolumeMB != null || (plan.budgetAiPerUserCHF ?? 0) > 0) && ( <> {plan.maxDataVolumeMB != null && ( - <> · Speicher {formatBinaryDataSizeFromMebibytes(plan.maxDataVolumeMB)} + <> · {t('Speicher')} {formatBinaryDataSizeFromMebibytes(plan.maxDataVolumeMB)} )} - {(plan.budgetAiPerUserCHF ?? 0) > 0 && <> · AI-Budget {_formatCurrency(plan.budgetAiPerUserCHF ?? 0)} / User} + {(plan.budgetAiPerUserCHF ?? 0) > 0 && <> · {t('AI-Budget')} {_formatCurrency(plan.budgetAiPerUserCHF ?? 0)} {t('/ User')}} )}
@@ -163,7 +163,7 @@ const PlanCard: React.FC = ({ plan, isCurrent, onActivate, activa }} > {activating - ? 'Weiterleitung...' + ? t('Weiterleitung…') : (!isFreePlan && !plan.trialDays) ? t('Kostenpflichtig abonnieren') : t('Auswählen')} )} @@ -243,7 +243,7 @@ const SubInfoCard: React.FC = ({ sub, plan, label, onCancel, onRea background: 'rgba(139,92,246,0.1)', border: '1px solid rgba(139,92,246,0.3)', color: '#8b5cf6', fontSize: '0.85rem', }}> - Dieses Abonnement wird am {_formatDate(sub.effectiveFrom)} aktiv, wenn das aktuelle Abonnement ausläuft. + {t('Dieses Abonnement wird am {date} aktiv, wenn das aktuelle Abonnement ausläuft.', { date: _formatDate(sub.effectiveFrom) })}
)} @@ -252,25 +252,25 @@ const SubInfoCard: React.FC = ({ sub, plan, label, onCancel, onRea fontSize: '0.85rem', color: 'var(--text-secondary)', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.25rem 1rem', }}> - Gestartet: {_formatDate(sub.startedAt)} - {plan && Periode: {periodLabel[plan.billingPeriod] || '—'}} - {sub.currentPeriodEnd && Periodenende: {_formatDate(sub.currentPeriodEnd)}} - {sub.trialEndsAt && Trial endet: {_formatDate(sub.trialEndsAt)}} + {t('Gestartet:')} {_formatDate(sub.startedAt)} + {plan && {t('Periode:')} {periodLabel[plan.billingPeriod] || t('—')}} + {sub.currentPeriodEnd && {t('Periodenende:')} {_formatDate(sub.currentPeriodEnd)}} + {sub.trialEndsAt && {t('Trial endet:')} {_formatDate(sub.trialEndsAt)}} {isActive && !sub.recurring && sub.currentPeriodEnd && ( - Läuft aus am: {_formatDate(sub.currentPeriodEnd)} + {t('Läuft aus am:')} {_formatDate(sub.currentPeriodEnd)} )} {plan && ( <> - AI-Budget: {_formatCurrency(plan.budgetAiPerUserCHF ?? 0)} / User / Monat - Module inkl.: {plan.includedModules ?? 0} + {t('AI-Budget:')} {_formatCurrency(plan.budgetAiPerUserCHF ?? 0)} {t('/ User / Monat')} + {t('Module inkl.:')} {plan.includedModules ?? 0} - Speicher:{' '} + {t('Speicher:')}{' '} {plan.maxDataVolumeMB == null - ? 'unbegrenzt' + ? t('unbegrenzt') : formatBinaryDataSizeFromMebibytes(plan.maxDataVolumeMB)} - Speicher über Plan: {_formatCurrency(storageOverageChfPerGbMonth)} / GB / Monat + {t('Speicher über Plan:')} {_formatCurrency(storageOverageChfPerGbMonth)} {t('/ GB / Monat')} )} @@ -362,7 +362,7 @@ export const SubscriptionTab: React.FC = ({ mandateId }) = const params = new URLSearchParams(window.location.search); if (params.get('success') === 'true') { const sessionId = params.get('session_id') || ''; - setCheckoutMessage({ type: 'success', text: 'Zahlung erfolgreich — Abonnement wird aktiviert...' }); + setCheckoutMessage({ type: 'success', text: t('Zahlung erfolgreich — Abonnement wird aktiviert…') }); setJustPaid(true); const url = new URL(window.location.href); @@ -376,7 +376,7 @@ export const SubscriptionTab: React.FC = ({ mandateId }) = try { const result = await verifyCheckout(sessionId); if (result.status === 'activated') { - setCheckoutMessage({ type: 'success', text: 'Abonnement wurde aktiviert.' }); + setCheckoutMessage({ type: 'success', text: t('Abonnement wurde aktiviert.') }); setJustPaid(false); return; } @@ -389,20 +389,20 @@ export const SubscriptionTab: React.FC = ({ mandateId }) = _pollUntilActive(); } } else if (params.get('canceled') === 'true') { - setCheckoutMessage({ type: 'info', text: 'Checkout abgebrochen. Ihr bestehendes Abonnement bleibt aktiv.' }); + setCheckoutMessage({ type: 'info', text: t('Checkout abgebrochen. Ihr bestehendes Abonnement bleibt aktiv.') }); const url = new URL(window.location.href); url.searchParams.delete('canceled'); window.history.replaceState({}, '', url.toString()); } - }, []); + }, [verifyCheckout, t]); useEffect(() => { if (!justPaid) return; if (subscription && subscription.status !== 'PENDING') { setJustPaid(false); - setCheckoutMessage({ type: 'success', text: 'Abonnement wurde aktiviert.' }); + setCheckoutMessage({ type: 'success', text: t('Abonnement wurde aktiviert.') }); } - }, [justPaid, subscription]); + }, [justPaid, subscription, t]); const _handleActivate = useCallback(async (planKey: string) => { setActivatingPlanKey(planKey); @@ -410,11 +410,11 @@ export const SubscriptionTab: React.FC = ({ mandateId }) = try { await activatePlan(planKey); } catch (err: any) { - setActionError(err?.response?.data?.detail || err.message || 'Fehler beim Aktivieren'); + setActionError(err?.response?.data?.detail || err.message || t('Fehler beim Aktivieren')); } finally { setActivatingPlanKey(null); } - }, [activatePlan]); + }, [activatePlan, t]); const _handleCancel = useCallback(async (subscriptionId: string) => { const sub = subscription?.id === subscriptionId ? subscription : scheduled; @@ -437,11 +437,11 @@ export const SubscriptionTab: React.FC = ({ mandateId }) = await cancelSubscription(subscriptionId); setCheckoutMessage(null); } catch (err: any) { - setActionError(err?.response?.data?.detail || err.message || 'Fehler'); + setActionError(err?.response?.data?.detail || err.message || t('Fehler')); } finally { setCancelling(false); } - }, [cancelSubscription, subscription, scheduled]); + }, [cancelSubscription, subscription, scheduled, confirm, t]); const _handleReactivate = useCallback(async (subscriptionId: string) => { setReactivating(true); @@ -449,11 +449,11 @@ export const SubscriptionTab: React.FC = ({ mandateId }) = try { await reactivateSubscription(subscriptionId); } catch (err: any) { - setActionError(err?.response?.data?.detail || err.message || 'Fehler beim Reaktivieren'); + setActionError(err?.response?.data?.detail || err.message || t('Fehler beim Reaktivieren')); } finally { setReactivating(false); } - }, [reactivateSubscription]); + }, [reactivateSubscription, t]); if (loading && !subscription) { return
{t('Abonnementdaten werden geladen…')}
; @@ -490,7 +490,7 @@ export const SubscriptionTab: React.FC = ({ mandateId }) = plan={currentPlan} label={subscription.status === 'PENDING' ? (justPaid ? t('Zahlung wird verarbeitet…') : t('Checkout läuft…')) - : 'Operatives Abonnement'} + : t('Operatives Abonnement')} onCancel={_handleCancel} onReactivate={_handleReactivate} cancelling={cancelling} @@ -499,7 +499,7 @@ export const SubscriptionTab: React.FC = ({ mandateId }) = /> ) : (
- Kein aktives Abonnement. Wählen Sie unten einen Plan. + {t('Kein aktives Abonnement. Wählen Sie unten einen Plan.')}
)}
diff --git a/src/pages/views/chatbot/ChatbotConversationsView.tsx b/src/pages/views/chatbot/ChatbotConversationsView.tsx index d2bbfce..0dc9cef 100644 --- a/src/pages/views/chatbot/ChatbotConversationsView.tsx +++ b/src/pages/views/chatbot/ChatbotConversationsView.tsx @@ -84,9 +84,9 @@ export const ChatbotConversationsView: React.FC = () => { const handleDeleteThread = useCallback(async (e: React.MouseEvent, workflowId: string) => { e.stopPropagation(); - const ok = await confirm('Möchten Sie diese Konversation wirklich löschen?', { + const ok = await confirm(t('Möchten Sie diese Konversation wirklich löschen?'), { title: t('Konversation löschen'), - confirmLabel: 'Löschen', + confirmLabel: t('Löschen'), variant: 'danger', }); if (!ok) return; @@ -96,7 +96,7 @@ export const ChatbotConversationsView: React.FC = () => { } finally { setDeletingId(null); } - }, [confirm, deleteThread]); + }, [confirm, deleteThread, t]); const formatDate = (timestamp?: number) => { if (!timestamp) return ''; @@ -109,10 +109,10 @@ export const ChatbotConversationsView: React.FC = () => { const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); - if (diffMins < 1) return 'Gerade eben'; - if (diffMins < 60) return `Vor ${diffMins} Min`; - if (diffHours < 24) return `Vor ${diffHours} Std`; - if (diffDays < 7) return `Vor ${diffDays} Tagen`; + if (diffMins < 1) return t('Gerade eben'); + if (diffMins < 60) return t('Vor {n} Min', { n: diffMins }); + if (diffHours < 24) return t('Vor {n} Std', { n: diffHours }); + if (diffDays < 7) return t('Vor {n} Tagen', { n: diffDays }); // For older dates, use the formatUnixTimestamp utility for consistent formatting const { time } = formatUnixTimestamp(timestamp, 'de-DE', { @@ -126,20 +126,20 @@ export const ChatbotConversationsView: React.FC = () => { const getThreadTitle = (thread: any) => { if (thread.name) return thread.name; // Try to get first message content as title - return 'Neue Konversation'; + return t('Neue Konversation'); }; return (
{/* Chat History Sidebar */} -
@@ -89,12 +108,16 @@ export const CommcoachDashboardView: React.FC = () => {
{ctx.title}
{_categoryLabel(ctx.category)} - {ctx.sessionCount} Sessions - {ctx.goalProgress != null && Ziele: {ctx.goalProgress}%} + + {ctx.sessionCount} {t('Sessions')} + + {ctx.goalProgress != null && ( + {t('Ziele: {pct}%', { pct: ctx.goalProgress })} + )}
{ctx.lastSessionAt && (
- Letzte Session: {_formatDate(ctx.lastSessionAt)} + {t('Letzte Session:')} {_formatDate(ctx.lastSessionAt)}
)}
@@ -108,8 +131,11 @@ export const CommcoachDashboardView: React.FC = () => {

{dashboard.level - ? `Level ${dashboard.level.number}: ${dashboard.level.label}` - : 'Auszeichnungen'} + ? t('Level {num}: {label}', { + num: dashboard.level.number, + label: dashboard.level.label, + }) + : t('Auszeichnungen')}

{dashboard.badges && dashboard.badges.length > 0 && (
@@ -128,28 +154,17 @@ export const CommcoachDashboardView: React.FC = () => {

{t('Tipp des Tages')}

-

Konsistenz schlägt Intensität. Auch 10 Minuten tägliches Coaching-Gespräch - bringt messbare Fortschritte in deiner Kommunikationskompetenz.

+

+ {t( + 'Konsistenz schlägt Intensität. Auch 10 Minuten tägliches Coaching-Gespräch bringt messbare Fortschritte in deiner Kommunikationskompetenz.', + )} +

); }; -function _categoryLabel(category: string): string { - const labels: Record = { - leadership: 'Führung', - conflict: 'Konflikt', - negotiation: 'Verhandlung', - presentation: 'Präsentation', - feedback: 'Feedback', - delegation: 'Delegation', - changeManagement: 'Change Mgmt', - custom: 'Individuell', - }; - return labels[category] || category; -} - function _badgeIcon(icon?: string): string { const icons: Record = { star: '\u2605', fire: '\u{1F525}', trophy: '\u{1F3C6}', diff --git a/src/pages/views/commcoach/CommcoachDossierView.tsx b/src/pages/views/commcoach/CommcoachDossierView.tsx index a145285..55f97d9 100644 --- a/src/pages/views/commcoach/CommcoachDossierView.tsx +++ b/src/pages/views/commcoach/CommcoachDossierView.tsx @@ -63,6 +63,10 @@ export const CommcoachDossierView: React.FC = ({ pers persistentMandateId, }) => { const { t } = useLanguage(); + const _toolPayloadForDisplay = (payload: Record): string => { + const formatted = _formatToolPayload(payload); + return formatted === '[unlesbar]' ? t('[unlesbar]') : formatted; + }; const routeInstanceId = useInstanceId(); const routeMandateId = useMandateId(); const instanceId = persistentInstanceId || routeInstanceId; @@ -512,17 +516,17 @@ export const CommcoachDossierView: React.FC = ({ pers : styles.agentActivityBadgeRunning }`} > - {toolCall.success === true ? 'fertig' : toolCall.success === false ? 'fehler' : 'läuft'} + {toolCall.success === true ? t('fertig') : toolCall.success === false ? t('fehler') : t('läuft')}
{toolCall.args && (
- Args: {_formatToolPayload(toolCall.args)} + {t('Argumente:')} {_toolPayloadForDisplay(toolCall.args)}
)} {toolCall.result && (
- Result: {toolCall.result} + {t('Ergebnis:')} {toolCall.result}
)} @@ -854,7 +858,7 @@ export const CommcoachDossierView: React.FC = ({ pers {_groupScoresByDimension(coach.scores).map(group => (
- {_dimensionLabel(group.dimension)} + {t(_dimensionLabel(group.dimension))} {Math.round(group.latest.score)}/100 {group.latest.trend === 'improving' ? 'steigend' : group.latest.trend === 'declining' ? 'sinkend' : 'stabil'} diff --git a/src/pages/views/commcoach/CommcoachSettingsView.tsx b/src/pages/views/commcoach/CommcoachSettingsView.tsx index 8cd2ff1..e052cc2 100644 --- a/src/pages/views/commcoach/CommcoachSettingsView.tsx +++ b/src/pages/views/commcoach/CommcoachSettingsView.tsx @@ -87,38 +87,38 @@ export const CommcoachSettingsView: React.FC = () => {

{t('Stimme/Sprache')}

- Stimme und Sprache werden zentral in den Benutzereinstellungen konfiguriert. + {t('Stimme und Sprache werden zentral in den Benutzereinstellungen konfiguriert.')}

{}} style={{ fontSize: '0.85rem', color: 'var(--primary-color, #2563eb)' }}> - Benutzereinstellungen oeffnen (Tab "Stimme & Sprache") + {t('Benutzereinstellungen öffnen (Tab "Stimme & Sprache")')}
-

Erinnerungen

+

{t('Erinnerungen')}

{reminderEnabled && (
- + setReminderTime(e.target.value)} />
)}
{profile && (
-

Statistik

+

{t('Statistik')}

{profile.totalSessions}{t('Sessions gesamt')}
{profile.totalMinutes}{t('Minuten gesamt')}
diff --git a/src/pages/views/graphicalEditor/GraphicalEditorTemplatesPage.tsx b/src/pages/views/graphicalEditor/GraphicalEditorTemplatesPage.tsx index 72d2499..9cf2f17 100644 --- a/src/pages/views/graphicalEditor/GraphicalEditorTemplatesPage.tsx +++ b/src/pages/views/graphicalEditor/GraphicalEditorTemplatesPage.tsx @@ -6,7 +6,7 @@ * Actions: Copy to my workflows, Share (scope upgrade), Delete. */ -import React, { useState, useCallback, useEffect } from 'react'; +import React, { useState, useCallback, useEffect, useMemo } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { FaCopy, FaSync, FaShareAlt, FaPen } from 'react-icons/fa'; import { usePrompt } from '../../../hooks/usePrompt'; @@ -28,13 +28,6 @@ import styles from '../../../pages/admin/Admin.module.css'; import { useLanguage } from '../../../providers/language/LanguageContext'; -const SCOPE_LABELS: Record = { - user: 'Meine', - instance: 'Instanz', - mandate: 'Mandant', - system: 'System', -}; - function _formatTs(ts?: number): string { if (ts == null || ts <= 0) return '—'; const sec = ts < 1e12 ? ts : ts / 1000; @@ -51,6 +44,16 @@ function _formatTs(ts?: number): string { export const GraphicalEditorTemplatesPage: React.FC = () => { const { t } = useLanguage(); + const scopeLabels = useMemo( + (): Record => ({ + user: t('Meine'), + instance: t('Instanz'), + mandate: t('Mandant'), + system: t('System'), + }), + [t], + ); + const instanceId = useInstanceId(); const { mandateId } = useParams<{ mandateId: string }>(); const { request } = useApiRequest(); @@ -81,11 +84,11 @@ export const GraphicalEditorTemplatesPage: React.FC = () => { } } catch (e) { console.error('[graphicalEditor] load templates failed', e); - showError('Fehler beim Laden der Vorlagen'); + showError(t('Fehler beim Laden der Vorlagen')); } finally { setLoading(false); } - }, [instanceId, request, showError, activeScope]); + }, [instanceId, request, showError, activeScope, t]); useEffect(() => { load(); @@ -96,15 +99,15 @@ export const GraphicalEditorTemplatesPage: React.FC = () => { if (!instanceId) return false; try { await deleteWorkflow(request, instanceId, templateId); - showSuccess('Vorlage gelöscht'); + showSuccess(t('Vorlage gelöscht')); await load(); return true; } catch (e: any) { - showError(`Fehler: ${e?.message || 'Löschen fehlgeschlagen'}`); + showError(t('Fehler: {msg}', { msg: e?.message || t('Löschen fehlgeschlagen') })); return false; } }, - [instanceId, request, showSuccess, showError, load] + [instanceId, request, showSuccess, showError, load, t] ); const handleCopy = useCallback( @@ -113,14 +116,14 @@ export const GraphicalEditorTemplatesPage: React.FC = () => { setCopyingId(row.id); try { await copyTemplate(request, instanceId, row.id); - showSuccess('Vorlage als Workflow kopiert'); + showSuccess(t('Vorlage als Workflow kopiert')); } catch (e: any) { - showError(`Fehler: ${e?.message || 'Kopieren fehlgeschlagen'}`); + showError(t('Fehler: {msg}', { msg: e?.message || t('Kopieren fehlgeschlagen') })); } finally { setCopyingId(null); } }, - [instanceId, request, showSuccess, showError] + [instanceId, request, showSuccess, showError, t] ); const handleShare = useCallback( @@ -129,15 +132,15 @@ export const GraphicalEditorTemplatesPage: React.FC = () => { setSharingId(row.id); try { await shareTemplate(request, instanceId, row.id, targetScope); - showSuccess(`Scope geändert: ${SCOPE_LABELS[targetScope]}`); + showSuccess(t('Scope geändert: {scope}', { scope: scopeLabels[targetScope] })); await load(); } catch (e: any) { - showError(`Fehler: ${e?.message || 'Scope-Änderung fehlgeschlagen'}`); + showError(t('Fehler: {msg}', { msg: e?.message || t('Scope-Änderung fehlgeschlagen') })); } finally { setSharingId(null); } }, - [instanceId, request, showSuccess, showError, load] + [instanceId, request, showSuccess, showError, load, scopeLabels, t] ); const [scopeMenuId, setScopeMenuId] = useState(null); @@ -145,21 +148,21 @@ export const GraphicalEditorTemplatesPage: React.FC = () => { const handleRename = useCallback( async (row: AutoWorkflowTemplate) => { if (!instanceId) return; - const newLabel = await promptInput('Neuer Name:', { + const newLabel = await promptInput(t('Neuer Name:'), { title: t('Vorlage umbenennen'), defaultValue: row.label, - placeholder: 'Vorlagen-Name', + placeholder: t('Vorlagen-Name'), }); if (!newLabel || newLabel.trim() === row.label) return; try { await updateWorkflow(request, instanceId, row.id, { label: newLabel.trim() }); - showSuccess('Vorlage umbenannt'); + showSuccess(t('Vorlage umbenannt')); await load(); } catch (e: any) { - showError(`Fehler: ${e?.message || 'Umbenennen fehlgeschlagen'}`); + showError(t('Fehler: {msg}', { msg: e?.message || t('Umbenennen fehlgeschlagen') })); } }, - [instanceId, request, promptInput, showSuccess, showError, load] + [instanceId, request, promptInput, showSuccess, showError, load, t] ); const handleEdit = useCallback( @@ -170,14 +173,15 @@ export const GraphicalEditorTemplatesPage: React.FC = () => { [mandateId, instanceId, navigate] ); - const columns: ColumnConfig[] = [ + const columns: ColumnConfig[] = useMemo( + () => [ { key: 'label', label: t('Vorlage'), type: 'string', width: 220, sortable: true }, { key: 'templateScope', label: t('Bereich'), type: 'string', width: 100, - formatter: (v: string) => SCOPE_LABELS[v as AutoTemplateScope] ?? v ?? '—', + formatter: (v: string) => scopeLabels[v as AutoTemplateScope] ?? v ?? '—', }, { key: 'sharedReadOnly', @@ -186,9 +190,9 @@ export const GraphicalEditorTemplatesPage: React.FC = () => { width: 100, formatter: (v: boolean) => v ? ( - Ja + {t('Ja')} ) : ( - Nein + {t('Nein')} ), }, { @@ -204,7 +208,9 @@ export const GraphicalEditorTemplatesPage: React.FC = () => { width: 140, formatter: (v: number) => _formatTs(v), }, - ]; + ], + [t, scopeLabels], + ); if (!instanceId) { return ( @@ -219,7 +225,7 @@ export const GraphicalEditorTemplatesPage: React.FC = () => {

- Vorlagen verwalten, kopieren und freigeben + {t('Vorlagen verwalten, kopieren und freigeben')}

@@ -231,7 +237,7 @@ export const GraphicalEditorTemplatesPage: React.FC = () => { onClick={() => setActiveScope(s)} disabled={loading} > - {s === 'all' ? 'Alle' : SCOPE_LABELS[s as AutoTemplateScope]} + {s === 'all' ? t('Alle') : scopeLabels[s as AutoTemplateScope]} ))}
@@ -240,7 +246,7 @@ export const GraphicalEditorTemplatesPage: React.FC = () => { onClick={() => load()} disabled={loading} > - Aktualisieren + {t('Aktualisieren')}
@@ -299,7 +305,7 @@ export const GraphicalEditorTemplatesPage: React.FC = () => { {/* Scope change dropdown overlay */} {scopeMenuId && (() => { - const tpl = templates.find(t => t.id === scopeMenuId); + const tpl = templates.find((row) => row.id === scopeMenuId); if (!tpl) return null; const currentScope = (tpl.templateScope || 'user') as AutoTemplateScope; const scopes: AutoTemplateScope[] = ['user', 'instance', 'mandate']; @@ -318,7 +324,7 @@ export const GraphicalEditorTemplatesPage: React.FC = () => { >

{t('Bereich ändern')}

- Aktuell: {SCOPE_LABELS[currentScope]} + {t('Aktuell:')} {scopeLabels[currentScope]}

{scopes.map(s => ( @@ -333,7 +339,7 @@ export const GraphicalEditorTemplatesPage: React.FC = () => { fontWeight: s === currentScope ? 600 : 400, }} > - {SCOPE_LABELS[s]} {s === currentScope && '(aktuell)'} + {scopeLabels[s]} {s === currentScope && t('(aktuell)')} ))}
@@ -341,7 +347,7 @@ export const GraphicalEditorTemplatesPage: React.FC = () => { onClick={() => setScopeMenuId(null)} style={{ marginTop: 12, padding: '4px 12px', border: '1px solid var(--border-color, #ddd)', borderRadius: 4, background: 'transparent', cursor: 'pointer', fontSize: '0.8rem' }} > - Abbrechen + {t('Abbrechen')}
diff --git a/src/pages/views/graphicalEditor/GraphicalEditorWorkflowsPage.tsx b/src/pages/views/graphicalEditor/GraphicalEditorWorkflowsPage.tsx index df05d50..72b6943 100644 --- a/src/pages/views/graphicalEditor/GraphicalEditorWorkflowsPage.tsx +++ b/src/pages/views/graphicalEditor/GraphicalEditorWorkflowsPage.tsx @@ -72,11 +72,11 @@ export const GraphicalEditorWorkflowsPage: React.FC = () => { } } catch (e) { console.error('[graphicalEditor] load workflows failed', e); - showError('Fehler beim Laden der Workflows'); + showError(t('Fehler beim Laden der Workflows')); } finally { setLoading(false); } - }, [instanceId, request, showError, activeFilter]); + }, [instanceId, request, showError, activeFilter, t]); useEffect(() => { load(); @@ -87,15 +87,15 @@ export const GraphicalEditorWorkflowsPage: React.FC = () => { if (!instanceId) return false; try { await deleteWorkflow(request, instanceId, workflowId); - showSuccess('Workflow gelöscht'); + showSuccess(t('Workflow gelöscht')); await load(); return true; } catch (e: any) { - showError(`Fehler: ${e?.message || 'Löschen fehlgeschlagen'}`); + showError(t('Fehler: {msg}', { msg: e?.message || t('Löschen fehlgeschlagen') })); return false; } }, - [instanceId, request, showSuccess, showError, load] + [instanceId, request, showSuccess, showError, load, t] ); const handleEdit = useCallback( @@ -121,32 +121,32 @@ export const GraphicalEditorWorkflowsPage: React.FC = () => { showSuccess(next ? t('Workflow aktiviert') : t('Workflow deaktiviert')); await load(); } catch (e: any) { - showError(`Fehler: ${e?.message || 'Status-Update fehlgeschlagen'}`); + showError(t('Fehler: {msg}', { msg: e?.message || t('Status-Update fehlgeschlagen') })); } finally { setTogglingId(null); } }, - [instanceId, request, showSuccess, showError, load] + [instanceId, request, showSuccess, showError, load, t] ); const handleRename = useCallback( async (row: Automation2Workflow) => { if (!instanceId) return; - const newLabel = await promptInput('Neuer Name:', { + const newLabel = await promptInput(t('Neuer Name:'), { title: t('Workflow umbenennen'), defaultValue: row.label, - placeholder: 'Workflow-Name', + placeholder: t('Workflow-Name'), }); if (!newLabel || newLabel.trim() === row.label) return; try { await updateWorkflow(request, instanceId, row.id, { label: newLabel.trim() }); - showSuccess('Workflow umbenannt'); + showSuccess(t('Workflow umbenannt')); await load(); } catch (e: any) { - showError(`Fehler: ${e?.message || 'Umbenennen fehlgeschlagen'}`); + showError(t('Fehler: {msg}', { msg: e?.message || t('Umbenennen fehlgeschlagen') })); } }, - [instanceId, request, promptInput, showSuccess, showError, load] + [instanceId, request, promptInput, showSuccess, showError, load, t] ); const handleExecute = useCallback( @@ -163,21 +163,21 @@ export const GraphicalEditorWorkflowsPage: React.FC = () => { }); if (result?.success) { if (result?.paused) { - showSuccess('Workflow gestartet und bei Human Task pausiert. Öffne Workflows & Tasks.'); + showSuccess(t('Workflow gestartet und bei Human Task pausiert. Öffne Workflows & Tasks.')); } else { - showSuccess('Workflow ausgeführt'); + showSuccess(t('Workflow ausgeführt')); } await load(); } else { - showError(result?.error || 'Ausführung fehlgeschlagen'); + showError(result?.error || t('Ausführung fehlgeschlagen')); } } catch (e: any) { - showError(`Fehler: ${e?.message || 'Ausführung fehlgeschlagen'}`); + showError(t('Fehler: {msg}', { msg: e?.message || t('Ausführung fehlgeschlagen') })); } finally { setExecutingId(null); } }, - [instanceId, request, showSuccess, showError, load] + [instanceId, request, showSuccess, showError, load, t] ); const columns: ColumnConfig[] = [ @@ -258,7 +258,7 @@ export const GraphicalEditorWorkflowsPage: React.FC = () => {

- Workflows verwalten, ausführen und bearbeiten + {t('Workflows verwalten, ausführen und bearbeiten')}

@@ -270,7 +270,7 @@ export const GraphicalEditorWorkflowsPage: React.FC = () => { onClick={() => setActiveFilter(f)} disabled={loading} > - {f === 'all' ? 'Alle' : f === 'active' ? t('Aktiv') : t('Inaktiv')} + {f === 'all' ? t('Alle') : f === 'active' ? t('Aktiv') : t('Inaktiv')} ))}
@@ -279,7 +279,7 @@ export const GraphicalEditorWorkflowsPage: React.FC = () => { onClick={() => load()} disabled={loading} > - Aktualisieren + {t('Aktualisieren')}
diff --git a/src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx b/src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx index c6c0813..96daa7f 100644 --- a/src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx +++ b/src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx @@ -165,27 +165,27 @@ export const GraphicalEditorWorkflowsTasksPage: React.FC = () => { }); if (result?.success) { if (result?.paused) { - showSuccess('Workflow gestartet und bei Human Task pausiert.'); + showSuccess(t('Workflow gestartet und bei Human Task pausiert.')); } else { - showSuccess('Workflow gestartet'); + showSuccess(t('Workflow gestartet')); } await load(); } else { - showError(result?.error || 'Ausführung fehlgeschlagen'); + showError(result?.error || t('Ausführung fehlgeschlagen')); } } catch (e: unknown) { const msg = - (e as { message?: string })?.message ?? 'Ausführung fehlgeschlagen'; + (e as { message?: string })?.message ?? t('Ausführung fehlgeschlagen'); showError(msg); } finally { setExecutingWorkflowId(null); } }, - [instanceId, request, showSuccess, showError, load] + [instanceId, request, showSuccess, showError, load, t] ); - const openTasks = tasks.filter((t) => t.status === 'pending'); - const completedTasks = tasks.filter((t) => t.status !== 'pending'); + const openTasks = tasks.filter((task) => task.status === 'pending'); + const completedTasks = tasks.filter((task) => task.status !== 'pending'); if (!instanceId) { return ( @@ -211,7 +211,7 @@ export const GraphicalEditorWorkflowsTasksPage: React.FC = () => { {/* Open tasks */}

- Offene Tasks + {t('Offene Tasks')} {openTasks.length > 0 && {openTasks.length}}

{openTasks.length === 0 ? ( @@ -272,7 +272,7 @@ export const GraphicalEditorWorkflowsTasksPage: React.FC = () => { onClick={() => setOutputExpanded((p) => !p)} > {outputExpanded ? : } - Output + {t('Resultate')} {completedRuns.length > 0 && ( {completedRuns.length} )} @@ -281,7 +281,7 @@ export const GraphicalEditorWorkflowsTasksPage: React.FC = () => {
{completedRuns.length === 0 ? (

- Keine abgeschlossenen Workflows. Führen Sie einen Workflow aus (z.B. im Editor), um hier die Ergebnisse zu sehen. + {t('Keine abgeschlossenen Workflows. Führen Sie einen Workflow aus (z.B. im Editor), um hier die Ergebnisse zu sehen.')}

) : ( completedRuns.map((run) => ( @@ -299,7 +299,7 @@ export const GraphicalEditorWorkflowsTasksPage: React.FC = () => {
{startableWorkflows.length === 0 ? (

- Keine aktiven Workflows mit manuellem oder Formular-Start. + {t('Keine aktiven Workflows mit manuellem oder Formular-Start.')}

) : ( startableWorkflows.map((wf) => { @@ -356,7 +356,7 @@ const OutputCard: React.FC<{ const fileId = (d.validationMetadata as Record)?.fileId as string | undefined; if (fileId) { files.push({ - name: String(d.documentName ?? d.fileName ?? 'Datei'), + name: String(d.documentName ?? d.fileName ?? t('Datei')), fileId, }); } @@ -366,11 +366,11 @@ const OutputCard: React.FC<{
- Workflow + {t('Workflow')} {run.workflowLabel || run.workflowId || '—'}
- Abgeschlossen + {t('Abgeschlossen')} {formatTimestamp(ts)}
@@ -449,7 +449,7 @@ function InputFormClickupTaskField({ .catch(() => { if (!cancelled) { setTasks([]); - setErr('Aufgaben konnten nicht geladen werden.'); + setErr(t('Aufgaben konnten nicht geladen werden.')); } }) .finally(() => { @@ -465,8 +465,7 @@ function InputFormClickupTaskField({ if (!connectionId.trim() || !listId.trim()) { return (

- Für dieses Feld sind im Formular-Node ClickUp-Verbindung und Listen-ID gesetzt — bitte Workflow - prüfen. + {t('Für dieses Feld sind im Formular-Node ClickUp-Verbindung und Listen-ID gesetzt — bitte Workflow prüfen.')}

); } @@ -670,14 +669,14 @@ const TaskCard: React.FC = ({ onClick={() => onSubmit({ approved: true })} disabled={submitting} > - Genehmigen + {t('Genehmigen')}
@@ -686,7 +685,7 @@ const TaskCard: React.FC = ({ return (