feat: übersetze gesamte UI auf Deutsch

- Alle Seiten (AirScale, LinkedIn, SERP, Ergebnisse, Einstellungen) auf Deutsch
- Gemeinsame Komponenten übersetzt: StatusBadge, ResultsTable-Spalten, FileDropZone, ExportButtons
- Sidebar API-Status-Label und TopBar-Breadcrumbs auf Deutsch
- Alle Toast-Nachrichten und Fehlermeldungen auf Deutsch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Timo Uttenweiler
2026-03-17 12:40:05 +01:00
parent 7486517827
commit f6bdc65b1e
11 changed files with 160 additions and 160 deletions

View File

@@ -59,8 +59,8 @@ export default function LinkedInPage() {
const urlValid = salesNavUrl.includes("linkedin.com/sales");
const startScrape = async () => {
if (!urlValid) return toast.error("Please paste a valid Sales Navigator URL");
if (!vayneConfigured) return toast.error("Configure your Vayne API token in Settings first");
if (!urlValid) return toast.error("Bitte gültige Sales Navigator URL einfügen");
if (!vayneConfigured) return toast.error("Bitte zuerst Vayne API-Token in den Einstellungen konfigurieren");
setStage("scraping");
setScrapeProgress({ current: 0, total: maxResults });
@@ -79,7 +79,7 @@ export default function LinkedInPage() {
addJob({ id: data.jobId, type: "linkedin-scrape", status: "running", progress: 0, total: maxResults });
pollScrape(data.jobId);
} catch (err) {
toast.error(err instanceof Error ? err.message : "Failed to start scrape");
toast.error(err instanceof Error ? err.message : "Scraping konnte nicht gestartet werden");
setStage("failed");
}
};
@@ -104,10 +104,10 @@ export default function LinkedInPage() {
setStage("scraped");
setResults(data.results || []);
setSelectedIds(data.results?.map(r => r.id) || []);
toast.success(`Scraped ${data.totalLeads} profiles from LinkedIn`);
toast.success(`${data.totalLeads} Profile von LinkedIn gescrapt`);
} else {
setStage("failed");
toast.error("Scrape failed. Check Vayne token in Settings.");
toast.error("Scraping fehlgeschlagen. Bitte Vayne-Token in den Einstellungen prüfen.");
}
}
} catch {
@@ -118,8 +118,8 @@ export default function LinkedInPage() {
};
const startEnrich = async () => {
if (!selectedIds.length) return toast.error("Select at least one profile to enrich");
if (!categories.length) return toast.error("Select at least one category");
if (!selectedIds.length) return toast.error("Mindestens ein Profil zum Anreichern auswählen");
if (!categories.length) return toast.error("Mindestens eine Kategorie auswählen");
setStage("enriching");
setEnrichProgress({ current: 0, total: selectedIds.length });
@@ -137,7 +137,7 @@ export default function LinkedInPage() {
addJob({ id: data.jobId, type: "linkedin-enrich", status: "running", progress: 0, total: selectedIds.length });
pollEnrich(data.jobId);
} catch (err) {
toast.error(err instanceof Error ? err.message : "Failed to start enrichment");
toast.error(err instanceof Error ? err.message : "Anreicherung konnte nicht gestartet werden");
setStage("scraped");
}
};
@@ -163,10 +163,10 @@ export default function LinkedInPage() {
if (data.status === "complete") {
setStage("done");
toast.success(`Found ${data.emailsFound} emails`);
toast.success(`${data.emailsFound} E-Mails gefunden`);
} else {
setStage("scraped");
toast.error("Enrichment failed");
toast.error("Anreicherung fehlgeschlagen");
}
}
} catch {
@@ -201,9 +201,9 @@ export default function LinkedInPage() {
<ChevronRight className="w-3 h-3" />
<span>LinkedIn Sales Navigator</span>
</div>
<h1 className="text-2xl font-bold text-white">LinkedIn Email Pipeline</h1>
<h1 className="text-2xl font-bold text-white">LinkedIn E-Mail Pipeline</h1>
<p className="text-gray-400 mt-1 text-sm">
Scrape Sales Navigator profiles via Vayne, then enrich with Anymailfinder.
Scrape Sales Navigator-Profile über Vayne und reichere sie mit Anymailfinder an.
</p>
</div>
</div>
@@ -216,7 +216,7 @@ export default function LinkedInPage() {
>
<div className="flex items-center gap-2 text-purple-300 font-medium">
<Info className="w-4 h-4" />
Recommended Sales Navigator Filter Settings
Empfohlene Sales Navigator Filtereinstellungen
</div>
{guideOpen ? <ChevronUp className="w-4 h-4 text-gray-500" /> : <ChevronDown className="w-4 h-4 text-gray-500" />}
</button>
@@ -224,7 +224,7 @@ export default function LinkedInPage() {
<div className="px-6 pb-5 border-t border-purple-500/10 space-y-4 text-sm">
<div className="grid grid-cols-2 gap-4 pt-4">
<div>
<p className="text-gray-400 font-medium mb-2">Keywords</p>
<p className="text-gray-400 font-medium mb-2">Schlüsselwörter</p>
<div className="flex flex-wrap gap-1.5">
{["Solarlösungen", "Founder", "Co-Founder", "CEO", "Geschäftsführer"].map(k => (
<span key={k} className="bg-purple-500/10 text-purple-300 px-2 py-0.5 rounded text-xs">{k}</span>
@@ -232,7 +232,7 @@ export default function LinkedInPage() {
</div>
</div>
<div>
<p className="text-gray-400 font-medium mb-2">Headcount</p>
<p className="text-gray-400 font-medium mb-2">Mitarbeiterzahl</p>
<div className="flex flex-wrap gap-1.5">
{["110", "1150", "51200"].map(k => (
<span key={k} className="bg-blue-500/10 text-blue-300 px-2 py-0.5 rounded text-xs">{k}</span>
@@ -240,11 +240,11 @@ export default function LinkedInPage() {
</div>
</div>
<div>
<p className="text-gray-400 font-medium mb-2">Country</p>
<span className="bg-green-500/10 text-green-300 px-2 py-0.5 rounded text-xs">Germany (Deutschland)</span>
<p className="text-gray-400 font-medium mb-2">Land</p>
<span className="bg-green-500/10 text-green-300 px-2 py-0.5 rounded text-xs">Deutschland</span>
</div>
<div>
<p className="text-gray-400 font-medium mb-2">Target Titles</p>
<p className="text-gray-400 font-medium mb-2">Zielpositionen</p>
<div className="flex flex-wrap gap-1.5">
{["Founder", "Co-Founder", "CEO", "CTO", "COO", "Owner", "President", "Principal", "Partner"].map(k => (
<span key={k} className="bg-purple-500/10 text-purple-300 px-2 py-0.5 rounded text-xs">{k}</span>
@@ -269,16 +269,16 @@ export default function LinkedInPage() {
: vayneConfigured ? "bg-green-500/5 border-green-500/20 text-green-400"
: "bg-red-500/5 border-red-500/20 text-red-400"
}`}>
{vayneConfigured === null ? "Checking Vayne configuration..."
{vayneConfigured === null ? "Vayne-Konfiguration wird geprüft..."
: vayneConfigured ? (
<><CheckCircle2 className="w-4 h-4" /> Vayne API token configured</>
<><CheckCircle2 className="w-4 h-4" /> Vayne API-Token konfiguriert</>
) : (
<><XCircle className="w-4 h-4" /> Vayne token not configured <a href="/settings" className="underline">go to Settings</a></>
<><XCircle className="w-4 h-4" /> Vayne-Token nicht konfiguriert <a href="/settings" className="underline">zu den Einstellungen</a></>
)}
</div>
<div>
<Label className="text-gray-300 text-sm mb-1.5 block">Sales Navigator Search URL</Label>
<Label className="text-gray-300 text-sm mb-1.5 block">Sales Navigator Such-URL</Label>
<Textarea
placeholder="https://www.linkedin.com/sales/search/people?query=..."
value={salesNavUrl}
@@ -286,12 +286,12 @@ export default function LinkedInPage() {
className="bg-[#0d0d18] border-[#2e2e3e] text-white placeholder:text-gray-600 focus:border-blue-500 resize-none h-20 font-mono text-xs"
/>
{salesNavUrl && !urlValid && (
<p className="text-xs text-red-400 mt-1">Must be a linkedin.com/sales/search URL</p>
<p className="text-xs text-red-400 mt-1">Muss eine linkedin.com/sales/search URL sein</p>
)}
</div>
<div>
<Label className="text-gray-300 text-sm mb-1.5 block">Max results to scrape</Label>
<Label className="text-gray-300 text-sm mb-1.5 block">Maximale Ergebnisse</Label>
<Input
type="number"
min={1}
@@ -306,8 +306,8 @@ export default function LinkedInPage() {
<div className="flex items-start gap-2 bg-yellow-500/5 border border-yellow-500/20 rounded-lg px-4 py-3">
<AlertTriangle className="w-4 h-4 text-yellow-400 flex-shrink-0 mt-0.5" />
<p className="text-xs text-yellow-300">
Do not close this tab while scraping is in progress. The job runs in the background
but progress tracking requires this tab to remain open.
Diesen Tab nicht schließen, solange das Scraping läuft. Der Job läuft im Hintergrund,
aber die Fortschrittsanzeige benötigt diesen Tab.
</p>
</div>
@@ -316,17 +316,17 @@ export default function LinkedInPage() {
disabled={!urlValid || !vayneConfigured || stage === "scraping"}
className="bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white font-medium px-8 shadow-lg hover:shadow-blue-500/25 transition-all disabled:opacity-50"
>
Start LinkedIn Scrape
LinkedIn-Scraping starten
</Button>
</Card>
{/* Scrape progress */}
{(stage === "scraping") && (
<ProgressCard
title="Scraping LinkedIn profiles via Vayne..."
title="LinkedIn-Profile werden über Vayne gescrapt..."
current={scrapeProgress.current}
total={scrapeProgress.total}
subtitle="Creating order → Scraping → Generating export..."
subtitle="Auftrag erstellen → Scraping → Export generieren..."
status="running"
/>
)}
@@ -336,9 +336,9 @@ export default function LinkedInPage() {
<Card className="bg-[#111118] border-[#1e1e2e] p-6 space-y-4">
<h2 className="text-base font-semibold text-white flex items-center gap-2">
<span className="w-6 h-6 rounded-full bg-blue-500/20 text-blue-400 text-xs flex items-center justify-center font-bold">2</span>
Scraped Profiles ({results.length})
Gescrapte Profile ({results.length})
</h2>
<p className="text-xs text-gray-500">Select profiles to include in email enrichment</p>
<p className="text-xs text-gray-500">Profile für die E-Mail-Anreicherung auswählen</p>
<ResultsTable
rows={results}
selectable
@@ -352,7 +352,7 @@ export default function LinkedInPage() {
<Card className="bg-[#111118] border-[#1e1e2e] p-6 space-y-5">
<h2 className="text-base font-semibold text-white flex items-center gap-2">
<span className="w-6 h-6 rounded-full bg-blue-500/20 text-blue-400 text-xs flex items-center justify-center font-bold">3</span>
Enrich with Emails
Mit E-Mails anreichern
</h2>
<div className="flex flex-wrap gap-2">
@@ -375,10 +375,10 @@ export default function LinkedInPage() {
{stage === "enriching" && (
<ProgressCard
title="Enriching profiles..."
title="Profile werden angereichert..."
current={enrichProgress.current}
total={enrichProgress.total}
subtitle="Finding emails via Anymailfinder"
subtitle="E-Mails werden über Anymailfinder gesucht"
status="running"
/>
)}
@@ -389,7 +389,7 @@ export default function LinkedInPage() {
disabled={!selectedIds.length || !categories.length}
className="bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white font-medium px-8 shadow-lg hover:shadow-blue-500/25 transition-all"
>
Enrich {selectedIds.length} Selected Profiles with Emails
{selectedIds.length} ausgewählte Profile mit E-Mails anreichern
</Button>
)}
</Card>
@@ -401,7 +401,7 @@ export default function LinkedInPage() {
<ExportButtons
rows={exportRows}
filename={`linkedin-leads-${scrapeJobId?.slice(0, 8) || "export"}`}
summary={`${emailsFound} emails found from ${results.length} profiles`}
summary={`${emailsFound} E-Mails aus ${results.length} Profilen gefunden`}
/>
</Card>
)}
@@ -410,8 +410,8 @@ export default function LinkedInPage() {
{stage === "idle" && (
<EmptyState
icon={Linkedin}
title="Start by pasting a Sales Navigator URL"
description="Configure your search filters in Sales Navigator, copy the URL, and paste it above to begin scraping."
title="Sales Navigator URL einfügen"
description="Suchfilter in Sales Navigator konfigurieren, URL kopieren und oben einfügen."
/>
)}
</div>