diff --git a/messages/de.json b/messages/de.json
new file mode 100644
index 0000000..f1e3847
--- /dev/null
+++ b/messages/de.json
@@ -0,0 +1,143 @@
+{
+ "ImageConverter": {
+ "uploadTitle": "Bilder hochladen",
+ "uploadInstructions": "Klicken oder per Drag & Drop hochladen",
+ "uploadHint": "PNG, JPG, WEBP 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}",
+ "downloadSingleTooltip": "Dieses Bild herunterladen",
+ "removeSingleTooltip": "Dieses Bild entfernen",
+ "imageSettingsTitle": "Bildeinstellungen",
+ "imageSettingsDescription": "Auflösung und Skalierung für alle Bilder anpassen.",
+ "aspectRatioLabel": "Seitenverhältnis",
+ "aspectRatioTooltip": "Wählen Sie ein voreingestelltes Seitenverhältnis oder 'Benutzerdefiniert', um die Abmessungen manuell einzugeben.",
+ "aspectRatioPlaceholder": "Seitenverhältnis auswählen",
+ "aspectRatios": {
+ "custom": "Benutzerdefiniert",
+ "square": "1:1 (Quadratisch)",
+ "standard": "4:3 (Standard)",
+ "photography": "3:2 (Fotografie)",
+ "widescreen": "16:9 (Breitbild)"
+ },
+ "widthLabel": "Breite (px)",
+ "widthTooltip": "Legen Sie die Ausgabebreite in Pixel fest. Leer lassen, um die Originalbreite zu verwenden.",
+ "widthPlaceholder": "Auto",
+ "swapDimensionsTooltip": "Die eingegebenen Breiten- und Höhenwerte tauschen.",
+ "heightLabel": "Höhe (px)",
+ "heightTooltip": "Legen Sie die Ausgabehöhe in Pixel fest. Leer lassen, um die Originalhöhe zu verwenden.",
+ "heightPlaceholder": "Auto",
+ "keepOrientationLabel": "Originalausrichtung beibehalten",
+ "keepOrientationTooltip": "Tauscht automatisch Breite und Höhe, um der Ausrichtung des Originalbildes zu entsprechen.",
+ "scalingLabel": "Skalierung",
+ "scalingTooltip": "Bestimmt, wie das Bild in die neuen Abmessungen passt.",
+ "scalingPlaceholder": "Skalierungsmodus auswählen",
+ "scalingOptions": {
+ "fill": "Füllen (strecken)",
+ "cover": "Abdecken (zuschneiden)",
+ "contain": "Enthalten (Letterbox)"
+ },
+ "positionLabel": "Position",
+ "positionTooltip": "Legt den Ankerpunkt für die Skalierung 'Abdecken' oder 'Enthalten' fest.",
+ "filenameSettingsTitle": "Dateinameneinstellungen",
+ "filenameSettingsDescription": "Passen Sie die Ausgabedateinamen an.",
+ "useDefaultBaseNameLabel": "Standard-Basisnamen verwenden",
+ "useDefaultBaseNameTooltip": "Wenn aktiviert, verwenden alle neu hochgeladenen Bilder den angegebenen Standard-Basisnamen.",
+ "defaultBaseNameLabel": "Standard-Basisname",
+ "defaultBaseNamePlaceholder": "z.B. new-york-reise",
+ "applyToAll": "Auf alle anwenden",
+ "applyToAllTooltip": "Diesen Basisnamen auf alle aktuell hochgeladenen Bilder anwenden.",
+ "prefixLabel": "Präfix",
+ "prefixTooltip": "Fügen Sie Text am Anfang jedes Dateinamens hinzu.",
+ "prefixPlaceholder": "z.B. reise-",
+ "suffixLabel": "Suffix",
+ "suffixTooltip": "Fügen Sie Text am Ende jedes Dateinamens hinzu (vor der Nummer).",
+ "suffixPlaceholder": "z.B. -bearbeitet",
+ "useCounterLabel": "Sequentielle Nummer hinzufügen",
+ "useCounterTooltip": "Fügen Sie jedem Dateinamen eine nummerierte Sequenz an.",
+ "counterStartLabel": "Startnummer",
+ "counterStartTooltip": "Die erste Nummer, die in der Sequenz verwendet wird.",
+ "counterDigitsLabel": "Auffüllziffern",
+ "counterDigitsTooltip": "Gesamtzahl der Ziffern für den Zähler, mit führenden Nullen aufgefüllt (z.B. 3 für 001).",
+ "qualitySettingsTitle": "Qualitätseinstellungen",
+ "qualitySettingsDescription": "Wählen Sie Format und Komprimierungsstufe.",
+ "formatLabel": "Format",
+ "formatTooltip": "Wählen Sie das Ausgabedateiformat für die Bilder.",
+ "formatPlaceholder": "Format auswählen",
+ "formatOptions": {
+ "png": "PNG",
+ "jpeg": "JPEG",
+ "webp": "WEBP"
+ },
+ "qualityLabel": "Qualität",
+ "qualityTooltip": "Stellen Sie die Komprimierungsqualität für JPEG/WEBP ein. Höher ist bessere Qualität, aber größere Dateigröße.",
+ "qualityDisabledTooltip": "Der Qualitätsschieberegler ist für PNG (verlustfreies Format) deaktiviert.",
+ "reset": "Zurücksetzen",
+ "resetTooltip": "Alle Einstellungen auf ihre Standardwerte zurücksetzen.",
+ "apply": "Anwenden",
+ "applyTooltip": "Bestätigen und alle oben genannten Einstellungen anwenden. Dies lädt die Bilder nicht herunter.",
+ "toasts": {
+ "noImages": "Bitte laden Sie zuerst Bilder hoch.",
+ "imagesAdded": "{count, plural, one {# Bild hinzugefügt.} other {# Bilder hinzugefügt.}}",
+ "noValidImages": "Keine gültigen Bilddateien gefunden.",
+ "allCleared": "Alle Bilder gelöscht.",
+ "conversionStarting": "Konvertierung für {count, plural, one {# Bild wird gestartet...} other {# Bilder werden gestartet...}}",
+ "conversionSuccess": "Alle {count, plural, one {# Bild erfolgreich exportiert!} other {# Bilder erfolgreich exportiert!}}",
+ "conversionError": "Während der Konvertierung ist ein unbekannter Fehler aufgetreten.",
+ "singleConversionStarting": "Konvertierung für {filename} wird gestartet...",
+ "singleConversionSuccess": "{filename} erfolgreich exportiert!",
+ "settingsUpdated": "Einstellungen aktualisiert und werden für alle Downloads verwendet.",
+ "settingsReset": "Alle Einstellungen wurden auf ihre Standardwerte zurückgesetzt.",
+ "defaultBaseNameMissing": "Bitte geben Sie einen Standard-Basisnamen ein, um ihn anzuwenden.",
+ "uploadImagesFirst": "Laden Sie zuerst einige Bilder hoch.",
+ "baseNameApplied": "Basisname auf \"{name}\" für alle {count, plural, one {# Bild gesetzt.} other {# Bilder gesetzt.}}",
+ "errorProcessing": "Konnte {filename} nicht verarbeiten.",
+ "errorLoading": "Fehler beim Laden von {filename} für die Konvertierung."
+ }
+ },
+ "HomePage": {
+ "title": "Image Web Exporter",
+ "description": "Laden Sie ein Bild hoch und exportieren Sie es in einer anderen Auflösung und einem anderen Format."
+ },
+ "Footer": {
+ "imprint": "Impressum",
+ "privacy": "Datenschutz"
+ },
+ "ChangelogPage": {
+ "back": "Zurück zum Konverter",
+ "title": "Änderungsprotokoll",
+ "description": "Verfolgung aller neuen Funktionen, Verbesserungen und Fehlerbehebungen."
+ },
+ "ImprintPage": {
+ "back": "Zurück zum Konverter",
+ "title": "Impressum",
+ "tmgInfo": "Angaben gemäß § 5 TMG",
+ "contactInfoTitle": "Kontaktinformationen:",
+ "companyName": "[Ihr Firmenname]",
+ "addressLine1": "[Straße & Hausnummer]",
+ "addressLine2": "[PLZ & Stadt]",
+ "email": "E-Mail: [ihre-email@beispiel.com]",
+ "phone": "Telefon: [ihre-telefonnummer]",
+ "representedByTitle": "Vertreten durch:",
+ "representativeName": "[Ihr Name/Name des Geschäftsführers]",
+ "disclaimerTitle": "Haftungsausschluss:",
+ "disclaimerText": "Dies ist ein Musterimpressum und nicht rechtsverbindlich. Bitte ersetzen Sie den Platzhalterinhalt durch Ihre eigenen Informationen und konsultieren Sie einen Rechtsexperten, um die Einhaltung aller geltenden Gesetze sicherzustellen."
+ },
+ "PrivacyPage": {
+ "back": "Zurück zum Konverter",
+ "title": "Datenschutzerklärung",
+ "generalInfoTitle": "1. Allgemeine Informationen",
+ "generalInfoText": "Dies ist ein Platzhalter für Ihre Datenschutzerklärung. Sie beschreibt, wie personenbezogene Daten erfasst, verwendet und geschützt werden, wenn Sie diese Website nutzen.",
+ "dataCollectionTitle": "2. Datenerfassung auf dieser Website",
+ "dataCollectionText": "Die gesamte Bildverarbeitung findet direkt in Ihrem Browser statt. Die von Ihnen hochgeladenen Bilder werden nicht an einen Server gesendet und von uns nicht gespeichert. Wir erheben keine personenbezogenen Daten aus den Bildern.",
+ "yourRightsTitle": "3. Ihre Rechte",
+ "yourRightsText": "Da keine personenbezogenen Daten erhoben werden, sind Rechte auf Auskunft, Berichtigung oder Löschung personenbezogener Daten in diesem Zusammenhang nicht anwendbar.",
+ "disclaimerTitle": "Haftungsausschluss:",
+ "disclaimerText": "Dies ist eine Muster-Datenschutzerklärung und nicht rechtsverbindlich. Es ist entscheidend, diesen Text an Ihre spezifischen Datenverarbeitungsaktivitäten anzupassen und einen Rechtsexperten zu konsultieren, um die vollständige DSGVO-Konformität sicherzustellen."
+ }
+}
\ No newline at end of file
diff --git a/messages/en.json b/messages/en.json
new file mode 100644
index 0000000..914caff
--- /dev/null
+++ b/messages/en.json
@@ -0,0 +1,143 @@
+{
+ "ImageConverter": {
+ "uploadTitle": "Upload Images",
+ "uploadInstructions": "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}",
+ "downloadSingleTooltip": "Download this image",
+ "removeSingleTooltip": "Remove this image",
+ "imageSettingsTitle": "Image Settings",
+ "imageSettingsDescription": "Adjust resolution and scaling for all images.",
+ "aspectRatioLabel": "Aspect Ratio",
+ "aspectRatioTooltip": "Choose a preset aspect ratio or select 'Custom' to enter dimensions manually.",
+ "aspectRatioPlaceholder": "Select aspect ratio",
+ "aspectRatios": {
+ "custom": "Custom",
+ "square": "1:1 (Square)",
+ "standard": "4:3 (Standard)",
+ "photography": "3:2 (Photography)",
+ "widescreen": "16:9 (Widescreen)"
+ },
+ "widthLabel": "Width (px)",
+ "widthTooltip": "Set the output width in pixels. Leave blank to use the original width.",
+ "widthPlaceholder": "Auto",
+ "swapDimensionsTooltip": "Swap the entered width and height values.",
+ "heightLabel": "Height (px)",
+ "heightTooltip": "Set the output height in pixels. Leave blank to use the original height.",
+ "heightPlaceholder": "Auto",
+ "keepOrientationLabel": "Keep original orientation",
+ "keepOrientationTooltip": "Automatically swaps width and height to match the original image's orientation.",
+ "scalingLabel": "Scaling",
+ "scalingTooltip": "Determines how the image fits into the new dimensions.",
+ "scalingPlaceholder": "Select scaling mode",
+ "scalingOptions": {
+ "fill": "Fill (stretch to fit)",
+ "cover": "Cover (crop to fit)",
+ "contain": "Contain (letterbox)"
+ },
+ "positionLabel": "Position",
+ "positionTooltip": "Sets the anchor point for 'Cover' or 'Contain' scaling.",
+ "filenameSettingsTitle": "Filename Settings",
+ "filenameSettingsDescription": "Customize the output filenames.",
+ "useDefaultBaseNameLabel": "Use default base name",
+ "useDefaultBaseNameTooltip": "When enabled, all newly uploaded images will use the specified default base name.",
+ "defaultBaseNameLabel": "Default base name",
+ "defaultBaseNamePlaceholder": "e.g., new-york-trip",
+ "applyToAll": "Apply to all",
+ "applyToAllTooltip": "Apply this base name to all currently uploaded images.",
+ "prefixLabel": "Prefix",
+ "prefixTooltip": "Add text to the beginning of every filename.",
+ "prefixPlaceholder": "e.g., travel-",
+ "suffixLabel": "Suffix",
+ "suffixTooltip": "Add text to the end of every filename (before the number).",
+ "suffixPlaceholder": "e.g., -edit",
+ "useCounterLabel": "Add sequential number",
+ "useCounterTooltip": "Append a numbered sequence to each filename.",
+ "counterStartLabel": "Start number",
+ "counterStartTooltip": "The first number to use in the sequence.",
+ "counterDigitsLabel": "Padding digits",
+ "counterDigitsTooltip": "Total number of digits for the counter, padded with leading zeros (e.g., 3 for 001).",
+ "qualitySettingsTitle": "Quality Settings",
+ "qualitySettingsDescription": "Choose format and compression level.",
+ "formatLabel": "Format",
+ "formatTooltip": "Choose the output file format for the images.",
+ "formatPlaceholder": "Select format",
+ "formatOptions": {
+ "png": "PNG",
+ "jpeg": "JPEG",
+ "webp": "WEBP"
+ },
+ "qualityLabel": "Quality",
+ "qualityTooltip": "Set compression quality for JPEG/WEBP. Higher is better quality but larger file size.",
+ "qualityDisabledTooltip": "Quality slider is disabled for PNG (lossless format).",
+ "reset": "Reset",
+ "resetTooltip": "Reset all settings to their default values.",
+ "apply": "Apply",
+ "applyTooltip": "Confirm and apply all the settings above. This does not download the images.",
+ "toasts": {
+ "noImages": "Please upload images first.",
+ "imagesAdded": "{count, plural, one {# image added.} other {# images added.}}",
+ "noValidImages": "No valid image files found.",
+ "allCleared": "All images cleared.",
+ "conversionStarting": "Starting conversion for {count, plural, one {# image...} other {# images...}}",
+ "conversionSuccess": "Successfully exported all {count, plural, one {# image!} other {# images!}}",
+ "conversionError": "An unknown error occurred during conversion.",
+ "singleConversionStarting": "Starting conversion for {filename}...",
+ "singleConversionSuccess": "Successfully exported {filename}!",
+ "settingsUpdated": "Settings updated and will be used for all downloads.",
+ "settingsReset": "All settings have been reset to their defaults.",
+ "defaultBaseNameMissing": "Please enter a default base name to apply.",
+ "uploadImagesFirst": "Upload some images first.",
+ "baseNameApplied": "Set base name to \"{name}\" for all {count, plural, one {# image.} other {# images.}}",
+ "errorProcessing": "Could not process {filename}.",
+ "errorLoading": "Failed to load {filename} for conversion."
+ }
+ },
+ "HomePage": {
+ "title": "Image Web Exporter",
+ "description": "Upload a picture, then export it in a different resolution and format."
+ },
+ "Footer": {
+ "imprint": "Imprint",
+ "privacy": "Privacy"
+ },
+ "ChangelogPage": {
+ "back": "Back to Converter",
+ "title": "Changelog",
+ "description": "Tracking all the new features, improvements, and bug fixes."
+ },
+ "ImprintPage": {
+ "back": "Back to Converter",
+ "title": "Imprint",
+ "tmgInfo": "Information according to § 5 TMG (German Telemedia Act)",
+ "contactInfoTitle": "Contact Information:",
+ "companyName": "[Your Company Name]",
+ "addressLine1": "[Street Name & Number]",
+ "addressLine2": "[Postal Code & City]",
+ "email": "Email: [your-email@example.com]",
+ "phone": "Phone: [your-phone-number]",
+ "representedByTitle": "Represented by:",
+ "representativeName": "[Your Name/CEO's Name]",
+ "disclaimerTitle": "Disclaimer:",
+ "disclaimerText": "This is a sample imprint and not legally binding. Please replace the placeholder content with your own information and consult a legal professional to ensure compliance with all applicable laws."
+ },
+ "PrivacyPage": {
+ "back": "Back to Converter",
+ "title": "Data Privacy Policy",
+ "generalInfoTitle": "1. General Information",
+ "generalInfoText": "This is a placeholder for your data privacy policy. It outlines how personal data is collected, used, and protected when you use this website.",
+ "dataCollectionTitle": "2. Data Collection on This Website",
+ "dataCollectionText": "All image processing happens directly in your browser. The images you upload are not sent to any server and are not stored by us. We do not collect any personal data from the images.",
+ "yourRightsTitle": "3. Your Rights",
+ "yourRightsText": "As no personal data is collected, rights regarding access, rectification, or erasure of personal data are not applicable in this context.",
+ "disclaimerTitle": "Disclaimer:",
+ "disclaimerText": "This is a sample privacy policy and not legally binding. It is crucial to adapt this text to your specific data processing activities and to consult with a legal professional to ensure full GDPR compliance."
+ }
+}
\ 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/changelog/page.tsx b/src/app/[locale]/changelog/page.tsx
similarity index 85%
rename from src/app/changelog/page.tsx
rename to src/app/[locale]/changelog/page.tsx
index ba7c4dd..9f00ded 100644
--- a/src/app/changelog/page.tsx
+++ b/src/app/[locale]/changelog/page.tsx
@@ -2,15 +2,17 @@ import { Changelog } from "@/components/changelog";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react";
+import { useTranslations } from "next-intl";
export default function ChangelogPage() {
+ const t = useTranslations("ChangelogPage");
return (
diff --git a/src/app/imprint/page.tsx b/src/app/[locale]/imprint/page.tsx
similarity index 50%
rename from src/app/imprint/page.tsx
rename to src/app/[locale]/imprint/page.tsx
index 4b252f5..eccbc1b 100644
--- a/src/app/imprint/page.tsx
+++ b/src/app/[locale]/imprint/page.tsx
@@ -2,43 +2,47 @@ import Link from "next/link";
import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { useTranslations } from "next-intl";
export default function ImprintPage() {
+ const t = useTranslations("ImprintPage");
return (
- Imprint
+ {t("title")}
-
- Information according to § 5 TMG (German Telemedia Act)
-
+
{t("tmgInfo")}
-
Contact Information:
-
[Your Company Name]
-
[Street Name & Number]
-
[Postal Code & City]
-
Email: [your-email@example.com]
-
Phone: [your-phone-number]
+
+ {t("contactInfoTitle")}
+
+
{t("companyName")}
+
{t("addressLine1")}
+
{t("addressLine2")}
+
{t("email")}
+
{t("phone")}
-
Represented by:
-
[Your Name/CEO's Name]
+
+ {t("representedByTitle")}
+
+
{t("representativeName")}
-
Disclaimer:
-
- This is a sample imprint and not legally binding. Please replace the placeholder content with your own information and consult a legal professional to ensure compliance with all applicable laws.
-
+
+ {t("disclaimerTitle")}
+
+
{t("disclaimerText")}
diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx
new file mode 100644
index 0000000..8a7b174
--- /dev/null
+++ b/src/app/[locale]/layout.tsx
@@ -0,0 +1,55 @@
+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 { Footer } from "@/components/footer";
+import { NextIntlClientProvider } from "next-intl";
+import { getMessages } from "next-intl/server";
+
+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 async function RootLayout({
+ children,
+ params: { locale },
+}: Readonly<{
+ children: React.ReactNode;
+ params: { locale: string };
+}>) {
+ const messages = await getMessages();
+
+ return (
+
+
+
+
+ {children}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/page.tsx b/src/app/[locale]/page.tsx
similarity index 82%
rename from src/app/page.tsx
rename to src/app/[locale]/page.tsx
index 42403e1..1d98281 100644
--- a/src/app/page.tsx
+++ b/src/app/[locale]/page.tsx
@@ -1,15 +1,17 @@
import { ImageConverter } from "@/components/image-converter";
+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/[locale]/privacy/page.tsx b/src/app/[locale]/privacy/page.tsx
new file mode 100644
index 0000000..a5c8236
--- /dev/null
+++ b/src/app/[locale]/privacy/page.tsx
@@ -0,0 +1,54 @@
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+import { ArrowLeft } from "lucide-react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { useTranslations } from "next-intl";
+
+export default function PrivacyPage() {
+ const t = useTranslations("PrivacyPage");
+ return (
+
+
+
+
+
+
+ {t("title")}
+
+
+
+
+ {t("generalInfoTitle")}
+
+
{t("generalInfoText")}
+
+
+
+ {t("dataCollectionTitle")}
+
+
{t("dataCollectionText")}
+
+
+
+ {t("yourRightsTitle")}
+
+
{t("yourRightsText")}
+
+
+
+ {t("disclaimerTitle")}
+
+
{t("disclaimerText")}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
deleted file mode 100644
index 0aaa019..0000000
--- a/src/app/layout.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-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 { Footer } from "@/components/footer";
-
-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/privacy/page.tsx b/src/app/privacy/page.tsx
deleted file mode 100644
index 8469cdd..0000000
--- a/src/app/privacy/page.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import Link from "next/link";
-import { Button } from "@/components/ui/button";
-import { ArrowLeft } from "lucide-react";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-
-export default function PrivacyPage() {
- return (
-
-
-
-
-
-
- Data Privacy Policy
-
-
-
-
1. General Information
-
- This is a placeholder for your data privacy policy. It outlines how personal data is collected, used, and protected when you use this website.
-
-
-
-
2. Data Collection on This Website
-
- All image processing happens directly in your browser. The images you upload are not sent to any server and are not stored by us. We do not collect any personal data from the images.
-
-
-
-
3. Your Rights
-
- As no personal data is collected, rights regarding access, rectification, or erasure of personal data are not applicable in this context.
-
-
-
-
Disclaimer:
-
- This is a sample privacy policy and not legally binding. It is crucial to adapt this text to your specific data processing activities and to consult with a legal professional to ensure full GDPR compliance.
-
-
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/src/components/changelog.tsx b/src/components/changelog.tsx
index 3de260e..81ae1e4 100644
--- a/src/components/changelog.tsx
+++ b/src/components/changelog.tsx
@@ -4,16 +4,18 @@ 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 { useTranslations } from "next-intl";
export function Changelog() {
+ const t = useTranslations("ChangelogPage");
return (
- Changelog
+ {t("title")}
- Tracking all the new features, improvements, and bug fixes.
+ {t("description")}
diff --git a/src/components/footer.tsx b/src/components/footer.tsx
index e63ab3a..d37c8ab 100644
--- a/src/components/footer.tsx
+++ b/src/components/footer.tsx
@@ -2,8 +2,11 @@ import Link from "next/link";
import { Github, Twitter } from "lucide-react";
import { Button } from "@/components/ui/button";
import { changelogData } from "@/lib/changelog-data";
+import { useTranslations } from "next-intl";
+import LanguageSwitcher from "./language-switcher";
export function Footer() {
+ const t = useTranslations("Footer");
const latestVersion = changelogData[0]?.version;
return (
@@ -14,6 +17,7 @@ export function Footer() {
+
- Imprint
- Privacy
+ {t("imprint")}
+ {t("privacy")}
{latestVersion && (
([]);
const [previewUrls, setPreviewUrls] = useState([]);
const [filenames, setFilenames] = useState([]);
@@ -104,7 +107,7 @@ export function ImageConverter() {
);
if (imageFiles.length === 0) {
- toast.error("No valid image files found.");
+ toast.error(t("toasts.noValidImages"));
return;
}
@@ -126,7 +129,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) => {
@@ -168,7 +171,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) => {
@@ -286,23 +289,23 @@ export function ImageConverter() {
document.body.removeChild(link);
resolve();
} else {
- reject(new Error(`Could not process ${image.name}.`));
+ reject(new Error(t("toasts.errorProcessing", { filename: image.name })));
}
};
img.onerror = () => {
- reject(new Error(`Failed to load ${image.name} for conversion.`));
+ reject(new Error(t("toasts.errorLoading", { filename: image.name })));
};
});
};
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)
@@ -310,12 +313,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);
@@ -324,16 +327,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);
@@ -341,7 +344,7 @@ export function ImageConverter() {
};
const handleApplySettings = () => {
- toast.info("Settings updated and will be used for all downloads.");
+ toast.info(t("toasts.settingsUpdated"));
};
const handleResetSettings = () => {
@@ -360,7 +363,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) => {
@@ -408,16 +411,16 @@ export function ImageConverter() {
const handleApplyDefaultBaseNameToAll = () => {
if (!defaultBaseName) {
- toast.error("Please enter a default base name to apply.");
+ toast.error(t("toasts.defaultBaseNameMissing"));
return;
}
if (!hasImages) {
- toast.info("Upload some images first.");
+ toast.info(t("toasts.uploadImagesFirst"));
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", { name: defaultBaseName, count: images.length }));
};
return (
@@ -427,7 +430,7 @@ export function ImageConverter() {
-
Upload Images
+
{t("uploadTitle")}
-
Click or drag and drop to upload
-
PNG, JPG, WEBP supported
+
{t("uploadInstructions")}
+
{t("uploadHint")}
@@ -453,25 +456,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")}
@@ -485,7 +488,7 @@ export function ImageConverter() {
-
+
- Final name: {finalFilename}.{format}
+ {t("finalNameLabel", { filename: `${finalFilename}.${format}` })}
@@ -510,7 +513,7 @@ export function ImageConverter() {
-
Download this image
+
{t("downloadSingleTooltip")}
@@ -526,7 +529,7 @@ export function ImageConverter() {
-
Remove this image
+
{t("removeSingleTooltip")}
@@ -544,9 +547,9 @@ export function ImageConverter() {
-
Image Settings
+
{t("imageSettingsTitle")}
- Adjust resolution and scaling for all images.
+ {t("imageSettingsDescription")}
@@ -554,19 +557,19 @@ export function ImageConverter() {
-
+
-
Choose a preset aspect ratio or select 'Custom' to enter dimensions manually.