[dyad] Adding internationalization with next-intl - wrote 19 file(s), renamed 5 file(s), added next-intl package(s)
This commit is contained in:
100
messages/en.json
Normal file
100
messages/en.json
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
{
|
||||||
|
"HomePage": {
|
||||||
|
"title": "Image Web Exporter",
|
||||||
|
"subtitle": "Upload a picture, then export it in a different resolution and format."
|
||||||
|
},
|
||||||
|
"ImageUploadArea": {
|
||||||
|
"title": "Upload Images",
|
||||||
|
"prompt": "Click or drag and drop to upload",
|
||||||
|
"supportedFormats": "PNG, JPG, WEBP supported"
|
||||||
|
},
|
||||||
|
"ImageList": {
|
||||||
|
"title": "Uploaded Images",
|
||||||
|
"clearAll": "Clear All",
|
||||||
|
"clearAllTooltip": "Remove all uploaded images.",
|
||||||
|
"downloadAll": "Download All ({count})",
|
||||||
|
"downloadAllDefault": "Download All",
|
||||||
|
"converting": "Converting...",
|
||||||
|
"downloadAllTooltip": "Convert and download all images with the current settings."
|
||||||
|
},
|
||||||
|
"ImageListItem": {
|
||||||
|
"baseName": "Base Name",
|
||||||
|
"finalName": "Final name: {finalFilename}.{format}",
|
||||||
|
"downloadTooltip": "Download this image",
|
||||||
|
"removeTooltip": "Remove this image"
|
||||||
|
},
|
||||||
|
"SettingsPanel": {
|
||||||
|
"imageSettingsTitle": "Image Settings",
|
||||||
|
"imageSettingsSubtitle": "Adjust resolution and scaling for all images.",
|
||||||
|
"filenameSettingsTitle": "Filename Settings",
|
||||||
|
"filenameSettingsSubtitle": "Customize the output filenames.",
|
||||||
|
"qualitySettingsTitle": "Quality Settings",
|
||||||
|
"qualitySettingsSubtitle": "Choose format and compression level."
|
||||||
|
},
|
||||||
|
"ImageSettings": {
|
||||||
|
"aspectRatio": "Aspect Ratio",
|
||||||
|
"aspectRatioTooltip": "Choose a preset aspect ratio or select 'Custom' to enter dimensions manually.",
|
||||||
|
"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.",
|
||||||
|
"swapTooltip": "Swap the entered width and height values.",
|
||||||
|
"height": "Height (px)",
|
||||||
|
"heightTooltip": "Set the output height in pixels. Leave blank to use the original height.",
|
||||||
|
"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.",
|
||||||
|
"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."
|
||||||
|
},
|
||||||
|
"FilenameSettings": {
|
||||||
|
"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)."
|
||||||
|
},
|
||||||
|
"QualitySettings": {
|
||||||
|
"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.",
|
||||||
|
"pngWarning": "Quality slider is disabled for PNG (lossless format)."
|
||||||
|
},
|
||||||
|
"ActionButtons": {
|
||||||
|
"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."
|
||||||
|
},
|
||||||
|
"Footer": {
|
||||||
|
"imprint": "Imprint",
|
||||||
|
"privacy": "Privacy"
|
||||||
|
},
|
||||||
|
"ChangelogPage": {
|
||||||
|
"back": "Back to Converter"
|
||||||
|
},
|
||||||
|
"ImprintPage": {
|
||||||
|
"back": "Back to Converter"
|
||||||
|
},
|
||||||
|
"PrivacyPage": {
|
||||||
|
"back": "Back to Converter"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,7 @@
|
|||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.511.0",
|
"lucide-react": "^0.511.0",
|
||||||
"next": "15.3.8",
|
"next": "15.3.8",
|
||||||
|
"next-intl": "^4.7.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.1",
|
"react": "^19.2.1",
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
|
|||||||
403
pnpm-lock.yaml
generated
403
pnpm-lock.yaml
generated
@@ -113,6 +113,9 @@ importers:
|
|||||||
next:
|
next:
|
||||||
specifier: 15.3.8
|
specifier: 15.3.8
|
||||||
version: 15.3.8(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
|
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:
|
next-themes:
|
||||||
specifier: ^0.4.6
|
specifier: ^0.4.6
|
||||||
version: 0.4.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
|
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':
|
'@floating-ui/utils@0.2.9':
|
||||||
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
|
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':
|
'@hookform/resolvers@5.0.1':
|
||||||
resolution: {integrity: sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==}
|
resolution: {integrity: sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -431,6 +452,88 @@ packages:
|
|||||||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||||
engines: {node: '>= 8'}
|
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':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -1047,15 +1150,90 @@ packages:
|
|||||||
'@radix-ui/rect@1.1.1':
|
'@radix-ui/rect@1.1.1':
|
||||||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
||||||
|
|
||||||
|
'@schummar/icu-type-parser@1.21.5':
|
||||||
|
resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==}
|
||||||
|
|
||||||
'@standard-schema/utils@0.3.0':
|
'@standard-schema/utils@0.3.0':
|
||||||
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
|
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':
|
'@swc/counter@0.1.3':
|
||||||
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
|
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
|
||||||
|
|
||||||
'@swc/helpers@0.5.15':
|
'@swc/helpers@0.5.15':
|
||||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
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':
|
'@types/d3-array@3.2.1':
|
||||||
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
|
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
|
||||||
|
|
||||||
@@ -1348,6 +1526,9 @@ packages:
|
|||||||
decimal.js-light@2.5.1:
|
decimal.js-light@2.5.1:
|
||||||
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
|
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
|
||||||
|
|
||||||
|
decimal.js@10.6.0:
|
||||||
|
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
|
||||||
|
|
||||||
detect-libc@2.0.4:
|
detect-libc@2.0.4:
|
||||||
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
|
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1499,6 +1680,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
intl-messageformat@10.7.18:
|
||||||
|
resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==}
|
||||||
|
|
||||||
is-arrayish@0.3.2:
|
is-arrayish@0.3.2:
|
||||||
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
|
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
|
||||||
|
|
||||||
@@ -1613,9 +1797,26 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
negotiator@1.0.0:
|
||||||
|
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
neo-async@2.6.2:
|
neo-async@2.6.2:
|
||||||
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
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:
|
next-themes@0.4.6:
|
||||||
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
|
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1643,6 +1844,9 @@ packages:
|
|||||||
sass:
|
sass:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
node-addon-api@7.1.1:
|
||||||
|
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||||
|
|
||||||
node-releases@2.0.19:
|
node-releases@2.0.19:
|
||||||
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
|
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
|
||||||
|
|
||||||
@@ -1679,6 +1883,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||||
engines: {node: '>=8.6'}
|
engines: {node: '>=8.6'}
|
||||||
|
|
||||||
|
picomatch@4.0.3:
|
||||||
|
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
pify@2.3.0:
|
pify@2.3.0:
|
||||||
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -1687,6 +1895,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
po-parser@2.1.1:
|
||||||
|
resolution: {integrity: sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==}
|
||||||
|
|
||||||
postcss-import@15.1.0:
|
postcss-import@15.1.0:
|
||||||
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
|
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
@@ -2031,6 +2242,11 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
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:
|
use-sidecar@1.1.3:
|
||||||
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
|
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -2145,6 +2361,36 @@ snapshots:
|
|||||||
|
|
||||||
'@floating-ui/utils@0.2.9': {}
|
'@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))':
|
'@hookform/resolvers@5.0.1(react-hook-form@7.56.4(react@19.2.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@standard-schema/utils': 0.3.0
|
'@standard-schema/utils': 0.3.0
|
||||||
@@ -2300,6 +2546,66 @@ snapshots:
|
|||||||
'@nodelib/fs.scandir': 2.1.5
|
'@nodelib/fs.scandir': 2.1.5
|
||||||
fastq: 1.19.1
|
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':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -2956,14 +3262,66 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/rect@1.1.1': {}
|
'@radix-ui/rect@1.1.1': {}
|
||||||
|
|
||||||
|
'@schummar/icu-type-parser@1.21.5': {}
|
||||||
|
|
||||||
'@standard-schema/utils@0.3.0': {}
|
'@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/counter@0.1.3': {}
|
||||||
|
|
||||||
'@swc/helpers@0.5.15':
|
'@swc/helpers@0.5.15':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@swc/types@0.1.25':
|
||||||
|
dependencies:
|
||||||
|
'@swc/counter': 0.1.3
|
||||||
|
|
||||||
'@types/d3-array@3.2.1': {}
|
'@types/d3-array@3.2.1': {}
|
||||||
|
|
||||||
'@types/d3-color@3.1.3': {}
|
'@types/d3-color@3.1.3': {}
|
||||||
@@ -3272,8 +3630,9 @@ snapshots:
|
|||||||
|
|
||||||
decimal.js-light@2.5.1: {}
|
decimal.js-light@2.5.1: {}
|
||||||
|
|
||||||
detect-libc@2.0.4:
|
decimal.js@10.6.0: {}
|
||||||
optional: true
|
|
||||||
|
detect-libc@2.0.4: {}
|
||||||
|
|
||||||
detect-node-es@1.1.0: {}
|
detect-node-es@1.1.0: {}
|
||||||
|
|
||||||
@@ -3402,6 +3761,13 @@ snapshots:
|
|||||||
|
|
||||||
internmap@2.0.3: {}
|
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:
|
is-arrayish@0.3.2:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -3496,8 +3862,28 @@ snapshots:
|
|||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
|
negotiator@1.0.0: {}
|
||||||
|
|
||||||
neo-async@2.6.2: {}
|
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):
|
next-themes@0.4.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.1
|
react: 19.2.1
|
||||||
@@ -3528,6 +3914,8 @@ snapshots:
|
|||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
|
|
||||||
|
node-addon-api@7.1.1: {}
|
||||||
|
|
||||||
node-releases@2.0.19: {}
|
node-releases@2.0.19: {}
|
||||||
|
|
||||||
normalize-path@3.0.0: {}
|
normalize-path@3.0.0: {}
|
||||||
@@ -3551,10 +3939,14 @@ snapshots:
|
|||||||
|
|
||||||
picomatch@2.3.1: {}
|
picomatch@2.3.1: {}
|
||||||
|
|
||||||
|
picomatch@4.0.3: {}
|
||||||
|
|
||||||
pify@2.3.0: {}
|
pify@2.3.0: {}
|
||||||
|
|
||||||
pirates@4.0.7: {}
|
pirates@4.0.7: {}
|
||||||
|
|
||||||
|
po-parser@2.1.1: {}
|
||||||
|
|
||||||
postcss-import@15.1.0(postcss@8.5.3):
|
postcss-import@15.1.0(postcss@8.5.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
postcss: 8.5.3
|
postcss: 8.5.3
|
||||||
@@ -3921,6 +4313,13 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.5
|
'@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):
|
use-sidecar@1.1.3(@types/react@19.1.5)(react@19.2.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
detect-node-es: 1.1.0
|
detect-node-es: 1.1.0
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { Changelog } from "@/components/changelog";
|
import { Changelog } from "@/components/changelog";
|
||||||
import Link from "next/link";
|
import Link from "next-intl/link";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ArrowLeft } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export default function ChangelogPage() {
|
export default function ChangelogPage() {
|
||||||
|
const t = useTranslations("ChangelogPage");
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col items-center min-h-screen p-4 sm:p-8 bg-gray-50 dark:bg-background font-[family-name:var(--font-geist-sans)]">
|
<div className="relative flex flex-col items-center min-h-screen p-4 sm:p-8 bg-gray-50 dark:bg-background font-[family-name:var(--font-geist-sans)]">
|
||||||
<div className="w-full max-w-4xl mx-auto">
|
<div className="w-full max-w-4xl mx-auto">
|
||||||
<Button asChild variant="ghost" className="mb-4 -ml-4">
|
<Button asChild variant="ghost" className="mb-4 -ml-4">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
Back to Converter
|
{t('back')}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
import Link from "next/link";
|
import Link from "next-intl/link";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ArrowLeft } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export default function ImprintPage() {
|
export default function ImprintPage() {
|
||||||
|
const t = useTranslations("ImprintPage");
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col items-center min-h-screen p-4 sm:p-8 bg-gray-50 dark:bg-background">
|
<div className="relative flex flex-col items-center min-h-screen p-4 sm:p-8 bg-gray-50 dark:bg-background">
|
||||||
<div className="w-full max-w-4xl mx-auto">
|
<div className="w-full max-w-4xl mx-auto">
|
||||||
<Button asChild variant="ghost" className="mb-4 -ml-4">
|
<Button asChild variant="ghost" className="mb-4 -ml-4">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
Back to Converter
|
{t('back')}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<main className="w-full">
|
<main className="w-full">
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "../globals.css";
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { Footer } from "@/components/footer";
|
import { Footer } from "@/components/footer";
|
||||||
|
import { NextIntlClientProvider } from 'next-intl';
|
||||||
|
import { getMessages } from 'next-intl/server';
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -20,16 +22,21 @@ export const metadata: Metadata = {
|
|||||||
description: "Upload a picture, then export it in a different resolution and format.",
|
description: "Upload a picture, then export it in a different resolution and format.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
|
params: { locale }
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
params: { locale: string };
|
||||||
}>) {
|
}>) {
|
||||||
|
const messages = await getMessages();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang={locale} suppressHydrationWarning>
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
|
<NextIntlClientProvider messages={messages}>
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
attribute="class"
|
attribute="class"
|
||||||
defaultTheme="system"
|
defaultTheme="system"
|
||||||
@@ -40,6 +47,7 @@ export default function RootLayout({
|
|||||||
<Footer />
|
<Footer />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
</NextIntlClientProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
import { ImageConverter } from "@/components/image-converter";
|
import { ImageConverter } from "@/components/image-converter";
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const t = useTranslations('HomePage');
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col items-center justify-center min-h-screen p-4 sm:p-8 bg-gray-50 dark:bg-background font-[family-name:var(--font-geist-sans)]">
|
<div className="relative flex flex-col items-center justify-center min-h-screen p-4 sm:p-8 bg-gray-50 dark:bg-background font-[family-name:var(--font-geist-sans)]">
|
||||||
<main className="flex flex-col items-center w-full max-w-6xl z-10">
|
<main className="flex flex-col items-center w-full max-w-6xl z-10">
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100 sm:text-5xl">
|
<h1 className="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100 sm:text-5xl">
|
||||||
Image Web Exporter
|
{t('title')}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-3 text-lg text-gray-600 dark:text-gray-400">
|
<p className="mt-3 text-lg text-gray-600 dark:text-gray-400">
|
||||||
Upload a picture, then export it in a different resolution and format.
|
{t('subtitle')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ImageConverter />
|
<ImageConverter />
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
import Link from "next/link";
|
import Link from "next-intl/link";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ArrowLeft } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export default function PrivacyPage() {
|
export default function PrivacyPage() {
|
||||||
|
const t = useTranslations("PrivacyPage");
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col items-center min-h-screen p-4 sm:p-8 bg-gray-50 dark:bg-background">
|
<div className="relative flex flex-col items-center min-h-screen p-4 sm:p-8 bg-gray-50 dark:bg-background">
|
||||||
<div className="w-full max-w-4xl mx-auto">
|
<div className="w-full max-w-4xl mx-auto">
|
||||||
<Button asChild variant="ghost" className="mb-4 -ml-4">
|
<Button asChild variant="ghost" className="mb-4 -ml-4">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
Back to Converter
|
{t('back')}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<main className="w-full">
|
<main className="w-full">
|
||||||
@@ -4,12 +4,14 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Check, RotateCcw } from "lucide-react";
|
import { Check, RotateCcw } from "lucide-react";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
interface ActionButtonsProps {
|
interface ActionButtonsProps {
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActionButtons({ onReset }: ActionButtonsProps) {
|
export function ActionButtons({ onReset }: ActionButtonsProps) {
|
||||||
|
const t = useTranslations("ActionButtons");
|
||||||
const handleApply = () => {
|
const handleApply = () => {
|
||||||
toast.info("Settings updated and will be used for all downloads.");
|
toast.info("Settings updated and will be used for all downloads.");
|
||||||
};
|
};
|
||||||
@@ -20,18 +22,18 @@ export function ActionButtons({ onReset }: ActionButtonsProps) {
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button onClick={onReset} className="w-full" variant="outline">
|
<Button onClick={onReset} className="w-full" variant="outline">
|
||||||
<RotateCcw className="mr-2 h-4 w-4" /> Reset
|
<RotateCcw className="mr-2 h-4 w-4" /> {t('reset')}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent><p>Reset all settings to their default values.</p></TooltipContent>
|
<TooltipContent><p>{t('resetTooltip')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button onClick={handleApply} className="w-full">
|
<Button onClick={handleApply} className="w-full">
|
||||||
<Check className="mr-2 h-4 w-4" /> Apply
|
<Check className="mr-2 h-4 w-4" /> {t('apply')}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent><p>Confirm and apply all the settings above. This does not download the images.</p></TooltipContent>
|
<TooltipContent><p>{t('applyTooltip')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import Link from "next/link";
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next-intl/link";
|
||||||
import { Github, Twitter } from "lucide-react";
|
import { Github, Twitter } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { changelogData } from "@/lib/changelog-data";
|
import { changelogData } from "@/lib/changelog-data";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export function Footer() {
|
export function Footer() {
|
||||||
|
const t = useTranslations("Footer");
|
||||||
const latestVersion = changelogData[0]?.version;
|
const latestVersion = changelogData[0]?.version;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -27,8 +31,8 @@ export function Footer() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
<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="/imprint" className="hover:text-primary transition-colors">{t('imprint')}</Link>
|
||||||
<Link href="/privacy" className="hover:text-primary transition-colors">Privacy</Link>
|
<Link href="/privacy" className="hover:text-primary transition-colors">{t('privacy')}</Link>
|
||||||
{latestVersion && (
|
{latestVersion && (
|
||||||
<Link
|
<Link
|
||||||
href="/changelog"
|
href="/changelog"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Download, X } from "lucide-react";
|
import { Download, X } from "lucide-react";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { generateFinalFilename } from "@/lib/image-processor";
|
import { generateFinalFilename } from "@/lib/image-processor";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
interface ImageListItemProps {
|
interface ImageListItemProps {
|
||||||
image: ImageFile;
|
image: ImageFile;
|
||||||
@@ -27,22 +28,24 @@ export function ImageListItem({
|
|||||||
onDownload,
|
onDownload,
|
||||||
isProcessing,
|
isProcessing,
|
||||||
}: ImageListItemProps) {
|
}: ImageListItemProps) {
|
||||||
|
const t = useTranslations("ImageListItem");
|
||||||
const finalFilename = generateFinalFilename(image.filename, settings, index);
|
const finalFilename = generateFinalFilename(image.filename, settings, index);
|
||||||
|
const finalNameText = t('finalName', { finalFilename, format: settings.format });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className="p-4 border rounded-lg flex items-center gap-4">
|
<div className="p-4 border rounded-lg flex items-center gap-4">
|
||||||
<img src={image.previewUrl} alt={`Preview ${index + 1}`} className="w-20 h-20 object-cover rounded-md shrink-0" />
|
<img src={image.previewUrl} alt={`Preview ${index + 1}`} className="w-20 h-20 object-cover rounded-md shrink-0" />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<Label htmlFor={`filename-${index}`} className="text-xs text-muted-foreground">Base Name</Label>
|
<Label htmlFor={`filename-${index}`} className="text-xs text-muted-foreground">{t('baseName')}</Label>
|
||||||
<Input
|
<Input
|
||||||
id={`filename-${index}`}
|
id={`filename-${index}`}
|
||||||
value={image.filename}
|
value={image.filename}
|
||||||
onChange={(e) => onFilenameChange(index, e.target.value)}
|
onChange={(e) => onFilenameChange(index, e.target.value)}
|
||||||
className="text-sm font-medium h-8 mt-1"
|
className="text-sm font-medium h-8 mt-1"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground truncate mt-1" title={`${finalFilename}.${settings.format}`}>
|
<p className="text-xs text-muted-foreground truncate mt-1" title={finalNameText}>
|
||||||
Final name: {finalFilename}.{settings.format}
|
{finalNameText}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center shrink-0">
|
<div className="flex items-center shrink-0">
|
||||||
@@ -58,7 +61,7 @@ export function ImageListItem({
|
|||||||
<Download className="h-4 w-4" />
|
<Download className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent><p>Download this image</p></TooltipContent>
|
<TooltipContent><p>{t('downloadTooltip')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -72,7 +75,7 @@ export function ImageListItem({
|
|||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent><p>Remove this image</p></TooltipContent>
|
<TooltipContent><p>{t('removeTooltip')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Download, Trash2 } from "lucide-react";
|
import { Download, Trash2 } from "lucide-react";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { ImageListItem } from "./image-list-item";
|
import { ImageListItem } from "./image-list-item";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
interface ImageListProps {
|
interface ImageListProps {
|
||||||
images: ImageFile[];
|
images: ImageFile[];
|
||||||
@@ -30,6 +31,8 @@ export function ImageList({
|
|||||||
isConverting,
|
isConverting,
|
||||||
convertingIndex,
|
convertingIndex,
|
||||||
}: ImageListProps) {
|
}: ImageListProps) {
|
||||||
|
const t = useTranslations("ImageList");
|
||||||
|
|
||||||
if (images.length === 0) {
|
if (images.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -41,24 +44,24 @@ export function ImageList({
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<CardTitle>Uploaded Images</CardTitle>
|
<CardTitle>{t('title')}</CardTitle>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button variant="ghost" size="sm" onClick={onClearAll} disabled={isProcessing}>
|
<Button variant="ghost" size="sm" onClick={onClearAll} disabled={isProcessing}>
|
||||||
<Trash2 className="mr-2 h-4 w-4" />Clear All
|
<Trash2 className="mr-2 h-4 w-4" />{t('clearAll')}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent><p>Remove all uploaded images.</p></TooltipContent>
|
<TooltipContent><p>{t('clearAllTooltip')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button onClick={onDownloadAll} disabled={isProcessing}>
|
<Button onClick={onDownloadAll} disabled={isProcessing}>
|
||||||
<Download className="mr-2 h-4 w-4" />
|
<Download className="mr-2 h-4 w-4" />
|
||||||
{isConverting ? "Converting..." : `Download All (${images.length})`}
|
{isConverting ? t('converting') : t('downloadAll', { count: images.length })}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent><p>Convert and download all images with the current settings.</p></TooltipContent>
|
<TooltipContent><p>{t('downloadAllTooltip')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Upload } from "lucide-react";
|
import { Upload } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Card, CardContent } from "./ui/card";
|
import { Card, CardContent } from "./ui/card";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
interface ImageUploadAreaProps {
|
interface ImageUploadAreaProps {
|
||||||
onFilesSelected: (files: FileList | null) => void;
|
onFilesSelected: (files: FileList | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ImageUploadArea({ onFilesSelected }: ImageUploadAreaProps) {
|
export function ImageUploadArea({ onFilesSelected }: ImageUploadAreaProps) {
|
||||||
|
const t = useTranslations("ImageUploadArea");
|
||||||
const [isDraggingOver, setIsDraggingOver] = useState(false);
|
const [isDraggingOver, setIsDraggingOver] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ export function ImageUploadArea({ onFilesSelected }: ImageUploadAreaProps) {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-lg font-medium">Upload Images</h3>
|
<h3 className="text-lg font-medium">{t('title')}</h3>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full h-48 rounded-lg border-2 border-dashed flex items-center justify-center relative transition-colors cursor-pointer hover:border-primary/60",
|
"w-full h-48 rounded-lg border-2 border-dashed flex items-center justify-center relative transition-colors cursor-pointer hover:border-primary/60",
|
||||||
@@ -52,8 +54,8 @@ export function ImageUploadArea({ onFilesSelected }: ImageUploadAreaProps) {
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col items-center justify-center text-center text-muted-foreground">
|
<div className="flex flex-col items-center justify-center text-center text-muted-foreground">
|
||||||
<Upload className="w-8 h-8 mb-2" />
|
<Upload className="w-8 h-8 mb-2" />
|
||||||
<p className="font-semibold">Click or drag and drop to upload</p>
|
<p className="font-semibold">{t('prompt')}</p>
|
||||||
<p className="text-xs text-muted-foreground mt-1">PNG, JPG, WEBP supported</p>
|
<p className="text-xs text-muted-foreground mt-1">{t('supportedFormats')}</p>
|
||||||
</div>
|
</div>
|
||||||
<Input type="file" ref={fileInputRef} onChange={handleImageChange} className="hidden" accept="image/*" multiple />
|
<Input type="file" ref={fileInputRef} onChange={handleImageChange} className="hidden" accept="image/*" multiple />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/
|
|||||||
import { ImageSettings } from "./settings/image-settings";
|
import { ImageSettings } from "./settings/image-settings";
|
||||||
import { FilenameSettings } from "./settings/filename-settings";
|
import { FilenameSettings } from "./settings/filename-settings";
|
||||||
import { QualitySettings } from "./settings/quality-settings";
|
import { QualitySettings } from "./settings/quality-settings";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
interface SettingsPanelProps {
|
interface SettingsPanelProps {
|
||||||
settings: ConversionSettings;
|
settings: ConversionSettings;
|
||||||
@@ -23,13 +24,14 @@ export function SettingsPanel({
|
|||||||
onApplyDefaultBaseNameToAll,
|
onApplyDefaultBaseNameToAll,
|
||||||
hasImages,
|
hasImages,
|
||||||
}: SettingsPanelProps) {
|
}: SettingsPanelProps) {
|
||||||
|
const t = useTranslations("SettingsPanel");
|
||||||
return (
|
return (
|
||||||
<Accordion type="single" collapsible defaultValue="image-settings" className="w-full space-y-4">
|
<Accordion type="single" collapsible defaultValue="image-settings" className="w-full space-y-4">
|
||||||
<AccordionItem value="image-settings" className="border rounded-lg bg-card">
|
<AccordionItem value="image-settings" className="border rounded-lg bg-card">
|
||||||
<AccordionTrigger className="p-6 hover:no-underline">
|
<AccordionTrigger className="p-6 hover:no-underline">
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<h3 className="text-lg font-medium leading-none">Image Settings</h3>
|
<h3 className="text-lg font-medium leading-none">{t('imageSettingsTitle')}</h3>
|
||||||
<p className="text-sm text-muted-foreground mt-1">Adjust resolution and scaling for all images.</p>
|
<p className="text-sm text-muted-foreground mt-1">{t('imageSettingsSubtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent className="px-6 pb-6">
|
<AccordionContent className="px-6 pb-6">
|
||||||
@@ -45,8 +47,8 @@ export function SettingsPanel({
|
|||||||
<AccordionItem value="filename-settings" className="border rounded-lg bg-card">
|
<AccordionItem value="filename-settings" className="border rounded-lg bg-card">
|
||||||
<AccordionTrigger className="p-6 hover:no-underline">
|
<AccordionTrigger className="p-6 hover:no-underline">
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<h3 className="text-lg font-medium leading-none">Filename Settings</h3>
|
<h3 className="text-lg font-medium leading-none">{t('filenameSettingsTitle')}</h3>
|
||||||
<p className="text-sm text-muted-foreground mt-1">Customize the output filenames.</p>
|
<p className="text-sm text-muted-foreground mt-1">{t('filenameSettingsSubtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent className="px-6 pb-6">
|
<AccordionContent className="px-6 pb-6">
|
||||||
@@ -62,8 +64,8 @@ export function SettingsPanel({
|
|||||||
<AccordionItem value="quality-settings" className="border rounded-lg bg-card">
|
<AccordionItem value="quality-settings" className="border rounded-lg bg-card">
|
||||||
<AccordionTrigger className="p-6 hover:no-underline">
|
<AccordionTrigger className="p-6 hover:no-underline">
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<h3 className="text-lg font-medium leading-none">Quality Settings</h3>
|
<h3 className="text-lg font-medium leading-none">{t('qualitySettingsTitle')}</h3>
|
||||||
<p className="text-sm text-muted-foreground mt-1">Choose format and compression level.</p>
|
<p className="text-sm text-muted-foreground mt-1">{t('qualitySettingsSubtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent className="px-6 pb-6">
|
<AccordionContent className="px-6 pb-6">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { HelpCircle } from "lucide-react";
|
import { HelpCircle } from "lucide-react";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
interface FilenameSettingsProps {
|
interface FilenameSettingsProps {
|
||||||
settings: ConversionSettings;
|
settings: ConversionSettings;
|
||||||
@@ -21,21 +22,22 @@ export function FilenameSettings({
|
|||||||
onApplyDefaultBaseNameToAll,
|
onApplyDefaultBaseNameToAll,
|
||||||
hasImages,
|
hasImages,
|
||||||
}: FilenameSettingsProps) {
|
}: FilenameSettingsProps) {
|
||||||
|
const t = useTranslations("FilenameSettings");
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch id="use-default-base-name" checked={settings.useDefaultBaseName} onCheckedChange={(checked) => onSettingsChange({ useDefaultBaseName: checked })} />
|
<Switch id="use-default-base-name" checked={settings.useDefaultBaseName} onCheckedChange={(checked) => onSettingsChange({ useDefaultBaseName: checked })} />
|
||||||
<Label htmlFor="use-default-base-name" className="flex items-center gap-1.5 cursor-pointer">
|
<Label htmlFor="use-default-base-name" className="flex items-center gap-1.5 cursor-pointer">
|
||||||
Use default base name
|
{t('useDefaultBaseName')}
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger onClick={(e) => e.preventDefault()}><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
<TooltipTrigger onClick={(e) => e.preventDefault()}><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||||
<TooltipContent><p>When enabled, all newly uploaded images will use the specified default base name.</p></TooltipContent>
|
<TooltipContent><p>{t('useDefaultBaseNameTooltip')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
{settings.useDefaultBaseName && (
|
{settings.useDefaultBaseName && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="default-base-name">Default base name</Label>
|
<Label htmlFor="default-base-name">{t('defaultBaseName')}</Label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Input
|
<Input
|
||||||
id="default-base-name"
|
id="default-base-name"
|
||||||
@@ -46,30 +48,30 @@ export function FilenameSettings({
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button variant="outline" size="sm" onClick={onApplyDefaultBaseNameToAll} disabled={!settings.defaultBaseName || !hasImages}>
|
<Button variant="outline" size="sm" onClick={onApplyDefaultBaseNameToAll} disabled={!settings.defaultBaseName || !hasImages}>
|
||||||
Apply to all
|
{t('applyToAll')}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent><p>Apply this base name to all currently uploaded images.</p></TooltipContent>
|
<TooltipContent><p>{t('applyToAllTooltip')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Label htmlFor="prefix">Prefix</Label>
|
<Label htmlFor="prefix">{t('prefix')}</Label>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||||
<TooltipContent><p>Add text to the beginning of every filename.</p></TooltipContent>
|
<TooltipContent><p>{t('prefixTooltip')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<Input id="prefix" placeholder="e.g., travel-" value={settings.prefix} onChange={(e) => onSettingsChange({ prefix: e.target.value })} />
|
<Input id="prefix" placeholder="e.g., travel-" value={settings.prefix} onChange={(e) => onSettingsChange({ prefix: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Label htmlFor="suffix">Suffix</Label>
|
<Label htmlFor="suffix">{t('suffix')}</Label>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||||
<TooltipContent><p>Add text to the end of every filename (before the number).</p></TooltipContent>
|
<TooltipContent><p>{t('suffixTooltip')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<Input id="suffix" placeholder="e.g., -edit" value={settings.suffix} onChange={(e) => onSettingsChange({ suffix: e.target.value })} />
|
<Input id="suffix" placeholder="e.g., -edit" value={settings.suffix} onChange={(e) => onSettingsChange({ suffix: e.target.value })} />
|
||||||
@@ -77,10 +79,10 @@ export function FilenameSettings({
|
|||||||
<div className="flex items-center space-x-2 pt-2">
|
<div className="flex items-center space-x-2 pt-2">
|
||||||
<Switch id="use-counter" checked={settings.useCounter} onCheckedChange={(checked) => onSettingsChange({ useCounter: checked })} />
|
<Switch id="use-counter" checked={settings.useCounter} onCheckedChange={(checked) => onSettingsChange({ useCounter: checked })} />
|
||||||
<Label htmlFor="use-counter" className="flex items-center gap-1.5 cursor-pointer">
|
<Label htmlFor="use-counter" className="flex items-center gap-1.5 cursor-pointer">
|
||||||
Add sequential number
|
{t('addSequentialNumber')}
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger onClick={(e) => e.preventDefault()}><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
<TooltipTrigger onClick={(e) => e.preventDefault()}><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||||
<TooltipContent><p>Append a numbered sequence to each filename.</p></TooltipContent>
|
<TooltipContent><p>{t('addSequentialNumberTooltip')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,10 +90,10 @@ export function FilenameSettings({
|
|||||||
<div className="grid grid-cols-2 gap-4 pt-2">
|
<div className="grid grid-cols-2 gap-4 pt-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Label htmlFor="counter-start">Start number</Label>
|
<Label htmlFor="counter-start">{t('startNumber')}</Label>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||||
<TooltipContent><p>The first number to use in the sequence.</p></TooltipContent>
|
<TooltipContent><p>{t('startNumberTooltip')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
@@ -104,10 +106,10 @@ export function FilenameSettings({
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Label htmlFor="counter-digits">Padding digits</Label>
|
<Label htmlFor="counter-digits">{t('paddingDigits')}</Label>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||||
<TooltipContent><p>Total number of digits for the counter, padded with leading zeros (e.g., 3 for 001).</p></TooltipContent>
|
<TooltipContent><p>{t('paddingDigitsTooltip')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -9,14 +9,7 @@ import { Checkbox } from "@/components/ui/checkbox";
|
|||||||
import { ArrowRightLeft, HelpCircle } from "lucide-react";
|
import { ArrowRightLeft, HelpCircle } from "lucide-react";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { ObjectPositionControl } from "@/components/object-position-control";
|
import { ObjectPositionControl } from "@/components/object-position-control";
|
||||||
|
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" },
|
|
||||||
];
|
|
||||||
|
|
||||||
interface ImageSettingsProps {
|
interface ImageSettingsProps {
|
||||||
settings: ConversionSettings;
|
settings: ConversionSettings;
|
||||||
@@ -31,14 +24,24 @@ export function ImageSettings({
|
|||||||
onAspectRatioChange,
|
onAspectRatioChange,
|
||||||
onSwapDimensions,
|
onSwapDimensions,
|
||||||
}: ImageSettingsProps) {
|
}: ImageSettingsProps) {
|
||||||
|
const t = useTranslations("ImageSettings");
|
||||||
|
|
||||||
|
const aspectRatios = [
|
||||||
|
{ name: t('custom'), value: "custom" },
|
||||||
|
{ name: t('square'), value: "1/1" },
|
||||||
|
{ name: t('standard'), value: "4/3" },
|
||||||
|
{ name: t('photography'), value: "3/2" },
|
||||||
|
{ name: t('widescreen'), value: "16/9" },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Label htmlFor="aspect-ratio">Aspect Ratio</Label>
|
<Label htmlFor="aspect-ratio">{t('aspectRatio')}</Label>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||||
<TooltipContent><p>Choose a preset aspect ratio or select 'Custom' to enter dimensions manually.</p></TooltipContent>
|
<TooltipContent><p>{t('aspectRatioTooltip')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<Select value={settings.aspectRatio} onValueChange={onAspectRatioChange}>
|
<Select value={settings.aspectRatio} onValueChange={onAspectRatioChange}>
|
||||||
@@ -53,10 +56,10 @@ export function ImageSettings({
|
|||||||
<div className="flex items-end gap-2">
|
<div className="flex items-end gap-2">
|
||||||
<div className="space-y-2 flex-1">
|
<div className="space-y-2 flex-1">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Label htmlFor="width">Width (px)</Label>
|
<Label htmlFor="width">{t('width')}</Label>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||||
<TooltipContent><p>Set the output width in pixels. Leave blank to use the original width.</p></TooltipContent>
|
<TooltipContent><p>{t('widthTooltip')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<Input id="width" type="number" placeholder="Auto" value={settings.width} onChange={(e) => { onSettingsChange({ width: e.target.value, aspectRatio: 'custom' }) }} />
|
<Input id="width" type="number" placeholder="Auto" value={settings.width} onChange={(e) => { onSettingsChange({ width: e.target.value, aspectRatio: 'custom' }) }} />
|
||||||
@@ -67,14 +70,14 @@ export function ImageSettings({
|
|||||||
<ArrowRightLeft className="h-4 w-4" />
|
<ArrowRightLeft className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent><p>Swap the entered width and height values.</p></TooltipContent>
|
<TooltipContent><p>{t('swapTooltip')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div className="space-y-2 flex-1">
|
<div className="space-y-2 flex-1">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Label htmlFor="height">Height (px)</Label>
|
<Label htmlFor="height">{t('height')}</Label>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||||
<TooltipContent><p>Set the output height in pixels. Leave blank to use the original height.</p></TooltipContent>
|
<TooltipContent><p>{t('heightTooltip')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<Input id="height" type="number" placeholder="Auto" value={settings.height} onChange={(e) => { onSettingsChange({ height: e.target.value, aspectRatio: 'custom' }) }} />
|
<Input id="height" type="number" placeholder="Auto" value={settings.height} onChange={(e) => { onSettingsChange({ height: e.target.value, aspectRatio: 'custom' }) }} />
|
||||||
@@ -83,37 +86,37 @@ export function ImageSettings({
|
|||||||
<div className="flex items-center space-x-2 pt-2">
|
<div className="flex items-center space-x-2 pt-2">
|
||||||
<Checkbox id="keep-orientation" checked={settings.keepOrientation} onCheckedChange={(checked) => onSettingsChange({ keepOrientation: Boolean(checked) })} />
|
<Checkbox id="keep-orientation" checked={settings.keepOrientation} onCheckedChange={(checked) => onSettingsChange({ keepOrientation: Boolean(checked) })} />
|
||||||
<Label htmlFor="keep-orientation" className="cursor-pointer flex items-center gap-1.5">
|
<Label htmlFor="keep-orientation" className="cursor-pointer flex items-center gap-1.5">
|
||||||
Keep original orientation
|
{t('keepOrientation')}
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger onClick={(e) => e.preventDefault()}><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
<TooltipTrigger onClick={(e) => e.preventDefault()}><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||||
<TooltipContent><p>Automatically swaps width and height to match the original image's orientation.</p></TooltipContent>
|
<TooltipContent><p>{t('keepOrientationTooltip')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 space-y-2">
|
<div className="mt-4 space-y-2">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Label htmlFor="scale-mode">Scaling</Label>
|
<Label htmlFor="scale-mode">{t('scaling')}</Label>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||||
<TooltipContent><p>Determines how the image fits into the new dimensions.</p></TooltipContent>
|
<TooltipContent><p>{t('scalingTooltip')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<Select value={settings.scaleMode} onValueChange={(value) => onSettingsChange({ scaleMode: value as any })}>
|
<Select value={settings.scaleMode} onValueChange={(value) => onSettingsChange({ scaleMode: value as any })}>
|
||||||
<SelectTrigger id="scale-mode"><SelectValue placeholder="Select scaling mode" /></SelectTrigger>
|
<SelectTrigger id="scale-mode"><SelectValue placeholder="Select scaling mode" /></SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="fill">Fill (stretch to fit)</SelectItem>
|
<SelectItem value="fill">{t('fill')}</SelectItem>
|
||||||
<SelectItem value="cover">Cover (crop to fit)</SelectItem>
|
<SelectItem value="cover">{t('cover')}</SelectItem>
|
||||||
<SelectItem value="contain">Contain (letterbox)</SelectItem>
|
<SelectItem value="contain">{t('contain')}</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
{settings.scaleMode !== 'fill' && (
|
{settings.scaleMode !== 'fill' && (
|
||||||
<div className="mt-4 space-y-2">
|
<div className="mt-4 space-y-2">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Label>Position</Label>
|
<Label>{t('position')}</Label>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||||
<TooltipContent><p>Sets the anchor point for 'Cover' or 'Contain' scaling.</p></TooltipContent>
|
<TooltipContent><p>{t('positionTooltip')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<ObjectPositionControl value={settings.objectPosition} onChange={(pos) => onSettingsChange({ objectPosition: pos as ObjectPosition })} />
|
<ObjectPositionControl value={settings.objectPosition} onChange={(pos) => onSettingsChange({ objectPosition: pos as ObjectPosition })} />
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
|||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { HelpCircle } from "lucide-react";
|
import { HelpCircle } from "lucide-react";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
interface QualitySettingsProps {
|
interface QualitySettingsProps {
|
||||||
settings: ConversionSettings;
|
settings: ConversionSettings;
|
||||||
@@ -13,14 +14,15 @@ interface QualitySettingsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function QualitySettings({ settings, onSettingsChange }: QualitySettingsProps) {
|
export function QualitySettings({ settings, onSettingsChange }: QualitySettingsProps) {
|
||||||
|
const t = useTranslations("QualitySettings");
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Label htmlFor="format">Format</Label>
|
<Label htmlFor="format">{t('format')}</Label>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||||
<TooltipContent><p>Choose the output file format for the images.</p></TooltipContent>
|
<TooltipContent><p>{t('formatTooltip')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<Select value={settings.format} onValueChange={(value: ImageFormat) => onSettingsChange({ format: value })}>
|
<Select value={settings.format} onValueChange={(value: ImageFormat) => onSettingsChange({ format: value })}>
|
||||||
@@ -35,10 +37,10 @@ export function QualitySettings({ settings, onSettingsChange }: QualitySettingsP
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Label htmlFor="quality">Quality</Label>
|
<Label htmlFor="quality">{t('quality')}</Label>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||||
<TooltipContent><p>Set compression quality for JPEG/WEBP. Higher is better quality but larger file size.</p></TooltipContent>
|
<TooltipContent><p>{t('qualityTooltip')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-muted-foreground">{settings.quality}%</span>
|
<span className="text-sm text-muted-foreground">{settings.quality}%</span>
|
||||||
@@ -53,7 +55,7 @@ export function QualitySettings({ settings, onSettingsChange }: QualitySettingsP
|
|||||||
disabled={settings.format === 'png'}
|
disabled={settings.format === 'png'}
|
||||||
/>
|
/>
|
||||||
{settings.format === 'png' && (
|
{settings.format === 'png' && (
|
||||||
<p className="text-xs text-muted-foreground pt-1">Quality slider is disabled for PNG (lossless format).</p>
|
<p className="text-xs text-muted-foreground pt-1">{t('pngWarning')}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
14
src/i18n.ts
Normal file
14
src/i18n.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import {getRequestConfig} from 'next-intl/server';
|
||||||
|
|
||||||
|
export const locales = ['en'];
|
||||||
|
export const defaultLocale = 'en';
|
||||||
|
|
||||||
|
export default getRequestConfig(async ({locale}) => {
|
||||||
|
if (!locales.includes(locale as any)) {
|
||||||
|
locale = defaultLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
messages: (await import(`../messages/${locale}.json`)).default
|
||||||
|
};
|
||||||
|
});
|
||||||
10
src/middleware.ts
Normal file
10
src/middleware.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import createMiddleware from 'next-intl/middleware';
|
||||||
|
|
||||||
|
export default createMiddleware({
|
||||||
|
locales: ['en'],
|
||||||
|
defaultLocale: 'en'
|
||||||
|
});
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)']
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user