[dyad] Translated app to English - wrote 16 file(s)

This commit is contained in:
[dyad]
2026-01-20 10:40:36 +01:00
parent ecdfb8f273
commit d425ab1021
16 changed files with 101 additions and 107 deletions

View File

@@ -10,7 +10,7 @@ export default function ChangelogPage() {
<Button asChild variant="ghost" className="mb-4 -ml-4"> <Button asChild variant="ghost" className="mb-4 -ml-4">
<Link href="/"> <Link href="/">
<ArrowLeft className="mr-2 h-4 w-4" /> <ArrowLeft className="mr-2 h-4 w-4" />
Zurück zum Konverter Back to Converter
</Link> </Link>
</Button> </Button>
<main className="w-full"> <main className="w-full">

View File

@@ -16,8 +16,8 @@ const geistMono = Geist_Mono({
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Webify | Bilder schnell konvertieren & optimieren", title: "Webify | Quickly Convert & Optimize Images",
description: "Konvertieren Sie Ihre Bilder mühelos in Web-Formate wie WEBP, JPEG oder PNG. Passen Sie Auflösung, Qualität und Dateinamen an alles direkt in Ihrem Browser.", description: "Effortlessly convert your images to web formats like WEBP, JPEG, or PNG. Adjust resolution, quality, and filenames—all directly in your browser.",
robots: { robots: {
index: false, index: false,
follow: false, follow: false,
@@ -30,7 +30,7 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="de" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className={`${geistSans.variable} ${geistMono.variable} antialiased`}
> >

View File

@@ -9,7 +9,7 @@ export default function Home() {
Webify Webify
</h1> </h1>
<p className="mt-3 text-lg text-gray-600 dark:text-gray-400"> <p className="mt-3 text-lg text-gray-600 dark:text-gray-400">
Laden Sie ein Bild hoch und exportieren Sie es in einer anderen Auflösung und einem anderen Format. Upload an image and export it in a different resolution and format.
</p> </p>
</div> </div>
<ImageConverter /> <ImageConverter />

View File

@@ -11,7 +11,7 @@ interface ActionButtonsProps {
export function ActionButtons({ onReset }: ActionButtonsProps) { export function ActionButtons({ onReset }: ActionButtonsProps) {
const handleApply = () => { const handleApply = () => {
toast.info("Einstellungen aktualisiert und werden für alle Downloads verwendet."); toast.info("Settings updated and will be used for all downloads.");
}; };
return ( return (
@@ -20,18 +20,18 @@ export function ActionButtons({ onReset }: ActionButtonsProps) {
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button onClick={onReset} className="w-full" variant="outline"> <Button onClick={onReset} className="w-full" variant="outline">
<RotateCcw className="mr-2 h-4 w-4" /> Zurücksetzen <RotateCcw className="mr-2 h-4 w-4" /> Reset
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent><p>Alle Einstellungen auf ihre Standardwerte zurücksetzen.</p></TooltipContent> <TooltipContent><p>Reset all settings to their default values.</p></TooltipContent>
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button onClick={handleApply} className="w-full"> <Button onClick={handleApply} className="w-full">
<Check className="mr-2 h-4 w-4" /> Anwenden <Check className="mr-2 h-4 w-4" /> Apply
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent><p>Bestätigen und alle oben genannten Einstellungen anwenden. Dies lädt die Bilder nicht herunter.</p></TooltipContent> <TooltipContent><p>Confirm and apply all settings above. This does not download the images.</p></TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
</TooltipProvider> </TooltipProvider>

View File

@@ -6,20 +6,14 @@ import { cn } from "@/lib/utils";
import { changelogData } from "@/lib/changelog-data"; import { changelogData } from "@/lib/changelog-data";
export function Changelog() { export function Changelog() {
const typeTranslations: { [key: string]: string } = {
"New": "Neu",
"Improved": "Verbessert",
"Fixed": "Behoben",
};
return ( return (
<div className="py-12"> <div className="py-12">
<div className="text-center mb-12"> <div className="text-center mb-12">
<h1 className="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100 sm:text-5xl"> <h1 className="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100 sm:text-5xl">
Änderungsprotokoll Changelog
</h1> </h1>
<p className="mt-3 text-lg text-gray-600 dark:text-gray-400"> <p className="mt-3 text-lg text-gray-600 dark:text-gray-400">
Verfolgung aller neuen Funktionen, Verbesserungen und Fehlerbehebungen. Tracking all new features, improvements, and bug fixes.
</p> </p>
</div> </div>
<div className="space-y-8"> <div className="space-y-8">
@@ -38,12 +32,12 @@ export function Changelog() {
<Badge <Badge
variant="outline" variant="outline"
className={cn("shrink-0 font-semibold", { className={cn("shrink-0 font-semibold", {
"border-blue-500/50 bg-blue-500/10 text-blue-700 dark:text-blue-300": change.type === "Verbessert", "border-blue-500/50 bg-blue-500/10 text-blue-700 dark:text-blue-300": change.type === "Improved",
"border-green-500/50 bg-green-500/10 text-green-700 dark:text-green-300": change.type === "Neu", "border-green-500/50 bg-green-500/10 text-green-700 dark:text-green-300": change.type === "New",
"border-red-500/50 bg-red-500/10 text-red-700 dark:text-red-300": change.type === "Behoben", "border-red-500/50 bg-red-500/10 text-red-700 dark:text-red-300": change.type === "Fixed",
})} })}
> >
{typeTranslations[change.type] || change.type} {change.type}
</Badge> </Badge>
<p className="text-foreground">{change.text}</p> <p className="text-foreground">{change.text}</p>
</li> </li>

View File

@@ -34,7 +34,7 @@ export function ImageListItem({
<div className="p-4 border rounded-lg flex items-center gap-4"> <div className="p-4 border rounded-lg flex items-center gap-4">
<img src={image.previewUrl} alt={`Preview ${index + 1}`} className="w-20 h-20 object-cover rounded-md shrink-0" /> <img src={image.previewUrl} alt={`Preview ${index + 1}`} className="w-20 h-20 object-cover rounded-md shrink-0" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<Label htmlFor={`filename-${index}`} className="text-xs text-muted-foreground">Basisname</Label> <Label htmlFor={`filename-${index}`} className="text-xs text-muted-foreground">Basename</Label>
<Input <Input
id={`filename-${index}`} id={`filename-${index}`}
value={image.filename} value={image.filename}
@@ -42,7 +42,7 @@ export function ImageListItem({
className="text-sm font-medium h-8 mt-1" className="text-sm font-medium h-8 mt-1"
/> />
<p className="text-xs text-muted-foreground truncate mt-1" title={`${finalFilename}.${settings.format}`}> <p className="text-xs text-muted-foreground truncate mt-1" title={`${finalFilename}.${settings.format}`}>
Finaler Name: {finalFilename}.{settings.format} Final name: {finalFilename}.{settings.format}
</p> </p>
</div> </div>
<div className="flex items-center shrink-0"> <div className="flex items-center shrink-0">
@@ -58,7 +58,7 @@ export function ImageListItem({
<Download className="h-4 w-4" /> <Download className="h-4 w-4" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent><p>Dieses Bild herunterladen</p></TooltipContent> <TooltipContent><p>Download this image</p></TooltipContent>
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@@ -72,7 +72,7 @@ export function ImageListItem({
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent><p>Dieses Bild entfernen</p></TooltipContent> <TooltipContent><p>Remove this image</p></TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
</div> </div>

View File

@@ -41,24 +41,24 @@ export function ImageList({
<Card> <Card>
<CardHeader> <CardHeader>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<CardTitle>Hochgeladene Bilder</CardTitle> <CardTitle>Uploaded Images</CardTitle>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button variant="ghost" size="sm" onClick={onClearAll} disabled={isProcessing}> <Button variant="ghost" size="sm" onClick={onClearAll} disabled={isProcessing}>
<Trash2 className="mr-2 h-4 w-4" />Alle löschen <Trash2 className="mr-2 h-4 w-4" />Clear All
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent><p>Alle hochgeladenen Bilder entfernen.</p></TooltipContent> <TooltipContent><p>Remove all uploaded images.</p></TooltipContent>
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button onClick={onDownloadAll} disabled={isProcessing}> <Button onClick={onDownloadAll} disabled={isProcessing}>
<Download className="mr-2 h-4 w-4" /> <Download className="mr-2 h-4 w-4" />
{isConverting ? "Konvertiere..." : `Alle herunterladen (${images.length})`} {isConverting ? "Converting..." : `Download All (${images.length})`}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent><p>Alle Bilder mit den aktuellen Einstellungen konvertieren und herunterladen.</p></TooltipContent> <TooltipContent><p>Convert and download all images with the current settings.</p></TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
</div> </div>

View File

@@ -39,7 +39,7 @@ export function ImageUploadArea({ onFilesSelected }: ImageUploadAreaProps) {
<Card> <Card>
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-lg font-medium">Bilder hochladen</h3> <h3 className="text-lg font-medium">Upload Images</h3>
<div <div
className={cn( className={cn(
"w-full h-48 rounded-lg border-2 border-dashed flex items-center justify-center relative transition-colors cursor-pointer hover:border-primary/60", "w-full h-48 rounded-lg border-2 border-dashed flex items-center justify-center relative transition-colors cursor-pointer hover:border-primary/60",
@@ -52,8 +52,8 @@ export function ImageUploadArea({ onFilesSelected }: ImageUploadAreaProps) {
> >
<div className="flex flex-col items-center justify-center text-center text-muted-foreground"> <div className="flex flex-col items-center justify-center text-center text-muted-foreground">
<Upload className="w-8 h-8 mb-2" /> <Upload className="w-8 h-8 mb-2" />
<p className="font-semibold">Klicken oder per Drag & Drop hochladen</p> <p className="font-semibold">Click or drag & drop to upload</p>
<p className="text-xs text-muted-foreground mt-1">PNG, JPG, WEBP werden unterstützt</p> <p className="text-xs text-muted-foreground mt-1">PNG, JPG, WEBP are supported</p>
</div> </div>
<Input type="file" ref={fileInputRef} onChange={handleImageChange} className="hidden" accept="image/*" multiple /> <Input type="file" ref={fileInputRef} onChange={handleImageChange} className="hidden" accept="image/*" multiple />
</div> </div>

View File

@@ -31,8 +31,8 @@ export function SettingsPanel({
<AccordionItem value="image-settings" className="border rounded-lg bg-card"> <AccordionItem value="image-settings" className="border rounded-lg bg-card">
<AccordionTrigger className="p-6 hover:no-underline"> <AccordionTrigger className="p-6 hover:no-underline">
<div className="text-left"> <div className="text-left">
<h3 className="text-lg font-medium leading-none">Bildeinstellungen</h3> <h3 className="text-lg font-medium leading-none">Image Settings</h3>
<p className="text-sm text-muted-foreground mt-1">Auflösung und Skalierung r alle Bilder anpassen.</p> <p className="text-sm text-muted-foreground mt-1">Adjust resolution and scaling for all images.</p>
</div> </div>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="px-6 pb-6"> <AccordionContent className="px-6 pb-6">
@@ -48,8 +48,8 @@ export function SettingsPanel({
<AccordionItem value="filename-settings" className="border rounded-lg bg-card"> <AccordionItem value="filename-settings" className="border rounded-lg bg-card">
<AccordionTrigger className="p-6 hover:no-underline"> <AccordionTrigger className="p-6 hover:no-underline">
<div className="text-left"> <div className="text-left">
<h3 className="text-lg font-medium leading-none">Dateinameneinstellungen</h3> <h3 className="text-lg font-medium leading-none">Filename Settings</h3>
<p className="text-sm text-muted-foreground mt-1">Die Ausgabe-Dateinamen anpassen.</p> <p className="text-sm text-muted-foreground mt-1">Customize the output filenames.</p>
</div> </div>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="px-6 pb-6"> <AccordionContent className="px-6 pb-6">
@@ -65,8 +65,8 @@ export function SettingsPanel({
<AccordionItem value="quality-settings" className="border rounded-lg bg-card"> <AccordionItem value="quality-settings" className="border rounded-lg bg-card">
<AccordionTrigger className="p-6 hover:no-underline"> <AccordionTrigger className="p-6 hover:no-underline">
<div className="text-left"> <div className="text-left">
<h3 className="text-lg font-medium leading-none">Qualitätseinstellungen</h3> <h3 className="text-lg font-medium leading-none">Quality Settings</h3>
<p className="text-sm text-muted-foreground mt-1">Format und Komprimierungsstufe auswählen.</p> <p className="text-sm text-muted-foreground mt-1">Select format and compression level.</p>
</div> </div>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="px-6 pb-6"> <AccordionContent className="px-6 pb-6">

View File

@@ -26,61 +26,61 @@ export function FilenameSettings({
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Switch id="use-default-base-name" checked={settings.useDefaultBaseName} onCheckedChange={(checked) => onSettingsChange({ useDefaultBaseName: checked })} /> <Switch id="use-default-base-name" checked={settings.useDefaultBaseName} onCheckedChange={(checked) => onSettingsChange({ useDefaultBaseName: checked })} />
<Label htmlFor="use-default-base-name" className="flex items-center gap-1.5 cursor-pointer"> <Label htmlFor="use-default-base-name" className="flex items-center gap-1.5 cursor-pointer">
Standard-Basisnamen verwenden Use default basename
<Tooltip> <Tooltip>
<TooltipTrigger onClick={(e) => e.preventDefault()}><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger> <TooltipTrigger onClick={(e) => e.preventDefault()}><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
<TooltipContent><p>Wenn aktiviert, verwenden alle neu hochgeladenen Bilder den angegebenen Standard-Basisnamen.</p></TooltipContent> <TooltipContent><p>When enabled, all newly uploaded images will use the specified default basename.</p></TooltipContent>
</Tooltip> </Tooltip>
</Label> </Label>
</div> </div>
{settings.useDefaultBaseName && ( {settings.useDefaultBaseName && (
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="default-base-name">Standard-Basisname</Label> <Label htmlFor="default-base-name">Default Basename</Label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Input <Input
id="default-base-name" id="default-base-name"
placeholder="z.B. new-york-reise" placeholder="e.g. new-york-trip"
value={settings.defaultBaseName} value={settings.defaultBaseName}
onChange={(e) => onSettingsChange({ defaultBaseName: e.target.value })} onChange={(e) => onSettingsChange({ defaultBaseName: e.target.value })}
/> />
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button variant="outline" size="sm" onClick={onApplyDefaultBaseNameToAll} disabled={!settings.defaultBaseName || !hasImages}> <Button variant="outline" size="sm" onClick={onApplyDefaultBaseNameToAll} disabled={!settings.defaultBaseName || !hasImages}>
Auf alle anwenden Apply to all
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent><p>Diesen Basisnamen auf alle aktuell hochgeladenen Bilder anwenden.</p></TooltipContent> <TooltipContent><p>Apply this basename to all currently uploaded images.</p></TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
)} )}
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Label htmlFor="prefix">Präfix</Label> <Label htmlFor="prefix">Prefix</Label>
<Tooltip> <Tooltip>
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger> <TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
<TooltipContent><p>Text am Anfang jedes Dateinamens hinzufügen.</p></TooltipContent> <TooltipContent><p>Add text to the beginning of each filename.</p></TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
<Input id="prefix" placeholder="z.B. reise-" value={settings.prefix} onChange={(e) => onSettingsChange({ prefix: e.target.value })} /> <Input id="prefix" placeholder="e.g. trip-" value={settings.prefix} onChange={(e) => onSettingsChange({ prefix: e.target.value })} />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Label htmlFor="suffix">Suffix</Label> <Label htmlFor="suffix">Suffix</Label>
<Tooltip> <Tooltip>
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger> <TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
<TooltipContent><p>Text am Ende jedes Dateinamens (vor der Nummer) hinzufügen.</p></TooltipContent> <TooltipContent><p>Add text to the end of each filename (before the number).</p></TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
<Input id="suffix" placeholder="z.B. -bearbeitet" value={settings.suffix} onChange={(e) => onSettingsChange({ suffix: e.target.value })} /> <Input id="suffix" placeholder="e.g. -edited" value={settings.suffix} onChange={(e) => onSettingsChange({ suffix: e.target.value })} />
</div> </div>
<div className="flex items-center space-x-2 pt-2"> <div className="flex items-center space-x-2 pt-2">
<Switch id="use-counter" checked={settings.useCounter} onCheckedChange={(checked) => onSettingsChange({ useCounter: checked })} /> <Switch id="use-counter" checked={settings.useCounter} onCheckedChange={(checked) => onSettingsChange({ useCounter: checked })} />
<Label htmlFor="use-counter" className="flex items-center gap-1.5 cursor-pointer"> <Label htmlFor="use-counter" className="flex items-center gap-1.5 cursor-pointer">
Fortlaufende Nummer hinzufügen Add sequential number
<Tooltip> <Tooltip>
<TooltipTrigger onClick={(e) => e.preventDefault()}><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger> <TooltipTrigger onClick={(e) => e.preventDefault()}><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
<TooltipContent><p>Eine nummerierte Sequenz an jeden Dateinamen anhängen.</p></TooltipContent> <TooltipContent><p>Append a numbered sequence to each filename.</p></TooltipContent>
</Tooltip> </Tooltip>
</Label> </Label>
</div> </div>
@@ -88,10 +88,10 @@ export function FilenameSettings({
<div className="grid grid-cols-2 gap-4 pt-2"> <div className="grid grid-cols-2 gap-4 pt-2">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Label htmlFor="counter-start">Startnummer</Label> <Label htmlFor="counter-start">Start number</Label>
<Tooltip> <Tooltip>
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger> <TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
<TooltipContent><p>Die erste Nummer, die in der Sequenz verwendet wird.</p></TooltipContent> <TooltipContent><p>The first number to be used in the sequence.</p></TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
<Input <Input
@@ -104,10 +104,10 @@ export function FilenameSettings({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Label htmlFor="counter-digits">Auffüllende Ziffern</Label> <Label htmlFor="counter-digits">Padding digits</Label>
<Tooltip> <Tooltip>
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger> <TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
<TooltipContent><p>Gesamtzahl der Ziffern für den Zähler, mit führenden Nullen aufgefüllt (z.B. 3 r 001).</p></TooltipContent> <TooltipContent><p>Total number of digits for the counter, padded with leading zeros (e.g., 3 for 001).</p></TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
<Input <Input

View File

@@ -11,11 +11,11 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
import { ObjectPositionControl } from "@/components/object-position-control"; import { ObjectPositionControl } from "@/components/object-position-control";
const aspectRatios = [ const aspectRatios = [
{ name: "Benutzerdefiniert", value: "custom" }, { name: "Custom", value: "custom" },
{ name: "1:1 (Quadratisch)", value: "1/1" }, { name: "1:1 (Square)", value: "1/1" },
{ name: "4:3 (Standard)", value: "4/3" }, { name: "4:3 (Standard)", value: "4/3" },
{ name: "3:2 (Fotografie)", value: "3/2" }, { name: "3:2 (Photography)", value: "3/2" },
{ name: "16:9 (Breitbild)", value: "16/9" }, { name: "16:9 (Widescreen)", value: "16/9" },
]; ];
interface ImageSettingsProps { interface ImageSettingsProps {
@@ -35,14 +35,14 @@ export function ImageSettings({
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Label htmlFor="aspect-ratio">Seitenverhältnis</Label> <Label htmlFor="aspect-ratio">Aspect Ratio</Label>
<Tooltip> <Tooltip>
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger> <TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
<TooltipContent><p>Wählen Sie ein voreingestelltes Seitenverhältnis oder 'Benutzerdefiniert', um die Abmessungen manuell einzugeben.</p></TooltipContent> <TooltipContent><p>Select a preset aspect ratio or 'Custom' to enter dimensions manually.</p></TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
<Select value={settings.aspectRatio} onValueChange={onAspectRatioChange}> <Select value={settings.aspectRatio} onValueChange={onAspectRatioChange}>
<SelectTrigger id="aspect-ratio" className="mt-2"><SelectValue placeholder="Seitenverhältnis auswählen" /></SelectTrigger> <SelectTrigger id="aspect-ratio" className="mt-2"><SelectValue placeholder="Select aspect ratio" /></SelectTrigger>
<SelectContent> <SelectContent>
{aspectRatios.map((ratio) => ( {aspectRatios.map((ratio) => (
<SelectItem key={ratio.value} value={ratio.value}>{ratio.name}</SelectItem> <SelectItem key={ratio.value} value={ratio.value}>{ratio.name}</SelectItem>
@@ -53,28 +53,28 @@ export function ImageSettings({
<div className="flex items-end gap-2"> <div className="flex items-end gap-2">
<div className="space-y-2 flex-1"> <div className="space-y-2 flex-1">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Label htmlFor="width">Breite (px)</Label> <Label htmlFor="width">Width (px)</Label>
<Tooltip> <Tooltip>
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger> <TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
<TooltipContent><p>Legen Sie die Ausgabebreite in Pixeln fest. Leer lassen, um die Originalbreite zu verwenden.</p></TooltipContent> <TooltipContent><p>Set the output width in pixels. Leave empty to use the original width.</p></TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
<Input id="width" type="number" placeholder="Auto" value={settings.width} onChange={(e) => { onSettingsChange({ width: e.target.value, aspectRatio: 'custom' }) }} /> <Input id="width" type="number" placeholder="Auto" value={settings.width} onChange={(e) => { onSettingsChange({ width: e.target.value, aspectRatio: 'custom' }) }} />
</div> </div>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button variant="outline" size="icon" onClick={onSwapDimensions} className="shrink-0" aria-label="Breite und Höhe tauschen"> <Button variant="outline" size="icon" onClick={onSwapDimensions} className="shrink-0" aria-label="Swap width and height">
<ArrowRightLeft className="h-4 w-4" /> <ArrowRightLeft className="h-4 w-4" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent><p>Die eingegebenen Werte für Breite und Höhe tauschen.</p></TooltipContent> <TooltipContent><p>Swap the entered values for width and height.</p></TooltipContent>
</Tooltip> </Tooltip>
<div className="space-y-2 flex-1"> <div className="space-y-2 flex-1">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Label htmlFor="height">Höhe (px)</Label> <Label htmlFor="height">Height (px)</Label>
<Tooltip> <Tooltip>
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger> <TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
<TooltipContent><p>Legen Sie die Ausgabehöhe in Pixeln fest. Leer lassen, um die Originalhöhe zu verwenden.</p></TooltipContent> <TooltipContent><p>Set the output height in pixels. Leave empty to use the original height.</p></TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
<Input id="height" type="number" placeholder="Auto" value={settings.height} onChange={(e) => { onSettingsChange({ height: e.target.value, aspectRatio: 'custom' }) }} /> <Input id="height" type="number" placeholder="Auto" value={settings.height} onChange={(e) => { onSettingsChange({ height: e.target.value, aspectRatio: 'custom' }) }} />
@@ -83,27 +83,27 @@ export function ImageSettings({
<div className="flex items-center space-x-2 pt-2"> <div className="flex items-center space-x-2 pt-2">
<Checkbox id="keep-orientation" checked={settings.keepOrientation} onCheckedChange={(checked) => onSettingsChange({ keepOrientation: Boolean(checked) })} /> <Checkbox id="keep-orientation" checked={settings.keepOrientation} onCheckedChange={(checked) => onSettingsChange({ keepOrientation: Boolean(checked) })} />
<Label htmlFor="keep-orientation" className="cursor-pointer flex items-center gap-1.5"> <Label htmlFor="keep-orientation" className="cursor-pointer flex items-center gap-1.5">
Originalausrichtung beibehalten Keep original orientation
<Tooltip> <Tooltip>
<TooltipTrigger onClick={(e) => e.preventDefault()}><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger> <TooltipTrigger onClick={(e) => e.preventDefault()}><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
<TooltipContent><p>Tauscht automatisch Breite und Höhe, um der Ausrichtung des Originalbildes zu entsprechen.</p></TooltipContent> <TooltipContent><p>Automatically swaps width and height to match the original image's orientation.</p></TooltipContent>
</Tooltip> </Tooltip>
</Label> </Label>
</div> </div>
<div className="mt-4 space-y-2"> <div className="mt-4 space-y-2">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Label htmlFor="scale-mode">Skalierung</Label> <Label htmlFor="scale-mode">Scaling</Label>
<Tooltip> <Tooltip>
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger> <TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
<TooltipContent><p>Bestimmt, wie das Bild in die neuen Abmessungen passt.</p></TooltipContent> <TooltipContent><p>Determines how the image fits into the new dimensions.</p></TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
<Select value={settings.scaleMode} onValueChange={(value) => onSettingsChange({ scaleMode: value as any })}> <Select value={settings.scaleMode} onValueChange={(value) => onSettingsChange({ scaleMode: value as any })}>
<SelectTrigger id="scale-mode"><SelectValue placeholder="Skalierungsmodus auswählen" /></SelectTrigger> <SelectTrigger id="scale-mode"><SelectValue placeholder="Select scale mode" /></SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="fill">Füllen (strecken)</SelectItem> <SelectItem value="fill">Fill (stretch)</SelectItem>
<SelectItem value="cover">Abdecken (zuschneiden)</SelectItem> <SelectItem value="cover">Cover (crop)</SelectItem>
<SelectItem value="contain">Enthalten (Letterbox)</SelectItem> <SelectItem value="contain">Contain (letterbox)</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@@ -113,7 +113,7 @@ export function ImageSettings({
<Label>Position</Label> <Label>Position</Label>
<Tooltip> <Tooltip>
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger> <TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
<TooltipContent><p>Legt den Ankerpunkt für die Skalierung 'Abdecken' oder 'Enthalten' fest.</p></TooltipContent> <TooltipContent><p>Sets the anchor point for 'Cover' or 'Contain' scaling.</p></TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
<ObjectPositionControl value={settings.objectPosition} onChange={(pos) => onSettingsChange({ objectPosition: pos as ObjectPosition })} /> <ObjectPositionControl value={settings.objectPosition} onChange={(pos) => onSettingsChange({ objectPosition: pos as ObjectPosition })} />

View File

@@ -39,7 +39,7 @@ export function PresetSettings({ onSettingsChange }: PresetSettingsProps) {
const selectedPreset = presets.find(p => p.name === presetName); const selectedPreset = presets.find(p => p.name === presetName);
if (selectedPreset) { if (selectedPreset) {
onSettingsChange(selectedPreset.settings); onSettingsChange(selectedPreset.settings);
toast.success(`Preset "${selectedPreset.name}" angewendet.`); toast.success(`Preset "${selectedPreset.name}" applied.`);
} }
}; };
@@ -49,7 +49,7 @@ export function PresetSettings({ onSettingsChange }: PresetSettingsProps) {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<CardTitle className="text-lg font-medium leading-none">Presets</CardTitle> <CardTitle className="text-lg font-medium leading-none">Presets</CardTitle>
<p className="text-sm text-muted-foreground mt-1">Schnell gängige Einstellungen anwenden.</p> <p className="text-sm text-muted-foreground mt-1">Quickly apply common settings.</p>
</div> </div>
<Switch id="enable-presets" checked={presetsEnabled} onCheckedChange={setPresetsEnabled} /> <Switch id="enable-presets" checked={presetsEnabled} onCheckedChange={setPresetsEnabled} />
</div> </div>
@@ -57,10 +57,10 @@ export function PresetSettings({ onSettingsChange }: PresetSettingsProps) {
{presetsEnabled && ( {presetsEnabled && (
<CardContent> <CardContent>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="presets">Preset auswählen</Label> <Label htmlFor="presets">Select Preset</Label>
<Select onValueChange={handlePresetSelect}> <Select onValueChange={handlePresetSelect}>
<SelectTrigger id="presets"> <SelectTrigger id="presets">
<SelectValue placeholder="Preset auswählen..." /> <SelectValue placeholder="Select a preset..." />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{presets.map((preset) => ( {presets.map((preset) => (

View File

@@ -20,11 +20,11 @@ export function QualitySettings({ settings, onSettingsChange }: QualitySettingsP
<Label htmlFor="format">Format</Label> <Label htmlFor="format">Format</Label>
<Tooltip> <Tooltip>
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger> <TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
<TooltipContent><p>Wählen Sie das Ausgabedateiformat r die Bilder.</p></TooltipContent> <TooltipContent><p>Select the output file format for the images.</p></TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
<Select value={settings.format} onValueChange={(value: ImageFormat) => onSettingsChange({ format: value })}> <Select value={settings.format} onValueChange={(value: ImageFormat) => onSettingsChange({ format: value })}>
<SelectTrigger id="format"><SelectValue placeholder="Format auswählen" /></SelectTrigger> <SelectTrigger id="format"><SelectValue placeholder="Select format" /></SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="png">PNG</SelectItem> <SelectItem value="png">PNG</SelectItem>
<SelectItem value="jpeg">JPEG</SelectItem> <SelectItem value="jpeg">JPEG</SelectItem>
@@ -35,10 +35,10 @@ export function QualitySettings({ settings, onSettingsChange }: QualitySettingsP
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Label htmlFor="quality">Qualität</Label> <Label htmlFor="quality">Quality</Label>
<Tooltip> <Tooltip>
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger> <TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
<TooltipContent><p>Stellen Sie die Komprimierungsqualität r JPEG/WEBP ein. Höher bedeutet bessere Qualität, aber größere Dateigröße.</p></TooltipContent> <TooltipContent><p>Set the compression quality for JPEG/WEBP. Higher means better quality but larger file size.</p></TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
<span className="text-sm text-muted-foreground">{settings.quality}%</span> <span className="text-sm text-muted-foreground">{settings.quality}%</span>
@@ -53,7 +53,7 @@ export function QualitySettings({ settings, onSettingsChange }: QualitySettingsP
disabled={settings.format === 'png'} disabled={settings.format === 'png'}
/> />
{settings.format === 'png' && ( {settings.format === 'png' && (
<p className="text-xs text-muted-foreground pt-1">Der Qualitätsregler ist r PNG (verlustfreies Format) deaktiviert.</p> <p className="text-xs text-muted-foreground pt-1">The quality slider is disabled for PNG (lossless format).</p>
)} )}
</div> </div>
</div> </div>

View File

@@ -29,10 +29,10 @@ export function ThemeToggle() {
System System
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("light")}> <DropdownMenuItem onClick={() => setTheme("light")}>
Hell Light
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}> <DropdownMenuItem onClick={() => setTheme("dark")}>
Dunkel Dark
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>

View File

@@ -43,7 +43,7 @@ export function useImageConverter() {
const imageFiles = Array.from(files).filter(file => file.type.startsWith("image/")); const imageFiles = Array.from(files).filter(file => file.type.startsWith("image/"));
if (imageFiles.length === 0) { if (imageFiles.length === 0) {
toast.error("Keine gültigen Bilddateien gefunden."); toast.error("No valid image files found.");
return; return;
} }
@@ -56,7 +56,7 @@ export function useImageConverter() {
})); }));
setImages(prev => [...prev, ...newImageFiles]); setImages(prev => [...prev, ...newImageFiles]);
toast.success(`${imageFiles.length} Bild(er) hinzugefügt.`); toast.success(`${imageFiles.length} image(s) added.`);
}, [settings.useDefaultBaseName, settings.defaultBaseName]); }, [settings.useDefaultBaseName, settings.defaultBaseName]);
const handleRemoveImage = useCallback((indexToRemove: number) => { const handleRemoveImage = useCallback((indexToRemove: number) => {
@@ -72,7 +72,7 @@ export function useImageConverter() {
const handleClearAll = useCallback(() => { const handleClearAll = useCallback(() => {
setImages([]); setImages([]);
updateSettings({ width: initialSettings.width, height: initialSettings.height }); updateSettings({ width: initialSettings.width, height: initialSettings.height });
toast.info("Alle Bilder gelöscht."); toast.info("All images cleared.");
}, [updateSettings]); }, [updateSettings]);
const handleFilenameChange = useCallback((index: number, newName: string) => { const handleFilenameChange = useCallback((index: number, newName: string) => {
@@ -87,15 +87,15 @@ export function useImageConverter() {
const handleConvertAndDownloadSingle = useCallback(async (index: number) => { const handleConvertAndDownloadSingle = useCallback(async (index: number) => {
setConvertingIndex(index); setConvertingIndex(index);
toast.info(`Starte Konvertierung für ${images[index].filename}...`); toast.info(`Starting conversion for ${images[index].filename}...`);
try { try {
const imageToConvert = images[index]; const imageToConvert = images[index];
const dataUrl = await processImage(imageToConvert, settings); const dataUrl = await processImage(imageToConvert, settings);
const finalFilename = generateFinalFilename(imageToConvert.filename, settings, index); const finalFilename = generateFinalFilename(imageToConvert.filename, settings, index);
downloadDataUrl(dataUrl, `${finalFilename}.${settings.format}`); downloadDataUrl(dataUrl, `${finalFilename}.${settings.format}`);
toast.success(`${imageToConvert.filename} erfolgreich exportiert!`); toast.success(`${imageToConvert.filename} exported successfully!`);
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Ein unbekannter Fehler ist aufgetreten."; const message = error instanceof Error ? error.message : "An unknown error occurred.";
toast.error(message); toast.error(message);
} finally { } finally {
setConvertingIndex(null); setConvertingIndex(null);
@@ -104,11 +104,11 @@ export function useImageConverter() {
const handleConvertAndDownloadAll = useCallback(async () => { const handleConvertAndDownloadAll = useCallback(async () => {
if (images.length === 0) { if (images.length === 0) {
toast.error("Bitte laden Sie zuerst Bilder hoch."); toast.error("Please upload images first.");
return; return;
} }
setIsConverting(true); setIsConverting(true);
toast.info(`Starte Konvertierung für ${images.length} Bilder...`); toast.info(`Starting conversion for ${images.length} images...`);
const conversionPromises = images.map(async (image, index) => { const conversionPromises = images.map(async (image, index) => {
try { try {
@@ -116,7 +116,7 @@ export function useImageConverter() {
const finalFilename = generateFinalFilename(image.filename, settings, index); const finalFilename = generateFinalFilename(image.filename, settings, index);
downloadDataUrl(dataUrl, `${finalFilename}.${settings.format}`); downloadDataUrl(dataUrl, `${finalFilename}.${settings.format}`);
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : `Verarbeitung von ${image.filename} fehlgeschlagen`; const message = error instanceof Error ? error.message : `Processing of ${image.filename} failed`;
toast.error(message); toast.error(message);
throw error; throw error;
} }
@@ -124,9 +124,9 @@ export function useImageConverter() {
try { try {
await Promise.all(conversionPromises); await Promise.all(conversionPromises);
toast.success(`Alle ${images.length} Bilder erfolgreich exportiert!`); toast.success(`All ${images.length} images exported successfully!`);
} catch (error) { } catch (error) {
toast.error("Einige Bilder konnten nicht konvertiert werden. Siehe einzelne Fehler."); toast.error("Some images could not be converted. See individual errors.");
} finally { } finally {
setIsConverting(false); setIsConverting(false);
} }
@@ -134,7 +134,7 @@ export function useImageConverter() {
const handleResetSettings = useCallback(() => { const handleResetSettings = useCallback(() => {
setSettings(initialSettings); setSettings(initialSettings);
toast.success("Alle Einstellungen wurden auf ihre Standardwerte zurückgesetzt."); toast.success("All settings have been reset to their default values.");
}, []); }, []);
const handleAspectRatioChange = useCallback((value: string) => { const handleAspectRatioChange = useCallback((value: string) => {
@@ -159,15 +159,15 @@ export function useImageConverter() {
const handleApplyDefaultBaseNameToAll = useCallback(() => { const handleApplyDefaultBaseNameToAll = useCallback(() => {
if (!settings.defaultBaseName) { if (!settings.defaultBaseName) {
toast.error("Bitte geben Sie einen Standard-Basisnamen zum Anwenden ein."); toast.error("Please enter a default basename to apply.");
return; return;
} }
if (images.length === 0) { if (images.length === 0) {
toast.info("Laden Sie zuerst einige Bilder hoch."); toast.info("Upload some images first.");
return; return;
} }
setImages(prev => prev.map(img => ({ ...img, filename: settings.defaultBaseName }))); setImages(prev => prev.map(img => ({ ...img, filename: settings.defaultBaseName })));
toast.success(`Basisname für alle ${images.length} Bilder auf "${settings.defaultBaseName}" gesetzt.`); toast.success(`Basename for all ${images.length} images set to "${settings.defaultBaseName}".`);
}, [images.length, settings.defaultBaseName]); }, [images.length, settings.defaultBaseName]);
return { return {

View File

@@ -1,9 +1,9 @@
export const changelogData = [ export const changelogData = [
{ {
version: "1.0.0", version: "1.0.0",
date: "15. Januar 2026", date: "January 15, 2026",
changes: [ changes: [
{ type: "Neu", text: "Erstveröffentlichung des Bild Web Exporters." }, { type: "New", text: "Initial release of the Image Web Exporter." },
], ],
}, },
]; ];