[dyad] Added language switcher - wrote 10 file(s), renamed 2 file(s), added next-intl package(s)
This commit is contained in:
5
i18n.ts
Normal file
5
i18n.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import {getRequestConfig} from 'next-intl/server';
|
||||
|
||||
export default getRequestConfig(async ({locale}) => ({
|
||||
messages: (await import(`./messages/${locale}.json`)).default
|
||||
}));
|
||||
87
messages/en.json
Normal file
87
messages/en.json
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"HomePage": {
|
||||
"title": "Image Web Exporter",
|
||||
"description": "Upload a picture, then export it in a different resolution and format."
|
||||
},
|
||||
"LanguageSwitcher": {
|
||||
"placeholder": "Language"
|
||||
},
|
||||
"ImageConverter": {
|
||||
"uploadTitle": "Upload Images",
|
||||
"uploadButton": "Click or drag and drop to upload",
|
||||
"uploadHint": "PNG, JPG, WEBP supported",
|
||||
"uploadedImagesTitle": "Uploaded Images",
|
||||
"clearAll": "Clear All",
|
||||
"downloadAll": "Download All ({count})",
|
||||
"converting": "Converting...",
|
||||
"baseNameLabel": "Base Name",
|
||||
"finalNameLabel": "Final name: {filename}",
|
||||
"downloadImageTooltip": "Download this image",
|
||||
"removeImageTooltip": "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.",
|
||||
"customAspectRatio": "Custom",
|
||||
"squareAspectRatio": "1:1 (Square)",
|
||||
"standardAspectRatio": "4:3 (Standard)",
|
||||
"photoAspectRatio": "3:2 (Photography)",
|
||||
"widescreenAspectRatio": "16:9 (Widescreen)",
|
||||
"widthLabel": "Width (px)",
|
||||
"widthTooltip": "Set the output width in pixels. Leave blank to use the original width.",
|
||||
"heightLabel": "Height (px)",
|
||||
"heightTooltip": "Set the output height in pixels. Leave blank to use the original height.",
|
||||
"originalPlaceholder": "Original",
|
||||
"swapDimensionsTooltip": "Swap the entered width and height values.",
|
||||
"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.",
|
||||
"scalingFill": "Fill (stretch to fit)",
|
||||
"scalingCover": "Cover (crop to fit)",
|
||||
"scalingContain": "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",
|
||||
"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.",
|
||||
"suffixLabel": "Suffix",
|
||||
"suffixTooltip": "Add text to the end of every filename (before the number).",
|
||||
"addSequentialNumberLabel": "Add sequential number",
|
||||
"addSequentialNumberTooltip": "Append a numbered sequence to each filename.",
|
||||
"startNumberLabel": "Start number",
|
||||
"startNumberTooltip": "The first number to use in the sequence.",
|
||||
"paddingDigitsLabel": "Padding digits",
|
||||
"paddingDigitsTooltip": "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.",
|
||||
"qualityLabel": "Quality",
|
||||
"qualityTooltip": "Set compression quality for JPEG/WEBP. Higher is better quality but larger file size.",
|
||||
"qualityDisabledHint": "Quality slider is disabled for PNG (lossless format).",
|
||||
"applySettings": "Apply Settings",
|
||||
"applySettingsTooltip": "Confirm and apply all the settings above. This does not download the images.",
|
||||
"toasts": {
|
||||
"noImages": "Please upload images first.",
|
||||
"conversionStarting": "Starting conversion for {count} images...",
|
||||
"conversionSuccess": "Successfully exported all {count} images!",
|
||||
"conversionError": "An unknown error occurred during conversion.",
|
||||
"singleConversionStarting": "Starting conversion for {filename}...",
|
||||
"singleConversionSuccess": "Successfully exported {filename}!",
|
||||
"settingsApplied": "Settings updated and will be used for all downloads.",
|
||||
"noValidImages": "No valid image files found.",
|
||||
"imagesAdded": "{count} image(s) added.",
|
||||
"allCleared": "All images cleared.",
|
||||
"noDefaultBaseName": "Please enter a default base name to apply.",
|
||||
"uploadImagesFirst": "Upload some images first.",
|
||||
"baseNameApplied": "Set base name to \\\"{baseName}\\\" for all {count} images."
|
||||
}
|
||||
}
|
||||
}
|
||||
87
messages/es.json
Normal file
87
messages/es.json
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"HomePage": {
|
||||
"title": "Exportador de Imágenes Web",
|
||||
"description": "Sube una imagen, luego expórtala en una resolución y formato diferente."
|
||||
},
|
||||
"LanguageSwitcher": {
|
||||
"placeholder": "Idioma"
|
||||
},
|
||||
"ImageConverter": {
|
||||
"uploadTitle": "Subir Imágenes",
|
||||
"uploadButton": "Haz clic o arrastra y suelta para subir",
|
||||
"uploadHint": "Soporta PNG, JPG, WEBP",
|
||||
"uploadedImagesTitle": "Imágenes Subidas",
|
||||
"clearAll": "Limpiar Todo",
|
||||
"downloadAll": "Descargar Todo ({count})",
|
||||
"converting": "Convirtiendo...",
|
||||
"baseNameLabel": "Nombre Base",
|
||||
"finalNameLabel": "Nombre final: {filename}",
|
||||
"downloadImageTooltip": "Descargar esta imagen",
|
||||
"removeImageTooltip": "Eliminar esta imagen",
|
||||
"imageSettingsTitle": "Ajustes de Imagen",
|
||||
"imageSettingsDescription": "Ajusta la resolución y el escalado para todas las imágenes.",
|
||||
"aspectRatioLabel": "Relación de Aspecto",
|
||||
"aspectRatioTooltip": "Elige una relación de aspecto preestablecida o selecciona 'Personalizado' para introducir las dimensiones manualmente.",
|
||||
"customAspectRatio": "Personalizado",
|
||||
"squareAspectRatio": "1:1 (Cuadrado)",
|
||||
"standardAspectRatio": "4:3 (Estándar)",
|
||||
"photoAspectRatio": "3:2 (Fotografía)",
|
||||
"widescreenAspectRatio": "16:9 (Panorámico)",
|
||||
"widthLabel": "Ancho (px)",
|
||||
"widthTooltip": "Establece el ancho de salida en píxeles. Déjalo en blanco para usar el ancho original.",
|
||||
"heightLabel": "Alto (px)",
|
||||
"heightTooltip": "Establece la altura de salida en píxeles. Déjala en blanco para usar la altura original.",
|
||||
"originalPlaceholder": "Original",
|
||||
"swapDimensionsTooltip": "Intercambia los valores de ancho y alto introducidos.",
|
||||
"keepOrientationLabel": "Mantener orientación original",
|
||||
"keepOrientationTooltip": "Intercambia automáticamente el ancho y el alto para que coincida con la orientación de la imagen original.",
|
||||
"scalingLabel": "Escalado",
|
||||
"scalingTooltip": "Determina cómo se ajusta la imagen a las nuevas dimensiones.",
|
||||
"scalingFill": "Rellenar (estirar para ajustar)",
|
||||
"scalingCover": "Cubrir (recortar para ajustar)",
|
||||
"scalingContain": "Contener (letterbox)",
|
||||
"positionLabel": "Posición",
|
||||
"positionTooltip": "Establece el punto de anclaje para el escalado 'Cubrir' o 'Contener'.",
|
||||
"filenameSettingsTitle": "Ajustes de Nombre de Archivo",
|
||||
"filenameSettingsDescription": "Personaliza los nombres de los archivos de salida.",
|
||||
"useDefaultBaseNameLabel": "Usar nombre base por defecto",
|
||||
"useDefaultBaseNameTooltip": "Cuando está activado, todas las imágenes recién subidas usarán el nombre base por defecto especificado.",
|
||||
"defaultBaseNameLabel": "Nombre base por defecto",
|
||||
"applyToAll": "Aplicar a todo",
|
||||
"applyToAllTooltip": "Aplica este nombre base a todas las imágenes subidas actualmente.",
|
||||
"prefixLabel": "Prefijo",
|
||||
"prefixTooltip": "Añade texto al principio de cada nombre de archivo.",
|
||||
"suffixLabel": "Sufijo",
|
||||
"suffixTooltip": "Añade texto al final de cada nombre de archivo (antes del número).",
|
||||
"addSequentialNumberLabel": "Añadir número secuencial",
|
||||
"addSequentialNumberTooltip": "Añade una secuencia numerada a cada nombre de archivo.",
|
||||
"startNumberLabel": "Número inicial",
|
||||
"startNumberTooltip": "El primer número a usar en la secuencia.",
|
||||
"paddingDigitsLabel": "Dígitos de relleno",
|
||||
"paddingDigitsTooltip": "Número total de dígitos para el contador, rellenado con ceros a la izquierda (ej. 3 para 001).",
|
||||
"qualitySettingsTitle": "Ajustes de Calidad",
|
||||
"qualitySettingsDescription": "Elige el formato y el nivel de compresión.",
|
||||
"formatLabel": "Formato",
|
||||
"formatTooltip": "Elige el formato de archivo de salida para las imágenes.",
|
||||
"qualityLabel": "Calidad",
|
||||
"qualityTooltip": "Establece la calidad de compresión para JPEG/WEBP. Más alto es mejor calidad pero mayor tamaño de archivo.",
|
||||
"qualityDisabledHint": "El control de calidad está desactivado para PNG (formato sin pérdidas).",
|
||||
"applySettings": "Aplicar Ajustes",
|
||||
"applySettingsTooltip": "Confirma y aplica todos los ajustes anteriores. Esto no descarga las imágenes.",
|
||||
"toasts": {
|
||||
"noImages": "Por favor, sube imágenes primero.",
|
||||
"conversionStarting": "Iniciando conversión para {count} imágenes...",
|
||||
"conversionSuccess": "¡Se exportaron con éxito todas las {count} imágenes!",
|
||||
"conversionError": "Ocurrió un error desconocido durante la conversión.",
|
||||
"singleConversionStarting": "Iniciando conversión para {filename}...",
|
||||
"singleConversionSuccess": "¡Se exportó con éxito {filename}!",
|
||||
"settingsApplied": "Ajustes actualizados y se usarán para todas las descargas.",
|
||||
"noValidImages": "No se encontraron archivos de imagen válidos.",
|
||||
"imagesAdded": "{count} imagen(es) añadida(s).",
|
||||
"allCleared": "Todas las imágenes eliminadas.",
|
||||
"noDefaultBaseName": "Por favor, introduce un nombre base por defecto para aplicar.",
|
||||
"uploadImagesFirst": "Sube algunas imágenes primero.",
|
||||
"baseNameApplied": "Se estableció el nombre base a \\\"{baseName}\\\" para todas las {count} imágenes."
|
||||
}
|
||||
}
|
||||
}
|
||||
10
middleware.ts
Normal file
10
middleware.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import createMiddleware from 'next-intl/middleware';
|
||||
|
||||
export default createMiddleware({
|
||||
locales: ['en', 'es'],
|
||||
defaultLocale: 'en'
|
||||
});
|
||||
|
||||
export const config = {
|
||||
matcher: ['/', '/(es|en)/:path*']
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
403
pnpm-lock.yaml
generated
403
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import "../globals.css";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { NextIntlClientProvider, useMessages } from "next-intl";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -21,23 +22,29 @@ export const metadata: Metadata = {
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
params: { locale },
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
params: { locale: string };
|
||||
}>) {
|
||||
const messages = useMessages();
|
||||
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
@@ -1,15 +1,22 @@
|
||||
import { ImageConverter } from "@/components/image-converter";
|
||||
import { LanguageSwitcher } from "@/components/language-switcher";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function Home() {
|
||||
const t = useTranslations("HomePage");
|
||||
|
||||
return (
|
||||
<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="absolute top-4 right-4 z-20">
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
<main className="flex flex-col items-center w-full max-w-6xl z-10">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100 sm:text-5xl">
|
||||
Image Web Exporter
|
||||
{t("title")}
|
||||
</h1>
|
||||
<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("description")}
|
||||
</p>
|
||||
</div>
|
||||
<ImageConverter />
|
||||
@@ -36,16 +36,19 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
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" },
|
||||
];
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export function ImageConverter() {
|
||||
const t = useTranslations("ImageConverter");
|
||||
|
||||
const aspectRatios = [
|
||||
{ name: t("customAspectRatio"), value: "custom" },
|
||||
{ name: t("squareAspectRatio"), value: "1/1" },
|
||||
{ name: t("standardAspectRatio"), value: "4/3" },
|
||||
{ name: t("photoAspectRatio"), value: "3/2" },
|
||||
{ name: t("widescreenAspectRatio"), value: "16/9" },
|
||||
];
|
||||
|
||||
const [images, setImages] = useState<File[]>([]);
|
||||
const [previewUrls, setPreviewUrls] = useState<string[]>([]);
|
||||
const [filenames, setFilenames] = useState<string[]>([]);
|
||||
@@ -86,7 +89,7 @@ export function ImageConverter() {
|
||||
);
|
||||
|
||||
if (imageFiles.length === 0) {
|
||||
toast.error("No valid image files found.");
|
||||
toast.error(t("toasts.noValidImages"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -108,7 +111,7 @@ export function ImageConverter() {
|
||||
setPreviewUrls(newPreviewUrls);
|
||||
setFilenames(newFilenames);
|
||||
|
||||
toast.success(`${imageFiles.length} image(s) added.`);
|
||||
toast.success(t("toasts.imagesAdded", { count: imageFiles.length }));
|
||||
};
|
||||
|
||||
const handleImageChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -148,7 +151,7 @@ export function ImageConverter() {
|
||||
setImages([]);
|
||||
setPreviewUrls([]);
|
||||
setFilenames([]);
|
||||
toast.info("All images cleared.");
|
||||
toast.info(t("toasts.allCleared"));
|
||||
};
|
||||
|
||||
const handleFilenameChange = (index: number, newName: string) => {
|
||||
@@ -260,12 +263,12 @@ export function ImageConverter() {
|
||||
|
||||
const handleConvertAndDownloadAll = async () => {
|
||||
if (images.length === 0) {
|
||||
toast.error("Please upload images first.");
|
||||
toast.error(t("toasts.noImages"));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsConverting(true);
|
||||
toast.info(`Starting conversion for ${images.length} images...`);
|
||||
toast.info(t("toasts.conversionStarting", { count: images.length }));
|
||||
|
||||
const conversionPromises = images.map((image, index) =>
|
||||
convertAndDownload(image, previewUrls[index], index)
|
||||
@@ -273,12 +276,12 @@ export function ImageConverter() {
|
||||
|
||||
try {
|
||||
await Promise.all(conversionPromises);
|
||||
toast.success(`Successfully exported all ${images.length} images!`);
|
||||
toast.success(t("toasts.conversionSuccess", { count: images.length }));
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
toast.error("An unknown error occurred during conversion.");
|
||||
toast.error(t("toasts.conversionError"));
|
||||
}
|
||||
} finally {
|
||||
setIsConverting(false);
|
||||
@@ -287,16 +290,16 @@ export function ImageConverter() {
|
||||
|
||||
const handleConvertAndDownloadSingle = async (index: number) => {
|
||||
setConvertingIndex(index);
|
||||
toast.info(`Starting conversion for ${filenames[index]}...`);
|
||||
toast.info(t("toasts.singleConversionStarting", { filename: filenames[index] }));
|
||||
|
||||
try {
|
||||
await convertAndDownload(images[index], previewUrls[index], index);
|
||||
toast.success(`Successfully exported ${filenames[index]}!`);
|
||||
toast.success(t("toasts.singleConversionSuccess", { filename: filenames[index] }));
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
toast.error("An unknown error occurred during conversion.");
|
||||
toast.error(t("toasts.conversionError"));
|
||||
}
|
||||
} finally {
|
||||
setConvertingIndex(null);
|
||||
@@ -304,7 +307,7 @@ export function ImageConverter() {
|
||||
};
|
||||
|
||||
const handleApplySettings = () => {
|
||||
toast.info("Settings updated and will be used for all downloads.");
|
||||
toast.info(t("toasts.settingsApplied"));
|
||||
};
|
||||
|
||||
const handleAspectRatioChange = (value: string) => {
|
||||
@@ -352,16 +355,16 @@ export function ImageConverter() {
|
||||
|
||||
const handleApplyDefaultBaseNameToAll = () => {
|
||||
if (!defaultBaseName) {
|
||||
toast.error("Please enter a default base name to apply.");
|
||||
toast.error(t("toasts.noDefaultBaseName"));
|
||||
return;
|
||||
}
|
||||
if (!hasImages) {
|
||||
toast.info("Upload some images first.");
|
||||
toast.info(t("toasts.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", { baseName: defaultBaseName, count: images.length }));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -372,9 +375,9 @@ export function ImageConverter() {
|
||||
<AccordionItem value="image-settings" className="border rounded-lg bg-card">
|
||||
<AccordionTrigger className="p-6 hover:no-underline">
|
||||
<div className="text-left">
|
||||
<h3 className="text-lg font-medium leading-none">Image Settings</h3>
|
||||
<h3 className="text-lg font-medium leading-none">{t("imageSettingsTitle")}</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Adjust resolution and scaling for all images.
|
||||
{t("imageSettingsDescription")}
|
||||
</p>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
@@ -382,19 +385,19 @@ export function ImageConverter() {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="aspect-ratio">Aspect Ratio</Label>
|
||||
<Label htmlFor="aspect-ratio">{t("aspectRatioLabel")}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Choose a preset aspect ratio or select 'Custom' to enter dimensions manually.</p>
|
||||
<p>{t("aspectRatioTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Select value={aspectRatio} onValueChange={handleAspectRatioChange}>
|
||||
<SelectTrigger id="aspect-ratio" className="mt-2">
|
||||
<SelectValue placeholder="Select aspect ratio" />
|
||||
<SelectValue placeholder={t("aspectRatioLabel")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{aspectRatios.map((ratio) => (
|
||||
@@ -408,17 +411,17 @@ export function ImageConverter() {
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="space-y-2 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="width">Width (px)</Label>
|
||||
<Label htmlFor="width">{t("widthLabel")}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Set the output width in pixels. Leave blank to use the original width.</p>
|
||||
<p>{t("widthTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Input id="width" type="number" placeholder="Original" value={width} onChange={handleWidthChange} />
|
||||
<Input id="width" type="number" placeholder={t("originalPlaceholder")} value={width} onChange={handleWidthChange} />
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -427,34 +430,34 @@ export function ImageConverter() {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Swap the entered width and height values.</p>
|
||||
<p>{t("swapDimensionsTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="space-y-2 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="height">Height (px)</Label>
|
||||
<Label htmlFor="height">{t("heightLabel")}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Set the output height in pixels. Leave blank to use the original height.</p>
|
||||
<p>{t("heightTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Input id="height" type="number" placeholder="Original" value={height} onChange={handleHeightChange} />
|
||||
<Input id="height" type="number" placeholder={t("originalPlaceholder")} value={height} onChange={handleHeightChange} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 pt-2">
|
||||
<Checkbox id="keep-orientation" checked={keepOrientation} onCheckedChange={(checked) => setKeepOrientation(Boolean(checked))} />
|
||||
<Label htmlFor="keep-orientation" className="cursor-pointer flex items-center gap-1.5">
|
||||
Keep original orientation
|
||||
{t("keepOrientationLabel")}
|
||||
<Tooltip>
|
||||
<TooltipTrigger onClick={(e) => e.preventDefault()}>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Automatically swaps width and height to match the original image's orientation.</p>
|
||||
<p>{t("keepOrientationTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</Label>
|
||||
@@ -462,35 +465,35 @@ export function ImageConverter() {
|
||||
</div>
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="scale-mode">Scaling</Label>
|
||||
<Label htmlFor="scale-mode">{t("scalingLabel")}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Determines how the image fits into the new dimensions.</p>
|
||||
<p>{t("scalingTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Select value={scaleMode} onValueChange={(value: 'fill' | 'cover' | 'contain') => setScaleMode(value)}>
|
||||
<SelectTrigger id="scale-mode"><SelectValue placeholder="Select scaling mode" /></SelectTrigger>
|
||||
<SelectTrigger id="scale-mode"><SelectValue placeholder={t("scalingLabel")} /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="fill">Fill (stretch to fit)</SelectItem>
|
||||
<SelectItem value="cover">Cover (crop to fit)</SelectItem>
|
||||
<SelectItem value="contain">Contain (letterbox)</SelectItem>
|
||||
<SelectItem value="fill">{t("scalingFill")}</SelectItem>
|
||||
<SelectItem value="cover">{t("scalingCover")}</SelectItem>
|
||||
<SelectItem value="contain">{t("scalingContain")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{scaleMode !== 'fill' && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label>Position</Label>
|
||||
<Label>{t("positionLabel")}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Sets the anchor point for 'Cover' or 'Contain' scaling.</p>
|
||||
<p>{t("positionTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -503,8 +506,8 @@ export function ImageConverter() {
|
||||
<AccordionItem value="filename-settings" className="border rounded-lg bg-card">
|
||||
<AccordionTrigger className="p-6 hover:no-underline">
|
||||
<div className="text-left">
|
||||
<h3 className="text-lg font-medium leading-none">Filename Settings</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">Customize the output filenames.</p>
|
||||
<h3 className="text-lg font-medium leading-none">{t("filenameSettingsTitle")}</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t("filenameSettingsDescription")}</p>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6">
|
||||
@@ -512,20 +515,20 @@ export function ImageConverter() {
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="use-default-base-name" checked={useDefaultBaseName} onCheckedChange={setUseDefaultBaseName} />
|
||||
<Label htmlFor="use-default-base-name" className="flex items-center gap-1.5 cursor-pointer">
|
||||
Use default base name
|
||||
{t("useDefaultBaseNameLabel")}
|
||||
<Tooltip>
|
||||
<TooltipTrigger onClick={(e) => e.preventDefault()}>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>When enabled, all newly uploaded images will use the specified default base name.</p>
|
||||
<p>{t("useDefaultBaseNameTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</Label>
|
||||
</div>
|
||||
{useDefaultBaseName && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="default-base-name">Default base name</Label>
|
||||
<Label htmlFor="default-base-name">{t("defaultBaseNameLabel")}</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
id="default-base-name"
|
||||
@@ -536,11 +539,11 @@ export function ImageConverter() {
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline" size="sm" onClick={handleApplyDefaultBaseNameToAll} disabled={!defaultBaseName || !hasImages}>
|
||||
Apply to all
|
||||
{t("applyToAll")}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Apply this base name to all currently uploaded images.</p>
|
||||
<p>{t("applyToAllTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -548,13 +551,13 @@ export function ImageConverter() {
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="prefix">Prefix</Label>
|
||||
<Label htmlFor="prefix">{t("prefixLabel")}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Add text to the beginning of every filename.</p>
|
||||
<p>{t("prefixTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -562,13 +565,13 @@ export function ImageConverter() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="suffix">Suffix</Label>
|
||||
<Label htmlFor="suffix">{t("suffixLabel")}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Add text to the end of every filename (before the number).</p>
|
||||
<p>{t("suffixTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -577,13 +580,13 @@ export function ImageConverter() {
|
||||
<div className="flex items-center space-x-2 pt-2">
|
||||
<Switch id="use-counter" checked={useCounter} onCheckedChange={setUseCounter} />
|
||||
<Label htmlFor="use-counter" className="flex items-center gap-1.5 cursor-pointer">
|
||||
Add sequential number
|
||||
{t("addSequentialNumberLabel")}
|
||||
<Tooltip>
|
||||
<TooltipTrigger onClick={(e) => e.preventDefault()}>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Append a numbered sequence to each filename.</p>
|
||||
<p>{t("addSequentialNumberTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</Label>
|
||||
@@ -592,13 +595,13 @@ export function ImageConverter() {
|
||||
<div className="grid grid-cols-2 gap-4 pt-2">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="counter-start">Start number</Label>
|
||||
<Label htmlFor="counter-start">{t("startNumberLabel")}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>The first number to use in the sequence.</p>
|
||||
<p>{t("startNumberTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -612,13 +615,13 @@ export function ImageConverter() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="counter-digits">Padding digits</Label>
|
||||
<Label htmlFor="counter-digits">{t("paddingDigitsLabel")}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Total number of digits for the counter, padded with leading zeros (e.g., 3 for 001).</p>
|
||||
<p>{t("paddingDigitsTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -639,26 +642,26 @@ export function ImageConverter() {
|
||||
<AccordionItem value="quality-settings" className="border rounded-lg bg-card">
|
||||
<AccordionTrigger className="p-6 hover:no-underline">
|
||||
<div className="text-left">
|
||||
<h3 className="text-lg font-medium leading-none">Quality Settings</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">Choose format and compression level.</p>
|
||||
<h3 className="text-lg font-medium leading-none">{t("qualitySettingsTitle")}</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t("qualitySettingsDescription")}</p>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6">
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="format">Format</Label>
|
||||
<Label htmlFor="format">{t("formatLabel")}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Choose the output file format for the images.</p>
|
||||
<p>{t("formatTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Select value={format} onValueChange={(value: "png" | "jpeg" | "webp") => setFormat(value)}>
|
||||
<SelectTrigger id="format"><SelectValue placeholder="Select format" /></SelectTrigger>
|
||||
<SelectTrigger id="format"><SelectValue placeholder={t("formatLabel")} /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="png">PNG</SelectItem>
|
||||
<SelectItem value="jpeg">JPEG</SelectItem>
|
||||
@@ -669,13 +672,13 @@ export function ImageConverter() {
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="quality">Quality</Label>
|
||||
<Label htmlFor="quality">{t("qualityLabel")}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Set compression quality for JPEG/WEBP. Higher is better quality but larger file size.</p>
|
||||
<p>{t("qualityTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -691,7 +694,7 @@ export function ImageConverter() {
|
||||
disabled={format === 'png'}
|
||||
/>
|
||||
{format === 'png' && (
|
||||
<p className="text-xs text-muted-foreground pt-1">Quality slider is disabled for PNG (lossless format).</p>
|
||||
<p className="text-xs text-muted-foreground pt-1">{t("qualityDisabledHint")}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -706,11 +709,11 @@ export function ImageConverter() {
|
||||
className="w-full"
|
||||
>
|
||||
<Check className="mr-2 h-4 w-4" />
|
||||
Apply Settings
|
||||
{t("applySettings")}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Confirm and apply all the settings above. This does not download the images.</p>
|
||||
<p>{t("applySettingsTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -719,7 +722,7 @@ export function ImageConverter() {
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">Upload Images</h3>
|
||||
<h3 className="text-lg font-medium">{t("uploadTitle")}</h3>
|
||||
<div
|
||||
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",
|
||||
@@ -732,8 +735,8 @@ export function ImageConverter() {
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center text-center text-muted-foreground">
|
||||
<Upload className="w-8 h-8 mb-2" />
|
||||
<p className="font-semibold">Click or drag and drop to upload</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">PNG, JPG, WEBP supported</p>
|
||||
<p className="font-semibold">{t("uploadButton")}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">{t("uploadHint")}</p>
|
||||
</div>
|
||||
<Input type="file" ref={fileInputRef} onChange={handleImageChange} className="hidden" accept="image/*" multiple />
|
||||
</div>
|
||||
@@ -745,25 +748,25 @@ export function ImageConverter() {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-center">
|
||||
<CardTitle>Uploaded Images</CardTitle>
|
||||
<CardTitle>{t("uploadedImagesTitle")}</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="sm" onClick={handleClearAll} disabled={isConverting || convertingIndex !== null}><Trash2 className="mr-2 h-4 w-4" />Clear All</Button>
|
||||
<Button variant="ghost" size="sm" onClick={handleClearAll} disabled={isConverting || convertingIndex !== null}><Trash2 className="mr-2 h-4 w-4" />{t("clearAll")}</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Remove all uploaded images.</p>
|
||||
<p>{t("removeImageTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button onClick={handleConvertAndDownloadAll} disabled={!hasImages || isConverting || convertingIndex !== null}>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
{isConverting ? "Converting..." : `Download All (${images.length})`}
|
||||
{isConverting ? t("converting") : t("downloadAll", { count: images.length })}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Convert and download all images with the current settings.</p>
|
||||
<p>{t("downloadImageTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -779,7 +782,7 @@ export function ImageConverter() {
|
||||
<div key={url} className="p-4 border rounded-lg flex items-center gap-4">
|
||||
<img src={url} alt={`Preview ${index + 1}`} className="w-20 h-20 object-cover rounded-md shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<Label htmlFor={`filename-${index}`} className="text-xs text-muted-foreground">Base Name</Label>
|
||||
<Label htmlFor={`filename-${index}`} className="text-xs text-muted-foreground">{t("baseNameLabel")}</Label>
|
||||
<Input
|
||||
id={`filename-${index}`}
|
||||
value={filenames[index]}
|
||||
@@ -787,7 +790,7 @@ export function ImageConverter() {
|
||||
className="text-sm font-medium h-8 mt-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground truncate mt-1" title={`${finalFilename}.${format}`}>
|
||||
Final name: {finalFilename}.{format}
|
||||
{t("finalNameLabel", { filename: `${finalFilename}.${format}` })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center shrink-0">
|
||||
@@ -804,7 +807,7 @@ export function ImageConverter() {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Download this image</p>
|
||||
<p>{t("downloadImageTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
@@ -820,7 +823,7 @@ export function ImageConverter() {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Remove this image</p>
|
||||
<p>{t("removeImageTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
34
src/components/language-switcher.tsx
Normal file
34
src/components/language-switcher.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { useLocale, useTranslations } from "next-intl";
|
||||
import { useRouter, usePathname } from "next-intl/client";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
export function LanguageSwitcher() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const locale = useLocale();
|
||||
const t = useTranslations("LanguageSwitcher");
|
||||
|
||||
const onSelectChange = (value: string) => {
|
||||
router.replace(pathname, { locale: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<Select defaultValue={locale} onValueChange={onSelectChange}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder={t("placeholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
<SelectItem value="es">Español</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user