diff --git a/messages/de.json b/messages/de.json new file mode 100644 index 0000000..d712ce2 --- /dev/null +++ b/messages/de.json @@ -0,0 +1,102 @@ +{ + "HomePage": { + "title": "Bild-Web-Exporter", + "description": "Laden Sie ein Bild hoch und exportieren Sie es in einer anderen Auflösung und einem anderen Format." + }, + "LanguageSwitcher": { + "placeholder": "Sprache", + "en": "Englisch", + "de": "Deutsch" + }, + "ImageConverter": { + "uploadTitle": "Bilder hochladen", + "uploadButton": "Klicken oder per Drag & Drop hochladen", + "uploadHint": "PNG, JPG, WEBP werden unterstützt", + "uploadedImagesTitle": "Hochgeladene Bilder", + "clearAll": "Alle löschen", + "clearAllTooltip": "Alle hochgeladenen Bilder entfernen.", + "downloadAll": "Alle herunterladen ({count})", + "downloadAllConverting": "Konvertiere...", + "downloadAllTooltip": "Alle Bilder mit den aktuellen Einstellungen konvertieren und herunterladen.", + "baseNameLabel": "Basisname", + "finalNameLabel": "Endgültiger Name: {filename}", + "downloadImageTooltip": "Dieses Bild herunterladen", + "removeImageTooltip": "Dieses Bild entfernen", + "applySettings": "Einstellungen anwenden", + "applySettingsTooltip": "Bestätigen und alle oben genannten Einstellungen anwenden. Dies lädt die Bilder nicht herunter.", + "settings": { + "image": { + "title": "Bildeinstellungen", + "description": "Passen Sie Auflösung und Skalierung für alle Bilder an.", + "aspectRatio": "Seitenverhältnis", + "aspectRatioTooltip": "Wählen Sie ein voreingestelltes Seitenverhältnis oder 'Benutzerdefiniert', um die Abmessungen manuell einzugeben.", + "aspectRatios": { + "custom": "Benutzerdefiniert", + "square": "1:1 (Quadratisch)", + "standard": "4:3 (Standard)", + "photography": "3:2 (Fotografie)", + "widescreen": "16:9 (Breitbild)" + }, + "width": "Breite (px)", + "widthTooltip": "Legen Sie die Ausgabebreite in Pixel fest. Leer lassen, um die Originalbreite zu verwenden.", + "height": "Höhe (px)", + "heightTooltip": "Legen Sie die Ausgabehöhe in Pixel fest. Leer lassen, um die Originalhöhe zu verwenden.", + "swapDimensionsTooltip": "Die eingegebenen Breiten- und Höhenwerte tauschen.", + "keepOrientation": "Originalausrichtung beibehalten", + "keepOrientationTooltip": "Tauscht automatisch Breite und Höhe, um der Ausrichtung des Originalbildes zu entsprechen.", + "scaling": "Skalierung", + "scalingTooltip": "Bestimmt, wie das Bild in die neuen Abmessungen passt.", + "scalingOptions": { + "fill": "Füllen (strecken)", + "cover": "Abdecken (zuschneiden)", + "contain": "Enthalten (Letterbox)" + }, + "position": "Position", + "positionTooltip": "Legt den Ankerpunkt für die Skalierung 'Abdecken' oder 'Enthalten' fest." + }, + "filename": { + "title": "Dateiname-Einstellungen", + "description": "Passen Sie die Ausgabedateinamen an.", + "useDefaultBaseName": "Standard-Basisnamen verwenden", + "useDefaultBaseNameTooltip": "Wenn aktiviert, verwenden alle neu hochgeladenen Bilder den angegebenen Standard-Basisnamen.", + "defaultBaseName": "Standard-Basisname", + "applyToAll": "Auf alle anwenden", + "applyToAllTooltip": "Diesen Basisnamen auf alle aktuell hochgeladenen Bilder anwenden.", + "prefix": "Präfix", + "prefixTooltip": "Fügen Sie Text am Anfang jedes Dateinamens hinzu.", + "suffix": "Suffix", + "suffixTooltip": "Fügen Sie Text am Ende jedes Dateinamens hinzu (vor der Nummer).", + "addSequentialNumber": "Laufende Nummer hinzufügen", + "addSequentialNumberTooltip": "Fügen Sie jedem Dateinamen eine nummerierte Sequenz an.", + "startNumber": "Startnummer", + "startNumberTooltip": "Die erste Nummer, die in der Sequenz verwendet wird.", + "paddingDigits": "Auffüllziffern", + "paddingDigitsTooltip": "Gesamtzahl der Ziffern für den Zähler, mit führenden Nullen aufgefüllt (z. B. 3 für 001)." + }, + "quality": { + "title": "Qualitätseinstellungen", + "description": "Wählen Sie Format und Komprimierungsstufe.", + "format": "Format", + "formatTooltip": "Wählen Sie das Ausgabedateiformat für die Bilder.", + "quality": "Qualität", + "qualityTooltip": "Stellen Sie die Komprimierungsqualität für JPEG/WEBP ein. Höher bedeutet bessere Qualität, aber größere Dateigröße.", + "pngHint": "Der Qualitätsschieberegler ist für PNG (verlustfreies Format) deaktiviert." + } + }, + "toasts": { + "noImages": "Bitte laden Sie zuerst Bilder hoch.", + "conversionStarting": "Konvertierung für {count} Bilder wird gestartet...", + "conversionSuccess": "Alle {count} Bilder erfolgreich exportiert!", + "conversionError": "Während der Konvertierung ist ein unbekannter Fehler aufgetreten.", + "singleConversionStarting": "Konvertierung für {filename} wird gestartet...", + "singleConversionSuccess": "{filename} erfolgreich exportiert!", + "settingsApplied": "Einstellungen aktualisiert und werden für alle Downloads verwendet.", + "noValidImages": "Keine gültigen Bilddateien gefunden.", + "imagesAdded": "{count} Bild(er) hinzugefügt.", + "allCleared": "Alle Bilder gelöscht.", + "noDefaultBaseName": "Bitte geben Sie einen Standard-Basisnamen ein, um ihn anzuwenden.", + "uploadFirst": "Laden Sie zuerst einige Bilder hoch.", + "baseNameApplied": "Basisname für alle {count} Bilder auf \"{baseName}\" gesetzt." + } + } +} \ No newline at end of file diff --git a/messages/en.json b/messages/en.json new file mode 100644 index 0000000..7e72514 --- /dev/null +++ b/messages/en.json @@ -0,0 +1,102 @@ +{ + "HomePage": { + "title": "Image Web Exporter", + "description": "Upload a picture, then export it in a different resolution and format." + }, + "LanguageSwitcher": { + "placeholder": "Language", + "en": "English", + "de": "German" + }, + "ImageConverter": { + "uploadTitle": "Upload Images", + "uploadButton": "Click or drag and drop to upload", + "uploadHint": "PNG, JPG, WEBP supported", + "uploadedImagesTitle": "Uploaded Images", + "clearAll": "Clear All", + "clearAllTooltip": "Remove all uploaded images.", + "downloadAll": "Download All ({count})", + "downloadAllConverting": "Converting...", + "downloadAllTooltip": "Convert and download all images with the current settings.", + "baseNameLabel": "Base Name", + "finalNameLabel": "Final name: {filename}", + "downloadImageTooltip": "Download this image", + "removeImageTooltip": "Remove this image", + "applySettings": "Apply Settings", + "applySettingsTooltip": "Confirm and apply all the settings above. This does not download the images.", + "settings": { + "image": { + "title": "Image Settings", + "description": "Adjust resolution and scaling for all images.", + "aspectRatio": "Aspect Ratio", + "aspectRatioTooltip": "Choose a preset aspect ratio or select 'Custom' to enter dimensions manually.", + "aspectRatios": { + "custom": "Custom", + "square": "1:1 (Square)", + "standard": "4:3 (Standard)", + "photography": "3:2 (Photography)", + "widescreen": "16:9 (Widescreen)" + }, + "width": "Width (px)", + "widthTooltip": "Set the output width in pixels. Leave blank to use the original width.", + "height": "Height (px)", + "heightTooltip": "Set the output height in pixels. Leave blank to use the original height.", + "swapDimensionsTooltip": "Swap the entered width and height values.", + "keepOrientation": "Keep original orientation", + "keepOrientationTooltip": "Automatically swaps width and height to match the original image's orientation.", + "scaling": "Scaling", + "scalingTooltip": "Determines how the image fits into the new dimensions.", + "scalingOptions": { + "fill": "Fill (stretch to fit)", + "cover": "Cover (crop to fit)", + "contain": "Contain (letterbox)" + }, + "position": "Position", + "positionTooltip": "Sets the anchor point for 'Cover' or 'Contain' scaling." + }, + "filename": { + "title": "Filename Settings", + "description": "Customize the output filenames.", + "useDefaultBaseName": "Use default base name", + "useDefaultBaseNameTooltip": "When enabled, all newly uploaded images will use the specified default base name.", + "defaultBaseName": "Default base name", + "applyToAll": "Apply to all", + "applyToAllTooltip": "Apply this base name to all currently uploaded images.", + "prefix": "Prefix", + "prefixTooltip": "Add text to the beginning of every filename.", + "suffix": "Suffix", + "suffixTooltip": "Add text to the end of every filename (before the number).", + "addSequentialNumber": "Add sequential number", + "addSequentialNumberTooltip": "Append a numbered sequence to each filename.", + "startNumber": "Start number", + "startNumberTooltip": "The first number to use in the sequence.", + "paddingDigits": "Padding digits", + "paddingDigitsTooltip": "Total number of digits for the counter, padded with leading zeros (e.g., 3 for 001)." + }, + "quality": { + "title": "Quality Settings", + "description": "Choose format and compression level.", + "format": "Format", + "formatTooltip": "Choose the output file format for the images.", + "quality": "Quality", + "qualityTooltip": "Set compression quality for JPEG/WEBP. Higher is better quality but larger file size.", + "pngHint": "Quality slider is disabled for PNG (lossless format)." + } + }, + "toasts": { + "noImages": "Please upload images first.", + "conversionStarting": "Starting conversion for {count} images...", + "conversionSuccess": "Successfully exported all {count} images!", + "conversionError": "An unknown error occurred during conversion.", + "singleConversionStarting": "Starting conversion for {filename}...", + "singleConversionSuccess": "Successfully exported {filename}!", + "settingsApplied": "Settings updated and will be used for all downloads.", + "noValidImages": "No valid image files found.", + "imagesAdded": "{count} image(s) added.", + "allCleared": "All images cleared.", + "noDefaultBaseName": "Please enter a default base name to apply.", + "uploadFirst": "Upload some images first.", + "baseNameApplied": "Set base name to \"{baseName}\" for all {count} images." + } + } +} \ No newline at end of file diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..47b4692 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,10 @@ +import createMiddleware from 'next-intl/middleware'; + +export default createMiddleware({ + locales: ['en', 'de'], + defaultLocale: 'en' +}); + +export const config = { + matcher: ['/', '/(de|en)/:path*'] +}; \ No newline at end of file diff --git a/package.json b/package.json index e6537d8..530c500 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "input-otp": "^1.4.2", "lucide-react": "^0.511.0", "next": "15.3.8", + "next-intl": "^4.7.0", "next-themes": "^0.4.6", "react": "^19.2.1", "react-day-picker": "^8.10.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1383c3..b2c681b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,6 +113,9 @@ importers: next: specifier: 15.3.8 version: 15.3.8(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + next-intl: + specifier: ^4.7.0 + version: 4.7.0(next@15.3.8(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(typescript@5.8.3) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -222,6 +225,24 @@ packages: '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + '@formatjs/ecma402-abstract@2.3.6': + resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==} + + '@formatjs/fast-memoize@2.2.7': + resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} + + '@formatjs/icu-messageformat-parser@2.11.4': + resolution: {integrity: sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==} + + '@formatjs/icu-skeleton-parser@1.8.16': + resolution: {integrity: sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==} + + '@formatjs/intl-localematcher@0.5.10': + resolution: {integrity: sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==} + + '@formatjs/intl-localematcher@0.6.2': + resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} + '@hookform/resolvers@5.0.1': resolution: {integrity: sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==} peerDependencies: @@ -431,6 +452,88 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@parcel/watcher-android-arm64@2.5.4': + resolution: {integrity: sha512-hoh0vx4v+b3BNI7Cjoy2/B0ARqcwVNrzN/n7DLq9ZB4I3lrsvhrkCViJyfTj/Qi5xM9YFiH4AmHGK6pgH1ss7g==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.4': + resolution: {integrity: sha512-kphKy377pZiWpAOyTgQYPE5/XEKVMaj6VUjKT5VkNyUJlr2qZAn8gIc7CPzx+kbhvqHDT9d7EqdOqRXT6vk0zw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.4': + resolution: {integrity: sha512-UKaQFhCtNJW1A9YyVz3Ju7ydf6QgrpNQfRZ35wNKUhTQ3dxJ/3MULXN5JN/0Z80V/KUBDGa3RZaKq1EQT2a2gg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.4': + resolution: {integrity: sha512-Dib0Wv3Ow/m2/ttvLdeI2DBXloO7t3Z0oCp4bAb2aqyqOjKPPGrg10pMJJAQ7tt8P4V2rwYwywkDhUia/FgS+Q==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.4': + resolution: {integrity: sha512-I5Vb769pdf7Q7Sf4KNy8Pogl/URRCKu9ImMmnVKYayhynuyGYMzuI4UOWnegQNa2sGpsPSbzDsqbHNMyeyPCgw==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.4': + resolution: {integrity: sha512-kGO8RPvVrcAotV4QcWh8kZuHr9bXi9a3bSZw7kFarYR0+fGliU7hd/zevhjw8fnvIKG3J9EO5G6sXNGCSNMYPQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.4': + resolution: {integrity: sha512-KU75aooXhqGFY2W5/p8DYYHt4hrjHZod8AhcGAmhzPn/etTa+lYCDB2b1sJy3sWJ8ahFVTdy+EbqSBvMx3iFlw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.4': + resolution: {integrity: sha512-Qx8uNiIekVutnzbVdrgSanM+cbpDD3boB1f8vMtnuG5Zau4/bdDbXyKwIn0ToqFhIuob73bcxV9NwRm04/hzHQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.4': + resolution: {integrity: sha512-UYBQvhYmgAv61LNUn24qGQdjtycFBKSK3EXr72DbJqX9aaLbtCOO8+1SkKhD/GNiJ97ExgcHBrukcYhVjrnogA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.4': + resolution: {integrity: sha512-YoRWCVgxv8akZrMhdyVi6/TyoeeMkQ0PGGOf2E4omODrvd1wxniXP+DBynKoHryStks7l+fDAMUBRzqNHrVOpg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.4': + resolution: {integrity: sha512-iby+D/YNXWkiQNYcIhg8P5hSjzXEHaQrk2SLrWOUD7VeC4Ohu0WQvmV+HDJokZVJ2UjJ4AGXW3bx7Lls9Ln4TQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.4': + resolution: {integrity: sha512-vQN+KIReG0a2ZDpVv8cgddlf67J8hk1WfZMMP7sMeZmJRSmEax5xNDNWKdgqSe2brOKTQQAs3aCCUal2qBHAyg==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.4': + resolution: {integrity: sha512-3A6efb6BOKwyw7yk9ro2vus2YTt2nvcd56AuzxdMiVOxL9umDyN5PKkKfZ/gZ9row41SjVmTVQNWQhaRRGpOKw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.4': + resolution: {integrity: sha512-WYa2tUVV5HiArWPB3ydlOc4R2ivq0IDrlqhMi3l7mVsFEXNcTfxYFPIHXHXIh/ca/y/V5N4E1zecyxdIBjYnkQ==} + engines: {node: '>= 10.0.0'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1047,15 +1150,90 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@schummar/icu-type-parser@1.21.5': + resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==} + '@standard-schema/utils@0.3.0': resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@swc/core-darwin-arm64@1.15.8': + resolution: {integrity: sha512-M9cK5GwyWWRkRGwwCbREuj6r8jKdES/haCZ3Xckgkl8MUQJZA3XB7IXXK1IXRNeLjg6m7cnoMICpXv1v1hlJOg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.8': + resolution: {integrity: sha512-j47DasuOvXl80sKJHSi2X25l44CMc3VDhlJwA7oewC1nV1VsSzwX+KOwE5tLnfORvVJJyeiXgJORNYg4jeIjYQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.8': + resolution: {integrity: sha512-siAzDENu2rUbwr9+fayWa26r5A9fol1iORG53HWxQL1J8ym4k7xt9eME0dMPXlYZDytK5r9sW8zEA10F2U3Xwg==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.8': + resolution: {integrity: sha512-o+1y5u6k2FfPYbTRUPvurwzNt5qd0NTumCTFscCNuBksycloXY16J8L+SMW5QRX59n4Hp9EmFa3vpvNHRVv1+Q==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.15.8': + resolution: {integrity: sha512-koiCqL09EwOP1S2RShCI7NbsQuG6r2brTqUYE7pV7kZm9O17wZ0LSz22m6gVibpwEnw8jI3IE1yYsQTVpluALw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-x64-gnu@1.15.8': + resolution: {integrity: sha512-4p6lOMU3bC+Vd5ARtKJ/FxpIC5G8v3XLoPEZ5s7mLR8h7411HWC/LmTXDHcrSXRC55zvAVia1eldy6zDLz8iFQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.15.8': + resolution: {integrity: sha512-z3XBnbrZAL+6xDGAhJoN4lOueIxC/8rGrJ9tg+fEaeqLEuAtHSW2QHDHxDwkxZMjuF/pZ6MUTjHjbp8wLbuRLA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.15.8': + resolution: {integrity: sha512-djQPJ9Rh9vP8GTS/Df3hcc6XP6xnG5c8qsngWId/BLA9oX6C7UzCPAn74BG/wGb9a6j4w3RINuoaieJB3t+7iQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.8': + resolution: {integrity: sha512-/wfAgxORg2VBaUoFdytcVBVCgf1isWZIEXB9MZEUty4wwK93M/PxAkjifOho9RN3WrM3inPLabICRCEgdHpKKQ==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.8': + resolution: {integrity: sha512-GpMePrh9Sl4d61o4KAHOOv5is5+zt6BEXCOCgs/H0FLGeii7j9bWDE8ExvKFy2GRRZVNR1ugsnzaGWHKM6kuzA==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.8': + resolution: {integrity: sha512-T8keoJjXaSUoVBCIjgL6wAnhADIb09GOELzKg10CjNg+vLX48P93SME6jTfte9MZIm5m+Il57H3rTSk/0kzDUw==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@swc/types@0.1.25': + resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} + '@types/d3-array@3.2.1': resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} @@ -1348,6 +1526,9 @@ packages: decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + detect-libc@2.0.4: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} @@ -1499,6 +1680,9 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + intl-messageformat@10.7.18: + resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==} + is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} @@ -1613,9 +1797,26 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + next-intl-swc-plugin-extractor@4.7.0: + resolution: {integrity: sha512-iAqflu2FWdQMWhwB0B2z52X7LmEpvnMNJXqVERZQ7bK5p9iqQLu70ur6Ka6NfiXLxfb+AeAkUX5qIciQOg+87A==} + + next-intl@4.7.0: + resolution: {integrity: sha512-gvROzcNr/HM0jTzQlKWQxUNk8jrZ0bREz+bht3wNbv+uzlZ5Kn3J+m+viosub18QJ72S08UJnVK50PXWcUvwpQ==} + peerDependencies: + next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + next-themes@0.4.6: resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: @@ -1643,6 +1844,9 @@ packages: sass: optional: true + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} @@ -1679,6 +1883,10 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -1687,6 +1895,9 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + po-parser@2.1.1: + resolution: {integrity: sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==} + postcss-import@15.1.0: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -2031,6 +2242,11 @@ packages: '@types/react': optional: true + use-intl@4.7.0: + resolution: {integrity: sha512-jyd8nSErVRRsSlUa+SDobKHo9IiWs5fjcPl9VBUnzUyEQpVM5mwJCgw8eUiylhvBpLQzUGox1KN0XlRivSID9A==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 + use-sidecar@1.1.3: resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} engines: {node: '>=10'} @@ -2145,6 +2361,36 @@ snapshots: '@floating-ui/utils@0.2.9': {} + '@formatjs/ecma402-abstract@2.3.6': + dependencies: + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/intl-localematcher': 0.6.2 + decimal.js: 10.6.0 + tslib: 2.8.1 + + '@formatjs/fast-memoize@2.2.7': + dependencies: + tslib: 2.8.1 + + '@formatjs/icu-messageformat-parser@2.11.4': + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + '@formatjs/icu-skeleton-parser': 1.8.16 + tslib: 2.8.1 + + '@formatjs/icu-skeleton-parser@1.8.16': + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + tslib: 2.8.1 + + '@formatjs/intl-localematcher@0.5.10': + dependencies: + tslib: 2.8.1 + + '@formatjs/intl-localematcher@0.6.2': + dependencies: + tslib: 2.8.1 + '@hookform/resolvers@5.0.1(react-hook-form@7.56.4(react@19.2.1))': dependencies: '@standard-schema/utils': 0.3.0 @@ -2300,6 +2546,66 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@parcel/watcher-android-arm64@2.5.4': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.4': + optional: true + + '@parcel/watcher-darwin-x64@2.5.4': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.4': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.4': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.4': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.4': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.4': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.4': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.4': + optional: true + + '@parcel/watcher-win32-arm64@2.5.4': + optional: true + + '@parcel/watcher-win32-ia32@2.5.4': + optional: true + + '@parcel/watcher-win32-x64@2.5.4': + optional: true + + '@parcel/watcher@2.5.4': + dependencies: + detect-libc: 2.0.4 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.3 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.4 + '@parcel/watcher-darwin-arm64': 2.5.4 + '@parcel/watcher-darwin-x64': 2.5.4 + '@parcel/watcher-freebsd-x64': 2.5.4 + '@parcel/watcher-linux-arm-glibc': 2.5.4 + '@parcel/watcher-linux-arm-musl': 2.5.4 + '@parcel/watcher-linux-arm64-glibc': 2.5.4 + '@parcel/watcher-linux-arm64-musl': 2.5.4 + '@parcel/watcher-linux-x64-glibc': 2.5.4 + '@parcel/watcher-linux-x64-musl': 2.5.4 + '@parcel/watcher-win32-arm64': 2.5.4 + '@parcel/watcher-win32-ia32': 2.5.4 + '@parcel/watcher-win32-x64': 2.5.4 + '@pkgjs/parseargs@0.11.0': optional: true @@ -2956,14 +3262,66 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@schummar/icu-type-parser@1.21.5': {} + '@standard-schema/utils@0.3.0': {} + '@swc/core-darwin-arm64@1.15.8': + optional: true + + '@swc/core-darwin-x64@1.15.8': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.8': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.8': + optional: true + + '@swc/core-linux-arm64-musl@1.15.8': + optional: true + + '@swc/core-linux-x64-gnu@1.15.8': + optional: true + + '@swc/core-linux-x64-musl@1.15.8': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.8': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.8': + optional: true + + '@swc/core-win32-x64-msvc@1.15.8': + optional: true + + '@swc/core@1.15.8': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.25 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.8 + '@swc/core-darwin-x64': 1.15.8 + '@swc/core-linux-arm-gnueabihf': 1.15.8 + '@swc/core-linux-arm64-gnu': 1.15.8 + '@swc/core-linux-arm64-musl': 1.15.8 + '@swc/core-linux-x64-gnu': 1.15.8 + '@swc/core-linux-x64-musl': 1.15.8 + '@swc/core-win32-arm64-msvc': 1.15.8 + '@swc/core-win32-ia32-msvc': 1.15.8 + '@swc/core-win32-x64-msvc': 1.15.8 + '@swc/counter@0.1.3': {} '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 + '@swc/types@0.1.25': + dependencies: + '@swc/counter': 0.1.3 + '@types/d3-array@3.2.1': {} '@types/d3-color@3.1.3': {} @@ -3272,8 +3630,9 @@ snapshots: decimal.js-light@2.5.1: {} - detect-libc@2.0.4: - optional: true + decimal.js@10.6.0: {} + + detect-libc@2.0.4: {} detect-node-es@1.1.0: {} @@ -3402,6 +3761,13 @@ snapshots: internmap@2.0.3: {} + intl-messageformat@10.7.18: + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/icu-messageformat-parser': 2.11.4 + tslib: 2.8.1 + is-arrayish@0.3.2: optional: true @@ -3496,8 +3862,28 @@ snapshots: nanoid@3.3.11: {} + negotiator@1.0.0: {} + neo-async@2.6.2: {} + next-intl-swc-plugin-extractor@4.7.0: {} + + next-intl@4.7.0(next@15.3.8(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(typescript@5.8.3): + dependencies: + '@formatjs/intl-localematcher': 0.5.10 + '@parcel/watcher': 2.5.4 + '@swc/core': 1.15.8 + negotiator: 1.0.0 + next: 15.3.8(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + next-intl-swc-plugin-extractor: 4.7.0 + po-parser: 2.1.1 + react: 19.2.1 + use-intl: 4.7.0(react@19.2.1) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - '@swc/helpers' + next-themes@0.4.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: react: 19.2.1 @@ -3528,6 +3914,8 @@ snapshots: - '@babel/core' - babel-plugin-macros + node-addon-api@7.1.1: {} + node-releases@2.0.19: {} normalize-path@3.0.0: {} @@ -3551,10 +3939,14 @@ snapshots: picomatch@2.3.1: {} + picomatch@4.0.3: {} + pify@2.3.0: {} pirates@4.0.7: {} + po-parser@2.1.1: {} + postcss-import@15.1.0(postcss@8.5.3): dependencies: postcss: 8.5.3 @@ -3921,6 +4313,13 @@ snapshots: optionalDependencies: '@types/react': 19.1.5 + use-intl@4.7.0(react@19.2.1): + dependencies: + '@formatjs/fast-memoize': 2.2.7 + '@schummar/icu-type-parser': 1.21.5 + intl-messageformat: 10.7.18 + react: 19.2.1 + use-sidecar@1.1.3(@types/react@19.1.5)(react@19.2.1): dependencies: detect-node-es: 1.1.0 diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx new file mode 100644 index 0000000..710fbc6 --- /dev/null +++ b/src/app/[locale]/layout.tsx @@ -0,0 +1,44 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; +import { ThemeProvider } from "@/components/theme-provider"; +import { Toaster } from "@/components/ui/sonner"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Image Web Exporter", + description: "Upload a picture, then export it in a different resolution and format.", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + {children} + + + + + ); +} \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/[locale]/page.tsx similarity index 68% rename from src/app/page.tsx rename to src/app/[locale]/page.tsx index 42403e1..7d4900b 100644 --- a/src/app/page.tsx +++ b/src/app/[locale]/page.tsx @@ -1,15 +1,22 @@ import { ImageConverter } from "@/components/image-converter"; +import { LanguageSwitcher } from "@/components/language-switcher"; +import { useTranslations } from "next-intl"; export default function Home() { + const t = useTranslations("HomePage"); + return (
+
+ +

- Image Web Exporter + {t("title")}

- Upload a picture, then export it in a different resolution and format. + {t("description")}

diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 710fbc6..5c7b711 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,44 +1,17 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; -import { ThemeProvider } from "@/components/theme-provider"; -import { Toaster } from "@/components/ui/sonner"; +import { ReactNode } from 'react'; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); - -export const metadata: Metadata = { - title: "Image Web Exporter", - description: "Upload a picture, then export it in a different resolution and format.", +type Props = { + children: ReactNode; + params: { locale: string }; }; -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { +// Even though this component is just passing its children through, the presence +// of this file fixes an issue in Next.js 13.4 where link clicks that switch +// the locale would otherwise cause a full page reload. +export default function RootLayout({ children, params: { locale } }: Props) { return ( - - - - {children} - - - + + {children} ); } \ No newline at end of file diff --git a/src/components/image-converter.tsx b/src/components/image-converter.tsx index 760def6..dedeed7 100644 --- a/src/components/image-converter.tsx +++ b/src/components/image-converter.tsx @@ -36,16 +36,18 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { useTranslations } from "next-intl"; 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" }, + { name: "custom", value: "custom" }, + { name: "square", value: "1/1" }, + { name: "standard", value: "4/3" }, + { name: "photography", value: "3/2" }, + { name: "widescreen", value: "16/9" }, ]; export function ImageConverter() { + const t = useTranslations("ImageConverter"); const [images, setImages] = useState([]); const [previewUrls, setPreviewUrls] = useState([]); const [filenames, setFilenames] = useState([]); @@ -86,7 +88,7 @@ export function ImageConverter() { ); if (imageFiles.length === 0) { - toast.error("No valid image files found."); + toast.error(t("toasts.noValidImages")); return; } @@ -108,7 +110,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) => { @@ -148,7 +150,7 @@ export function ImageConverter() { setImages([]); setPreviewUrls([]); setFilenames([]); - toast.info("All images cleared."); + toast.info(t("toasts.allCleared")); }; const handleFilenameChange = (index: number, newName: string) => { @@ -260,12 +262,12 @@ export function ImageConverter() { const handleConvertAndDownloadAll = async () => { if (images.length === 0) { - toast.error("Please upload images first."); + toast.error(t("toasts.noImages")); 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) @@ -273,12 +275,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); @@ -287,16 +289,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); @@ -304,7 +306,7 @@ export function ImageConverter() { }; const handleApplySettings = () => { - toast.info("Settings updated and will be used for all downloads."); + toast.info(t("toasts.settingsApplied")); }; const handleAspectRatioChange = (value: string) => { @@ -352,16 +354,16 @@ export function ImageConverter() { const handleApplyDefaultBaseNameToAll = () => { if (!defaultBaseName) { - toast.error("Please enter a default base name to apply."); + toast.error(t("toasts.noDefaultBaseName")); return; } if (!hasImages) { - toast.info("Upload some images first."); + toast.info(t("toasts.uploadFirst")); 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 ( @@ -372,9 +374,9 @@ export function ImageConverter() {
-

Image Settings

+

{t("settings.image.title")}

- Adjust resolution and scaling for all images. + {t("settings.image.description")}

@@ -382,13 +384,13 @@ export function ImageConverter() {
- + -

Choose a preset aspect ratio or select 'Custom' to enter dimensions manually.

+

{t("settings.image.aspectRatioTooltip")}

@@ -399,7 +401,7 @@ export function ImageConverter() { {aspectRatios.map((ratio) => ( - {ratio.name} + {t(`settings.image.aspectRatios.${ratio.name}`)} ))} @@ -408,13 +410,13 @@ export function ImageConverter() {
- + -

Set the output width in pixels. Leave blank to use the original width.

+

{t("settings.image.widthTooltip")}

@@ -427,18 +429,18 @@ export function ImageConverter() { -

Swap the entered width and height values.

+

{t("settings.image.swapDimensionsTooltip")}

- + -

Set the output height in pixels. Leave blank to use the original height.

+

{t("settings.image.heightTooltip")}

@@ -448,13 +450,13 @@ export function ImageConverter() {
setKeepOrientation(Boolean(checked))} /> @@ -462,35 +464,35 @@ export function ImageConverter() {
- + -

Determines how the image fits into the new dimensions.

+

{t("settings.image.scalingTooltip")}

{scaleMode !== 'fill' && (
- + -

Sets the anchor point for 'Cover' or 'Contain' scaling.

+

{t("settings.image.positionTooltip")}

@@ -503,8 +505,8 @@ export function ImageConverter() {
-

Filename Settings

-

Customize the output filenames.

+

{t("settings.filename.title")}

+

{t("settings.filename.description")}

@@ -512,20 +514,20 @@ export function ImageConverter() {
{useDefaultBaseName && (
- +
-

Apply this base name to all currently uploaded images.

+

{t("settings.filename.applyToAllTooltip")}

@@ -548,13 +550,13 @@ export function ImageConverter() { )}
- + -

Add text to the beginning of every filename.

+

{t("settings.filename.prefixTooltip")}

@@ -562,13 +564,13 @@ export function ImageConverter() {
- + -

Add text to the end of every filename (before the number).

+

{t("settings.filename.suffixTooltip")}

@@ -577,13 +579,13 @@ export function ImageConverter() {
@@ -592,13 +594,13 @@ export function ImageConverter() {
- + -

The first number to use in the sequence.

+

{t("settings.filename.startNumberTooltip")}

@@ -612,13 +614,13 @@ export function ImageConverter() {
- + -

Total number of digits for the counter, padded with leading zeros (e.g., 3 for 001).

+

{t("settings.filename.paddingDigitsTooltip")}

@@ -639,21 +641,21 @@ export function ImageConverter() {
-

Quality Settings

-

Choose format and compression level.

+

{t("settings.quality.title")}

+

{t("settings.quality.description")}

- + -

Choose the output file format for the images.

+

{t("settings.quality.formatTooltip")}

@@ -669,13 +671,13 @@ export function ImageConverter() {
- + -

Set compression quality for JPEG/WEBP. Higher is better quality but larger file size.

+

{t("settings.quality.qualityTooltip")}

@@ -691,7 +693,7 @@ export function ImageConverter() { disabled={format === 'png'} /> {format === 'png' && ( -

Quality slider is disabled for PNG (lossless format).

+

{t("settings.quality.pngHint")}

)}
@@ -706,11 +708,11 @@ export function ImageConverter() { className="w-full" > - Apply Settings + {t("applySettings")} -

Confirm and apply all the settings above. This does not download the images.

+

{t("applySettingsTooltip")}

@@ -719,7 +721,7 @@ export function ImageConverter() {
-

Upload Images

+

{t("uploadTitle")}

-

Click or drag and drop to upload

-

PNG, JPG, WEBP supported

+

{t("uploadButton")}

+

{t("uploadHint")}

@@ -745,25 +747,25 @@ export function ImageConverter() {
- Uploaded Images + {t("uploadedImagesTitle")}
- + -

Remove all uploaded images.

+

{t("clearAllTooltip")}

-

Convert and download all images with the current settings.

+

{t("downloadAllTooltip")}

@@ -775,19 +777,20 @@ export function ImageConverter() { const baseFilename = generateFinalFilename(index); const dimensionSuffix = width && height ? `_${width}x${height}` : ''; const finalFilename = `${baseFilename}${dimensionSuffix}`; + const finalFilenameWithExt = `${finalFilename}.${format}`; return (
{`Preview
- + handleFilenameChange(index, e.target.value)} className="text-sm font-medium h-8 mt-1" /> -

- Final name: {finalFilename}.{format} +

+ {t("finalNameLabel", { filename: finalFilenameWithExt })}

@@ -804,7 +807,7 @@ export function ImageConverter() { -

Download this image

+

{t("downloadImageTooltip")}

@@ -820,7 +823,7 @@ export function ImageConverter() { -

Remove this image

+

{t("removeImageTooltip")}

diff --git a/src/components/language-switcher.tsx b/src/components/language-switcher.tsx new file mode 100644 index 0000000..0140966 --- /dev/null +++ b/src/components/language-switcher.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useLocale } from "next-intl"; +import { useRouter, usePathname } from "next-intl/client"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useTranslations } from "next-intl"; + +export function LanguageSwitcher() { + const t = useTranslations("LanguageSwitcher"); + const locale = useLocale(); + const router = useRouter(); + const pathname = usePathname(); + + const onSelectChange = (value: string) => { + router.replace(pathname, { locale: value }); + }; + + return ( + + ); +} \ No newline at end of file diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..7871cdb --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,5 @@ +import {getRequestConfig} from 'next-intl/server'; + +export default getRequestConfig(async ({locale}) => ({ + messages: (await import(`../messages/${locale}.json`)).default +})); \ No newline at end of file