[dyad] Applying translations to the entire app - wrote 8 file(s)

This commit is contained in:
[dyad]
2026-01-18 15:52:46 +01:00
parent cad0921161
commit 2918d92a95
8 changed files with 358 additions and 100 deletions

View File

@@ -4,16 +4,19 @@ import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { changelogData } from "@/lib/changelog-data";
import { useTranslation } from "@/context/i18n-context";
export function Changelog() {
const { t } = useTranslation();
return (
<div className="w-full max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100 sm:text-5xl">
Changelog
{t('changelogPage.title')}
</h1>
<p className="mt-3 text-lg text-gray-600 dark:text-gray-400">
Tracking all the new features, improvements, and bug fixes.
{t('changelogPage.description')}
</p>
</div>
<div className="space-y-8">
@@ -21,7 +24,7 @@ export function Changelog() {
<Card key={entry.version}>
<CardHeader>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<CardTitle className="text-2xl font-bold">Version {entry.version}</CardTitle>
<CardTitle className="text-2xl font-bold">{t('changelogPage.version', { version: entry.version })}</CardTitle>
<p className="text-sm text-muted-foreground">{entry.date}</p>
</div>
</CardHeader>
@@ -37,7 +40,7 @@ export function Changelog() {
"border-red-500/50 bg-red-500/10 text-red-700 dark:text-red-300": change.type === "Fixed",
})}
>
{change.type}
{t(`changelog.${change.type.toLowerCase()}`)}
</Badge>
<p className="text-foreground">{change.text}</p>
</li>

View File

@@ -1,10 +1,14 @@
"use client";
import Link from "next/link";
import { Github, Twitter } from "lucide-react";
import { Button } from "@/components/ui/button";
import { changelogData } from "@/lib/changelog-data";
import { LanguageSwitcher } from "./language-switcher";
import { useTranslation } from "@/context/i18n-context";
export function Footer() {
const { t } = useTranslation();
const latestVersion = changelogData[0]?.version;
return (
@@ -29,8 +33,8 @@ export function Footer() {
</div>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<Link href="/imprint" className="hover:text-primary transition-colors">Imprint</Link>
<Link href="/privacy" className="hover:text-primary transition-colors">Privacy</Link>
<Link href="/imprint" className="hover:text-primary transition-colors">{t('footer.imprint')}</Link>
<Link href="/privacy" className="hover:text-primary transition-colors">{t('footer.privacy')}</Link>
{latestVersion && (
<Link
href="/changelog"

View File

@@ -38,14 +38,6 @@ import {
} from "@/components/ui/tooltip";
import { useTranslation } from "@/context/i18n-context";
const aspectRatios = [
{ name: "Custom", value: "custom" },
{ name: "1:1 (Square)", value: "1/1" },
{ name: "4:3 (Standard)", value: "4/3" },
{ name: "3:2 (Photography)", value: "3/2" },
{ name: "16:9 (Widescreen)", value: "16/9" },
];
const initialSettings = {
width: "",
height: "",
@@ -92,6 +84,14 @@ export function ImageConverter() {
const [isDraggingOver, setIsDraggingOver] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const aspectRatios = [
{ name: t('settings.image.custom'), value: "custom" },
{ name: t('settings.image.square'), value: "1/1" },
{ name: t('settings.image.standard'), value: "4/3" },
{ name: t('settings.image.photography'), value: "3/2" },
{ name: t('settings.image.widescreen'), value: "16/9" },
];
useEffect(() => {
return () => {
previewUrls.forEach((url) => URL.revokeObjectURL(url));
@@ -106,7 +106,7 @@ export function ImageConverter() {
);
if (imageFiles.length === 0) {
toast.error("No valid image files found.");
toast.error(t('toasts.noValidImages'));
return;
}
@@ -128,7 +128,7 @@ export function ImageConverter() {
setPreviewUrls(newPreviewUrls);
setFilenames(newFilenames);
toast.success(`${imageFiles.length} image(s) added.`);
toast.success(t('toasts.imagesAdded', { count: imageFiles.length }));
};
const handleImageChange = (e: ChangeEvent<HTMLInputElement>) => {
@@ -170,7 +170,7 @@ export function ImageConverter() {
setFilenames([]);
setWidth(initialSettings.width);
setHeight(initialSettings.height);
toast.info("All images cleared.");
toast.info(t('toasts.allCleared'));
};
const handleFilenameChange = (index: number, newName: string) => {
@@ -299,12 +299,12 @@ export function ImageConverter() {
const handleConvertAndDownloadAll = async () => {
if (images.length === 0) {
toast.error("Please upload images first.");
toast.error(t('toasts.uploadFirst'));
return;
}
setIsConverting(true);
toast.info(`Starting conversion for ${images.length} images...`);
toast.info(t('toasts.conversionStarting', { count: images.length }));
const conversionPromises = images.map((image, index) =>
convertAndDownload(image, previewUrls[index], index)
@@ -312,12 +312,12 @@ export function ImageConverter() {
try {
await Promise.all(conversionPromises);
toast.success(`Successfully exported all ${images.length} images!`);
toast.success(t('toasts.conversionSuccess', { count: images.length }));
} catch (error) {
if (error instanceof Error) {
toast.error(error.message);
} else {
toast.error("An unknown error occurred during conversion.");
toast.error(t('toasts.conversionError'));
}
} finally {
setIsConverting(false);
@@ -326,16 +326,16 @@ export function ImageConverter() {
const handleConvertAndDownloadSingle = async (index: number) => {
setConvertingIndex(index);
toast.info(`Starting conversion for ${filenames[index]}...`);
toast.info(t('toasts.singleConversionStarting', { filename: filenames[index] }));
try {
await convertAndDownload(images[index], previewUrls[index], index);
toast.success(`Successfully exported ${filenames[index]}!`);
toast.success(t('toasts.singleConversionSuccess', { filename: filenames[index] }));
} catch (error) {
if (error instanceof Error) {
toast.error(error.message);
} else {
toast.error("An unknown error occurred during conversion.");
toast.error(t('toasts.conversionError'));
}
} finally {
setConvertingIndex(null);
@@ -343,7 +343,7 @@ export function ImageConverter() {
};
const handleApplySettings = () => {
toast.info("Settings updated and will be used for all downloads.");
toast.info(t('toasts.settingsApplied'));
};
const handleResetSettings = () => {
@@ -362,7 +362,7 @@ export function ImageConverter() {
setDefaultBaseName(initialSettings.defaultBaseName);
setScaleMode(initialSettings.scaleMode);
setObjectPosition(initialSettings.objectPosition);
toast.success("All settings have been reset to their defaults.");
toast.success(t('toasts.settingsReset'));
};
const handleAspectRatioChange = (value: string) => {
@@ -410,16 +410,16 @@ export function ImageConverter() {
const handleApplyDefaultBaseNameToAll = () => {
if (!defaultBaseName) {
toast.error("Please enter a default base name to apply.");
toast.error(t('toasts.enterBaseName'));
return;
}
if (!hasImages) {
toast.info("Upload some images first.");
toast.info(t('toasts.uploadToApplyBaseName'));
return;
}
const newFilenames = filenames.map(() => defaultBaseName);
setFilenames(newFilenames);
toast.success(`Set base name to "${defaultBaseName}" for all ${images.length} images.`);
toast.success(t('toasts.baseNameApplied', { baseName: defaultBaseName, count: images.length }));
};
return (
@@ -462,7 +462,7 @@ export function ImageConverter() {
<Button variant="ghost" size="sm" onClick={handleClearAll} disabled={isConverting || convertingIndex !== null}><Trash2 className="mr-2 h-4 w-4" />{t('clearAll')}</Button>
</TooltipTrigger>
<TooltipContent>
<p>Remove all uploaded images.</p>
<p>{t('imageCard.removeAllTooltip')}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
@@ -487,7 +487,7 @@ export function ImageConverter() {
<div key={url} className="p-4 border rounded-lg flex items-center gap-4">
<img src={url} alt={`Preview ${index + 1}`} className="w-20 h-20 object-cover rounded-md shrink-0" />
<div className="flex-1 min-w-0">
<Label htmlFor={`filename-${index}`} className="text-xs text-muted-foreground">Base Name</Label>
<Label htmlFor={`filename-${index}`} className="text-xs text-muted-foreground">{t('imageCard.baseName')}</Label>
<Input
id={`filename-${index}`}
value={filenames[index]}
@@ -495,7 +495,7 @@ export function ImageConverter() {
className="text-sm font-medium h-8 mt-1"
/>
<p className="text-xs text-muted-foreground truncate mt-1" title={`${finalFilename}.${format}`}>
Final name: {finalFilename}.{format}
{t('imageCard.finalName', { filename: `${finalFilename}.${format}` })}
</p>
</div>
<div className="flex items-center shrink-0">
@@ -512,7 +512,7 @@ export function ImageConverter() {
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Download this image</p>
<p>{t('imageCard.downloadTooltip')}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
@@ -528,7 +528,7 @@ export function ImageConverter() {
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Remove this image</p>
<p>{t('imageCard.removeTooltip')}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -546,9 +546,9 @@ export function ImageConverter() {
<AccordionItem value="image-settings" className="border rounded-lg bg-card">
<AccordionTrigger className="p-6 hover:no-underline">
<div className="text-left">
<h3 className="text-lg font-medium leading-none">Image Settings</h3>
<h3 className="text-lg font-medium leading-none">{t('settings.image.title')}</h3>
<p className="text-sm text-muted-foreground mt-1">
Adjust resolution and scaling for all images.
{t('settings.image.description')}
</p>
</div>
</AccordionTrigger>
@@ -556,13 +556,13 @@ export function ImageConverter() {
<div className="space-y-4">
<div>
<div className="flex items-center gap-1.5">
<Label htmlFor="aspect-ratio">Aspect Ratio</Label>
<Label htmlFor="aspect-ratio">{t('settings.image.aspectRatio')}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>Choose a preset aspect ratio or select 'Custom' to enter dimensions manually.</p>
<p>{t('settings.image.aspectRatioTooltip')}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -582,13 +582,13 @@ export function ImageConverter() {
<div className="flex items-end gap-2">
<div className="space-y-2 flex-1">
<div className="flex items-center gap-1.5">
<Label htmlFor="width">Width (px)</Label>
<Label htmlFor="width">{t('settings.image.width')}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>Set the output width in pixels. Leave blank to use the original width.</p>
<p>{t('settings.image.widthTooltip')}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -601,18 +601,18 @@ export function ImageConverter() {
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Swap the entered width and height values.</p>
<p>{t('settings.image.swapDimensionsTooltip')}</p>
</TooltipContent>
</Tooltip>
<div className="space-y-2 flex-1">
<div className="flex items-center gap-1.5">
<Label htmlFor="height">Height (px)</Label>
<Label htmlFor="height">{t('settings.image.height')}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>Set the output height in pixels. Leave blank to use the original height.</p>
<p>{t('settings.image.heightTooltip')}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -622,13 +622,13 @@ export function ImageConverter() {
<div className="flex items-center space-x-2 pt-2">
<Checkbox id="keep-orientation" checked={keepOrientation} onCheckedChange={(checked) => setKeepOrientation(Boolean(checked))} />
<Label htmlFor="keep-orientation" className="cursor-pointer flex items-center gap-1.5">
Keep original orientation
{t('settings.image.keepOrientation')}
<Tooltip>
<TooltipTrigger onClick={(e) => e.preventDefault()}>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>Automatically swaps width and height to match the original image's orientation.</p>
<p>{t('settings.image.keepOrientationTooltip')}</p>
</TooltipContent>
</Tooltip>
</Label>
@@ -636,35 +636,35 @@ export function ImageConverter() {
</div>
<div className="mt-4 space-y-2">
<div className="flex items-center gap-1.5">
<Label htmlFor="scale-mode">Scaling</Label>
<Label htmlFor="scale-mode">{t('settings.image.scaling')}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>Determines how the image fits into the new dimensions.</p>
<p>{t('settings.image.scalingTooltip')}</p>
</TooltipContent>
</Tooltip>
</div>
<Select value={scaleMode} onValueChange={(value: 'fill' | 'cover' | 'contain') => setScaleMode(value)}>
<SelectTrigger id="scale-mode"><SelectValue placeholder="Select scaling mode" /></SelectTrigger>
<SelectContent>
<SelectItem value="fill">Fill (stretch to fit)</SelectItem>
<SelectItem value="cover">Cover (crop to fit)</SelectItem>
<SelectItem value="contain">Contain (letterbox)</SelectItem>
<SelectItem value="fill">{t('settings.image.fill')}</SelectItem>
<SelectItem value="cover">{t('settings.image.cover')}</SelectItem>
<SelectItem value="contain">{t('settings.image.contain')}</SelectItem>
</SelectContent>
</Select>
</div>
{scaleMode !== 'fill' && (
<div className="mt-4 space-y-2">
<div className="flex items-center gap-1.5">
<Label>Position</Label>
<Label>{t('settings.image.position')}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>Sets the anchor point for 'Cover' or 'Contain' scaling.</p>
<p>{t('settings.image.positionTooltip')}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -677,8 +677,8 @@ export function ImageConverter() {
<AccordionItem value="filename-settings" className="border rounded-lg bg-card">
<AccordionTrigger className="p-6 hover:no-underline">
<div className="text-left">
<h3 className="text-lg font-medium leading-none">Filename Settings</h3>
<p className="text-sm text-muted-foreground mt-1">Customize the output filenames.</p>
<h3 className="text-lg font-medium leading-none">{t('settings.filename.title')}</h3>
<p className="text-sm text-muted-foreground mt-1">{t('settings.filename.description')}</p>
</div>
</AccordionTrigger>
<AccordionContent className="px-6 pb-6">
@@ -686,20 +686,20 @@ export function ImageConverter() {
<div className="flex items-center space-x-2">
<Switch id="use-default-base-name" checked={useDefaultBaseName} onCheckedChange={setUseDefaultBaseName} />
<Label htmlFor="use-default-base-name" className="flex items-center gap-1.5 cursor-pointer">
Use default base name
{t('settings.filename.useDefaultBaseName')}
<Tooltip>
<TooltipTrigger onClick={(e) => e.preventDefault()}>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>When enabled, all newly uploaded images will use the specified default base name.</p>
<p>{t('settings.filename.useDefaultBaseNameTooltip')}</p>
</TooltipContent>
</Tooltip>
</Label>
</div>
{useDefaultBaseName && (
<div className="space-y-2">
<Label htmlFor="default-base-name">Default base name</Label>
<Label htmlFor="default-base-name">{t('settings.filename.defaultBaseName')}</Label>
<div className="flex items-center gap-2">
<Input
id="default-base-name"
@@ -710,11 +710,11 @@ export function ImageConverter() {
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="sm" onClick={handleApplyDefaultBaseNameToAll} disabled={!defaultBaseName || !hasImages}>
Apply to all
{t('settings.filename.applyToAll')}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Apply this base name to all currently uploaded images.</p>
<p>{t('settings.filename.applyToAllTooltip')}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -722,13 +722,13 @@ export function ImageConverter() {
)}
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label htmlFor="prefix">Prefix</Label>
<Label htmlFor="prefix">{t('settings.filename.prefix')}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>Add text to the beginning of every filename.</p>
<p>{t('settings.filename.prefixTooltip')}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -736,13 +736,13 @@ export function ImageConverter() {
</div>
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label htmlFor="suffix">Suffix</Label>
<Label htmlFor="suffix">{t('settings.filename.suffix')}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>Add text to the end of every filename (before the number).</p>
<p>{t('settings.filename.suffixTooltip')}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -751,13 +751,13 @@ export function ImageConverter() {
<div className="flex items-center space-x-2 pt-2">
<Switch id="use-counter" checked={useCounter} onCheckedChange={setUseCounter} />
<Label htmlFor="use-counter" className="flex items-center gap-1.5 cursor-pointer">
Add sequential number
{t('settings.filename.addSequentialNumber')}
<Tooltip>
<TooltipTrigger onClick={(e) => e.preventDefault()}>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>Append a numbered sequence to each filename.</p>
<p>{t('settings.filename.addSequentialNumberTooltip')}</p>
</TooltipContent>
</Tooltip>
</Label>
@@ -766,13 +766,13 @@ export function ImageConverter() {
<div className="grid grid-cols-2 gap-4 pt-2">
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label htmlFor="counter-start">Start number</Label>
<Label htmlFor="counter-start">{t('settings.filename.startNumber')}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>The first number to use in the sequence.</p>
<p>{t('settings.filename.startNumberTooltip')}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -786,13 +786,13 @@ export function ImageConverter() {
</div>
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label htmlFor="counter-digits">Padding digits</Label>
<Label htmlFor="counter-digits">{t('settings.filename.paddingDigits')}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>Total number of digits for the counter, padded with leading zeros (e.g., 3 for 001).</p>
<p>{t('settings.filename.paddingDigitsTooltip')}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -813,21 +813,21 @@ export function ImageConverter() {
<AccordionItem value="quality-settings" className="border rounded-lg bg-card">
<AccordionTrigger className="p-6 hover:no-underline">
<div className="text-left">
<h3 className="text-lg font-medium leading-none">Quality Settings</h3>
<p className="text-sm text-muted-foreground mt-1">Choose format and compression level.</p>
<h3 className="text-lg font-medium leading-none">{t('settings.quality.title')}</h3>
<p className="text-sm text-muted-foreground mt-1">{t('settings.quality.description')}</p>
</div>
</AccordionTrigger>
<AccordionContent className="px-6 pb-6">
<div className="space-y-6">
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label htmlFor="format">Format</Label>
<Label htmlFor="format">{t('settings.quality.format')}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>Choose the output file format for the images.</p>
<p>{t('settings.quality.formatTooltip')}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -843,13 +843,13 @@ export function ImageConverter() {
<div className="space-y-2">
<div className="flex justify-between items-center">
<div className="flex items-center gap-1.5">
<Label htmlFor="quality">Quality</Label>
<Label htmlFor="quality">{t('settings.quality.quality')}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>Set compression quality for JPEG/WEBP. Higher is better quality but larger file size.</p>
<p>{t('settings.quality.qualityTooltip')}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -865,7 +865,7 @@ export function ImageConverter() {
disabled={format === 'png'}
/>
{format === 'png' && (
<p className="text-xs text-muted-foreground pt-1">Quality slider is disabled for PNG (lossless format).</p>
<p className="text-xs text-muted-foreground pt-1">{t('settings.quality.pngQualityDisabled')}</p>
)}
</div>
</div>
@@ -881,11 +881,11 @@ export function ImageConverter() {
variant="outline"
>
<RotateCcw className="mr-2 h-4 w-4" />
Reset
{t('settings.reset')}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Reset all settings to their default values.</p>
<p>{t('settings.resetTooltip')}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
@@ -895,11 +895,11 @@ export function ImageConverter() {
className="w-full"
>
<Check className="mr-2 h-4 w-4" />
Apply
{t('settings.apply')}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Confirm and apply all the settings above. This does not download the images.</p>
<p>{t('settings.applyTooltip')}</p>
</TooltipContent>
</Tooltip>
</div>