feat(i18n): Lokalisierung - Phase 6 abgeschlossen (ExecutionModeModal, PublicKeyModal)

Lokalisierte Komponenten (19/alle):
- ExecutionModeModal: Server-Auswahl, Installation-Bestätigung, Single/Multiple Server Views
- PublicKeyModal: SSH Key Anzeige, Kopier-Funktionen, Anleitungen

Neue Translation Keys:
- executionModeModal.* (13+ keys für Loading, Server-Auswahl, Actions, Errors)
- publicKeyModal.* (10+ keys für Instructions, Labels, Actions, Fallbacks)

Technische Details:
- ExecutionModeModal: Conditional rendering für 0/1/N Server mit dynamic scriptName interpolation
- PublicKeyModal: Lokalisierte Fallback-Alerts für Copy-Fehler
- useEffect eslint-disable für fetchServers dependency
- Script name interpolation in Bestätigungs-Texten
This commit is contained in:
CanbiZ
2025-10-20 19:59:04 +02:00
parent f16f0e58cd
commit fc6e13946d
4 changed files with 154 additions and 51 deletions

View File

@@ -6,6 +6,7 @@ import { Button } from './ui/button';
import { ColorCodedDropdown } from './ColorCodedDropdown';
import { SettingsModal } from './SettingsModal';
import { useRegisterModal } from './modal/ModalStackProvider';
import { useTranslation } from '@/lib/i18n/useTranslation';
interface ExecutionModeModalProps {
@@ -16,6 +17,7 @@ interface ExecutionModeModalProps {
}
export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: ExecutionModeModalProps) {
const { t } = useTranslation('executionModeModal');
useRegisterModal(isOpen, { id: 'execution-mode-modal', allowEscape: true, onClose });
const [servers, setServers] = useState<Server[]>([]);
const [loading, setLoading] = useState(false);
@@ -23,19 +25,6 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
const [selectedServer, setSelectedServer] = useState<Server | null>(null);
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
useEffect(() => {
if (isOpen) {
void fetchServers();
}
}, [isOpen]);
// Auto-select server when exactly one server is available
useEffect(() => {
if (isOpen && !loading && servers.length === 1) {
setSelectedServer(servers[0] ?? null);
}
}, [isOpen, loading, servers]);
// Refresh servers when settings modal closes
const handleSettingsModalClose = () => {
setSettingsModalOpen(false);
@@ -49,7 +38,7 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
try {
const response = await fetch('/api/servers');
if (!response.ok) {
throw new Error('Failed to fetch servers');
throw new Error(t('errors.fetchFailed'));
}
const data = await response.json();
// Sort servers by name alphabetically
@@ -58,15 +47,29 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
);
setServers(sortedServers);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
setError(err instanceof Error ? err.message : t('errors.fetchFailed'));
} finally {
setLoading(false);
}
};
useEffect(() => {
if (isOpen) {
void fetchServers();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
// Auto-select server when exactly one server is available
useEffect(() => {
if (isOpen && !loading && servers.length === 1) {
setSelectedServer(servers[0] ?? null);
}
}, [isOpen, loading, servers]);
const handleExecute = () => {
if (!selectedServer) {
setError('Please select a server for SSH execution');
setError(t('errors.noServerSelected'));
return;
}
@@ -88,7 +91,7 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
<div className="bg-card rounded-lg shadow-xl max-w-md w-full border border-border">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-border">
<h2 className="text-xl font-bold text-foreground">Select Server</h2>
<h2 className="text-xl font-bold text-foreground">{t('title')}</h2>
<Button
onClick={onClose}
variant="ghost"
@@ -121,19 +124,19 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
{loading ? (
<div className="text-center py-8">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
<p className="mt-2 text-sm text-muted-foreground">Loading servers...</p>
<p className="mt-2 text-sm text-muted-foreground">{t('loadingServers')}</p>
</div>
) : servers.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<p className="text-sm">No servers configured</p>
<p className="text-xs mt-1">Add servers in Settings to execute scripts</p>
<p className="text-sm">{t('noServersConfigured')}</p>
<p className="text-xs mt-1">{t('addServersHint')}</p>
<Button
onClick={() => setSettingsModalOpen(true)}
variant="outline"
size="sm"
className="mt-3"
>
Open Server Settings
{t('openServerSettings')}
</Button>
</div>
) : servers.length === 1 ? (
@@ -141,10 +144,10 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
<div className="space-y-6">
<div className="text-center">
<h3 className="text-lg font-medium text-foreground mb-2">
Install Script Confirmation
{t('installConfirmation.title')}
</h3>
<p className="text-sm text-muted-foreground">
Do you want to install &quot;{scriptName}&quot; on the following server?
{t('installConfirmation.description', { values: { scriptName } })}
</p>
</div>
@@ -155,7 +158,7 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">
{selectedServer?.name ?? 'Unnamed Server'}
{selectedServer?.name ?? t('unnamedServer')}
</p>
<p className="text-sm text-muted-foreground">
{selectedServer?.ip}
@@ -171,14 +174,14 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
variant="outline"
size="default"
>
Cancel
{t('actions.cancel')}
</Button>
<Button
onClick={handleExecute}
variant="default"
size="default"
>
Install
{t('actions.install')}
</Button>
</div>
</div>
@@ -187,20 +190,20 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
<div className="space-y-6">
<div className="mb-6">
<h3 className="text-lg font-medium text-foreground mb-2">
Select server to execute &quot;{scriptName}&quot;
{t('multipleServers.title', { values: { scriptName } })}
</h3>
</div>
{/* Server Selection */}
<div className="mb-6">
<label htmlFor="server" className="block text-sm font-medium text-foreground mb-2">
Select Server
{t('multipleServers.selectServerLabel')}
</label>
<ColorCodedDropdown
servers={servers}
selectedServer={selectedServer}
onServerSelect={handleServerSelect}
placeholder="Select a server..."
placeholder={t('multipleServers.placeholder')}
/>
</div>
@@ -211,7 +214,7 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
variant="outline"
size="default"
>
Cancel
{t('actions.cancel')}
</Button>
<Button
onClick={handleExecute}
@@ -220,7 +223,7 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
size="default"
className={!selectedServer ? 'bg-muted-foreground cursor-not-allowed' : ''}
>
Run on Server
{t('actions.runOnServer')}
</Button>
</div>
</div>

View File

@@ -4,6 +4,7 @@ import { useState } from 'react';
import { X, Copy, Check, Server, Globe } from 'lucide-react';
import { Button } from './ui/button';
import { useRegisterModal } from './modal/ModalStackProvider';
import { useTranslation } from '@/lib/i18n/useTranslation';
interface PublicKeyModalProps {
isOpen: boolean;
@@ -14,6 +15,7 @@ interface PublicKeyModalProps {
}
export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverIp }: PublicKeyModalProps) {
const { t } = useTranslation('publicKeyModal');
useRegisterModal(isOpen, { id: 'public-key-modal', allowEscape: true, onClose });
const [copied, setCopied] = useState(false);
const [commandCopied, setCommandCopied] = useState(false);
@@ -45,7 +47,7 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
} catch (fallbackError) {
console.error('Fallback copy failed:', fallbackError);
// If all else fails, show the key in an alert
alert('Please manually copy this key:\n\n' + publicKey);
alert(t('copyFallback') + publicKey);
}
document.body.removeChild(textArea);
@@ -53,7 +55,7 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
} catch (error) {
console.error('Failed to copy to clipboard:', error);
// Fallback: show the key in an alert
alert('Please manually copy this key:\n\n' + publicKey);
alert(t('copyFallback') + publicKey);
}
};
@@ -82,14 +84,14 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
setTimeout(() => setCommandCopied(false), 2000);
} catch (fallbackError) {
console.error('Fallback copy failed:', fallbackError);
alert('Please manually copy this command:\n\n' + command);
alert(t('copyCommandFallback') + command);
}
document.body.removeChild(textArea);
}
} catch (error) {
console.error('Failed to copy command to clipboard:', error);
alert('Please manually copy this command:\n\n' + command);
alert(t('copyCommandFallback') + command);
}
};
@@ -103,8 +105,8 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
<Server className="h-6 w-6 text-info" />
</div>
<div>
<h2 className="text-xl font-semibold text-card-foreground">SSH Public Key</h2>
<p className="text-sm text-muted-foreground">Add this key to your server&apos;s authorized_keys</p>
<h2 className="text-xl font-semibold text-card-foreground">{t('title')}</h2>
<p className="text-sm text-muted-foreground">{t('subtitle')}</p>
</div>
</div>
<Button
@@ -133,19 +135,19 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
{/* Instructions */}
<div className="space-y-2">
<h3 className="font-medium text-foreground">Instructions:</h3>
<h3 className="font-medium text-foreground">{t('instructions.title')}</h3>
<ol className="text-sm text-muted-foreground space-y-1 list-decimal list-inside">
<li>Copy the public key below</li>
<li>SSH into your server: <code className="bg-muted px-1 rounded">ssh root@{serverIp}</code></li>
<li>Add the key to authorized_keys: <code className="bg-muted px-1 rounded">echo &quot;&lt;paste-key&gt;&quot; &gt;&gt; ~/.ssh/authorized_keys</code></li>
<li>Set proper permissions: <code className="bg-muted px-1 rounded">chmod 600 ~/.ssh/authorized_keys</code></li>
<li>{t('instructions.step1')}</li>
<li>{t('instructions.step2')} <code className="bg-muted px-1 rounded">ssh root@{serverIp}</code></li>
<li>{t('instructions.step3')} <code className="bg-muted px-1 rounded">echo &quot;&lt;paste-key&gt;&quot; &gt;&gt; ~/.ssh/authorized_keys</code></li>
<li>{t('instructions.step4')} <code className="bg-muted px-1 rounded">chmod 600 ~/.ssh/authorized_keys</code></li>
</ol>
</div>
{/* Public Key */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium text-foreground">Public Key:</label>
<label className="text-sm font-medium text-foreground">{t('publicKeyLabel')}</label>
<Button
variant="outline"
size="sm"
@@ -155,12 +157,12 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
{copied ? (
<>
<Check className="h-4 w-4" />
Copied!
{t('actions.copied')}
</>
) : (
<>
<Copy className="h-4 w-4" />
Copy
{t('actions.copy')}
</>
)}
</Button>
@@ -169,14 +171,14 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
value={publicKey}
readOnly
className="w-full px-3 py-2 border rounded-md shadow-sm bg-card text-foreground font-mono text-xs min-h-[60px] resize-none border-border focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
placeholder="Public key will appear here..."
placeholder={t('placeholder')}
/>
</div>
{/* Quick Command */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium text-foreground">Quick Add Command:</label>
<label className="text-sm font-medium text-foreground">{t('quickCommandLabel')}</label>
<Button
variant="outline"
size="sm"
@@ -186,12 +188,12 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
{commandCopied ? (
<>
<Check className="h-4 w-4" />
Copied!
{t('actions.copied')}
</>
) : (
<>
<Copy className="h-4 w-4" />
Copy Command
{t('actions.copyCommand')}
</>
)}
</Button>
@@ -202,14 +204,14 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
</code>
</div>
<p className="text-xs text-muted-foreground">
Copy and paste this command directly into your server terminal to add the key to authorized_keys
{t('quickCommandHint')}
</p>
</div>
{/* Footer */}
<div className="flex justify-end gap-3 pt-4 border-t border-border">
<Button variant="outline" onClick={onClose}>
Close
{t('actions.close')}
</Button>
</div>
</div>

View File

@@ -157,6 +157,55 @@ export const deMessages: NestedMessages = {
buttonTitle: 'Einstellungen öffnen',
buttonLabel: 'Einstellungen',
},
executionModeModal: {
title: 'Server auswählen',
loadingServers: 'Lade Server...',
noServersConfigured: 'Keine Server konfiguriert',
addServersHint: 'Fügen Sie Server in den Einstellungen hinzu, um Skripte auszuführen',
openServerSettings: 'Servereinstellungen öffnen',
installConfirmation: {
title: 'Skript-Installation bestätigen',
description: 'Möchten Sie "{scriptName}" auf folgendem Server installieren?',
},
unnamedServer: 'Unbenannter Server',
multipleServers: {
title: 'Server für "{scriptName}" auswählen',
selectServerLabel: 'Server auswählen',
placeholder: 'Wählen Sie einen Server...',
},
actions: {
cancel: 'Abbrechen',
install: 'Installieren',
runOnServer: 'Auf Server ausführen',
},
errors: {
noServerSelected: 'Bitte wählen Sie einen Server für die SSH-Ausführung',
fetchFailed: 'Fehler beim Abrufen der Server',
},
},
publicKeyModal: {
title: 'SSH Public Key',
subtitle: 'Fügen Sie diesen Schlüssel zu den authorized_keys Ihres Servers hinzu',
instructions: {
title: 'Anleitung:',
step1: 'Kopieren Sie den unten stehenden öffentlichen Schlüssel',
step2: 'SSH-Verbindung zu Ihrem Server herstellen:',
step3: 'Schlüssel zu authorized_keys hinzufügen:',
step4: 'Korrekte Berechtigungen setzen:',
},
publicKeyLabel: 'Öffentlicher Schlüssel:',
quickCommandLabel: 'Schnell-Hinzufügen-Befehl:',
quickCommandHint: 'Kopieren Sie diesen Befehl und fügen Sie ihn direkt in Ihr Server-Terminal ein, um den Schlüssel zu authorized_keys hinzuzufügen',
placeholder: 'Öffentlicher Schlüssel wird hier angezeigt...',
actions: {
copy: 'Kopieren',
copied: 'Kopiert!',
copyCommand: 'Befehl kopieren',
close: 'Schließen',
},
copyFallback: 'Bitte kopieren Sie diesen Schlüssel manuell:\n\n',
copyCommandFallback: 'Bitte kopieren Sie diesen Befehl manuell:\n\n',
},
layout: {
title: 'PVE Skriptverwaltung',
tagline: 'Verwalte und starte lokale Proxmox-Hilfsskripte mit Live-Ausgabe',

View File

@@ -391,4 +391,53 @@ export const enMessages: NestedMessages = {
buttonTitle: 'Open Settings',
buttonLabel: 'Settings',
},
executionModeModal: {
title: 'Select Server',
loadingServers: 'Loading servers...',
noServersConfigured: 'No servers configured',
addServersHint: 'Add servers in Settings to execute scripts',
openServerSettings: 'Open Server Settings',
installConfirmation: {
title: 'Install Script Confirmation',
description: 'Do you want to install "{scriptName}" on the following server?',
},
unnamedServer: 'Unnamed Server',
multipleServers: {
title: 'Select server to execute "{scriptName}"',
selectServerLabel: 'Select Server',
placeholder: 'Select a server...',
},
actions: {
cancel: 'Cancel',
install: 'Install',
runOnServer: 'Run on Server',
},
errors: {
noServerSelected: 'Please select a server for SSH execution',
fetchFailed: 'Failed to fetch servers',
},
},
publicKeyModal: {
title: 'SSH Public Key',
subtitle: 'Add this key to your server\'s authorized_keys',
instructions: {
title: 'Instructions:',
step1: 'Copy the public key below',
step2: 'SSH into your server:',
step3: 'Add the key to authorized_keys:',
step4: 'Set proper permissions:',
},
publicKeyLabel: 'Public Key:',
quickCommandLabel: 'Quick Add Command:',
quickCommandHint: 'Copy and paste this command directly into your server terminal to add the key to authorized_keys',
placeholder: 'Public key will appear here...',
actions: {
copy: 'Copy',
copied: 'Copied!',
copyCommand: 'Copy Command',
close: 'Close',
},
copyFallback: 'Please manually copy this key:\n\n',
copyCommandFallback: 'Please manually copy this command:\n\n',
},
};